From 578e1317a08b68eb0f9df0945fe671ad5de4dd19 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Wed, 1 Jul 2020 17:19:56 +1200 Subject: [PATCH 001/289] Introduce IContentNestedDataSerializer to allow injecting a custom serializer for nucache --- .../PublishedContent/NuCacheChildrenTests.cs | 5 +- .../PublishedContent/NuCacheTests.cs | 5 +- .../Scoping/ScopedNuCacheTests.cs | 6 +- .../ContentTypeServiceVariantsTests.cs | 7 +- .../NuCache/DataSource/ContentNestedData.cs | 2 +- .../NuCache/DataSource/CultureVariation.cs | 2 +- .../NuCache/DataSource/DatabaseDataSource.cs | 31 ++-- .../IContentNestedDataSerializer.cs | 14 ++ .../JsonContentNestedDataSerializer.cs | 31 ++++ ...opertiesJsonContentNestedDataSerializer.cs | 157 ++++++++++++++++++ .../NuCache/DataSource/PropertyData.cs | 2 +- .../PublishedCache/NuCache/NuCacheComposer.cs | 3 + .../NuCache/PublishedSnapshotService.cs | 6 +- src/Umbraco.Web/Umbraco.Web.csproj | 2 + 14 files changed, 245 insertions(+), 28 deletions(-) create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/MappedPropertiesJsonContentNestedDataSerializer.cs diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index 834d211994..fef096498c 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -37,6 +37,7 @@ namespace Umbraco.Tests.PublishedContent private ContentType _contentTypeInvariant; private ContentType _contentTypeVariant; private TestDataSource _source; + private IContentNestedDataSerializer _contentNestedDataSerializer; [TearDown] public void Teardown() @@ -134,6 +135,7 @@ namespace Umbraco.Tests.PublishedContent // create a data source for NuCache _source = new TestDataSource(kits()); + _contentNestedDataSerializer = new JsonContentNestedDataSerializer(); // at last, create the complete NuCache snapshot service! var options = new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }; @@ -155,7 +157,8 @@ namespace Umbraco.Tests.PublishedContent globalSettings, Mock.Of(), Mock.Of(), - new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() })); + new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), + _contentNestedDataSerializer); // invariant is the current default _variationAccesor.VariationContext = new VariationContext(); diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs index 0e05e6baad..792ccc8529 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs @@ -33,6 +33,7 @@ namespace Umbraco.Tests.PublishedContent { private IPublishedSnapshotService _snapshotService; private IVariationContextAccessor _variationAccesor; + private IContentNestedDataSerializer _contentNestedDataSerializer; private ContentType _contentType; private PropertyType _propertyType; @@ -114,6 +115,7 @@ namespace Umbraco.Tests.PublishedContent // create a data source for NuCache var dataSource = new TestDataSource(kit); + _contentNestedDataSerializer = new JsonContentNestedDataSerializer(); var runtime = Mock.Of(); Mock.Get(runtime).Setup(x => x.Level).Returns(RuntimeLevel.Run); @@ -201,7 +203,8 @@ namespace Umbraco.Tests.PublishedContent globalSettings, Mock.Of(), Mock.Of(), - new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() })); + new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), + _contentNestedDataSerializer); // invariant is the current default _variationAccesor.VariationContext = new VariationContext(); diff --git a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs index c7c403b260..5f72947382 100644 --- a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs @@ -82,6 +82,7 @@ namespace Umbraco.Tests.Scoping var mediaRepository = Mock.Of(); var memberRepository = Mock.Of(); + var nestedContentDataSerializer = new JsonContentNestedDataSerializer(); return new PublishedSnapshotService( options, null, @@ -95,11 +96,12 @@ namespace Umbraco.Tests.Scoping ScopeProvider, documentRepository, mediaRepository, memberRepository, DefaultCultureAccessor, - new DatabaseDataSource(), + new DatabaseDataSource(nestedContentDataSerializer), Factory.GetInstance(), Factory.GetInstance(), Mock.Of(), - new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() })); + new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), + nestedContentDataSerializer); } protected UmbracoContext GetUmbracoContextNu(string url, int templateId = 1234, RouteData routeData = null, bool setSingleton = false, IUmbracoSettingsSection umbracoSettings = null, IEnumerable urlProviders = null) diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index 9391b7442f..938b14c3a9 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -53,6 +53,8 @@ namespace Umbraco.Tests.Services var mediaRepository = Mock.Of(); var memberRepository = Mock.Of(); + var nestedContentDataSerializer = new JsonContentNestedDataSerializer(); + return new PublishedSnapshotService( options, null, @@ -66,11 +68,12 @@ namespace Umbraco.Tests.Services ScopeProvider, documentRepository, mediaRepository, memberRepository, DefaultCultureAccessor, - new DatabaseDataSource(), + new DatabaseDataSource(nestedContentDataSerializer), Factory.GetInstance(), Factory.GetInstance(), Mock.Of(), - new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() })); + new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), + nestedContentDataSerializer); } public class LocalServerMessenger : ServerMessengerBase diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentNestedData.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentNestedData.cs index ec5424ad9a..5f3edc4aa9 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentNestedData.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentNestedData.cs @@ -7,7 +7,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// /// The content item 1:M data that is serialized to JSON /// - internal class ContentNestedData + public class ContentNestedData { //dont serialize empty properties [JsonProperty("pd")] diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/CultureVariation.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/CultureVariation.cs index 57ffbba34e..b59e8c403c 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/CultureVariation.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/CultureVariation.cs @@ -6,7 +6,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// /// Represents the culture variation information on a content item /// - internal class CultureVariation + public class CultureVariation { [JsonProperty("nm")] public string Name { get; set; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index 694dac04df..80cfabd470 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -20,7 +20,15 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // provides efficient database access for NuCache internal class DatabaseDataSource : IDataSource { + + private const int PageSize = 500; + private readonly IContentNestedDataSerializer _contentNestedDataSerializer; + + internal DatabaseDataSource(IContentNestedDataSerializer contentNestedDataSerializer) + { + _contentNestedDataSerializer = contentNestedDataSerializer; + } // we want arrays, we want them all loaded, not an enumerable @@ -198,7 +206,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource yield return CreateMediaNodeKit(row); } - private static ContentNodeKit CreateContentNodeKit(ContentSourceDto dto) + private ContentNodeKit CreateContentNodeKit(ContentSourceDto dto) { ContentData d = null; ContentData p = null; @@ -213,7 +221,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } else { - var nested = DeserializeNestedData(dto.EditData); + var nested = _contentNestedDataSerializer.Deserialize(dto.EditData); d = new ContentData { @@ -240,7 +248,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } else { - var nested = DeserializeNestedData(dto.PubData); + var nested = _contentNestedDataSerializer.Deserialize(dto.PubData); p = new ContentData { @@ -271,12 +279,12 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return s; } - private static ContentNodeKit CreateMediaNodeKit(ContentSourceDto dto) + private ContentNodeKit CreateMediaNodeKit(ContentSourceDto dto) { if (dto.EditData == null) throw new Exception("No data for media " + dto.Id); - var nested = DeserializeNestedData(dto.EditData); + var nested = _contentNestedDataSerializer.Deserialize(dto.EditData); var p = new ContentData { @@ -303,17 +311,6 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return s; } - private static ContentNestedData DeserializeNestedData(string data) - { - // by default JsonConvert will deserialize our numeric values as Int64 - // which is bad, because they were Int32 in the database - take care - - var settings = new JsonSerializerSettings - { - Converters = new List { new ForceInt32Converter() } - }; - - return JsonConvert.DeserializeObject(data, settings); - } + } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs new file mode 100644 index 0000000000..d9e2702d08 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + public interface IContentNestedDataSerializer + { + ContentNestedData Deserialize(string data); + string Serialize(ContentNestedData nestedData); + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs new file mode 100644 index 0000000000..4ef63c09fb --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Serialization; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + internal class JsonContentNestedDataSerializer : IContentNestedDataSerializer + { + public ContentNestedData Deserialize(string data) + { + // by default JsonConvert will deserialize our numeric values as Int64 + // which is bad, because they were Int32 in the database - take care + + var settings = new JsonSerializerSettings + { + Converters = new List { new ForceInt32Converter() } + }; + + return JsonConvert.DeserializeObject(data, settings); + } + + public string Serialize(ContentNestedData nestedData) + { + return JsonConvert.SerializeObject(nestedData); + } + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MappedPropertiesJsonContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MappedPropertiesJsonContentNestedDataSerializer.cs new file mode 100644 index 0000000000..4919404e11 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MappedPropertiesJsonContentNestedDataSerializer.cs @@ -0,0 +1,157 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Serialization; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + public class MappedPropertiesJsonContentNestedDataSerializer : IContentNestedDataSerializer + { + private readonly IDictionary _serializeMap; + private readonly IDictionary _deserializeMap; + + + /// + /// Constructor + /// + /// Map for PropertData properties + /// + public MappedPropertiesJsonContentNestedDataSerializer(IDictionary serializeMap, IDictionary deserializeMap) + { + _serializeMap = serializeMap; + _deserializeMap = deserializeMap; + } + + public (string mappedName, bool isCompressed) ToSerializedProperty(string name) + { + if(_serializeMap.TryGetValue(name,out PropertyMap map)) + { + return (map.To,map.IsCompressed); + } + return (name,false); + } + public (string mappedName, bool isCompressed) ToDeserializedProperty(string name) + { + if (_deserializeMap.TryGetValue(name, out PropertyMap map)) + { + return (map.To, map.IsCompressed); + } + return (name, false); + } + + public ContentNestedData Deserialize(string data) + { + // by default JsonConvert will deserialize our numeric values as Int64 + // which is bad, because they were Int32 in the database - take care + + var settings = new JsonSerializerSettings + { + Converters = new List { new ForceInt32Converter() }, + + }; + + return JsonConvert.DeserializeObject(data, settings); + } + + public string Serialize(ContentNestedData nestedData) + { + return JsonConvert.SerializeObject(nestedData); + } + } + + public class MappedPropertyDataContractResolver : DefaultContractResolver + { + private readonly IDictionary _serializeMap; + private readonly IDictionary _deserializeMap; + + + /// + /// Constructor + /// + /// Map for PropertData properties + /// + public MappedPropertyDataContractResolver(IDictionary serializeMap, IDictionary deserializeMap) + { + _serializeMap = serializeMap; + _deserializeMap = deserializeMap; + } + + public (string mappedName, bool isCompressed) ToSerializedProperty(string name) + { + if (_serializeMap.TryGetValue(name, out PropertyMap map)) + { + return (map.To, map.IsCompressed); + } + return (name, false); + } + public (string mappedName, bool isCompressed) ToDeserializedProperty(string name) + { + if (_deserializeMap.TryGetValue(name, out PropertyMap map)) + { + return (map.To, map.IsCompressed); + } + return (name, false); + } + + private readonly Type _type = typeof(PropertyData); + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + var property = base.CreateProperty(member, memberSerialization); + if (property.DeclaringType == _type) + { + if (property.PropertyName.Equals("LongPropertyName", StringComparison.OrdinalIgnoreCase)) + { + property.PropertyName = "Short"; + } + } + return property; + } + protected override string ResolvePropertyName(string propertyName) + { + return base.ResolvePropertyName(propertyName); + } + protected override JsonDictionaryContract CreateDictionaryContract(Type objectType) + { + JsonDictionaryContract contract = base.CreateDictionaryContract(objectType); + + contract.DictionaryKeyResolver = propertyName => propertyName; + + return contract; + } + } + + public class MappedNamingStrategy : NamingStrategy + { + public MappedNamingStrategy() + { + ProcessDictionaryKeys = true; + } + public override string GetDictionaryKey(string key) + { + return key; + } + + protected override string ResolvePropertyName(string name) + { + return name; + } + } + + + public class PropertyMap + { + /// + /// PropertyName + /// + public string To { get; set; } + /// + /// Whether the property is compressed + /// + public bool IsCompressed { get; set; } + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/PropertyData.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/PropertyData.cs index 4abcbc7e6f..cf7ab95360 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/PropertyData.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/PropertyData.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { - internal class PropertyData + public class PropertyData { private string _culture; private string _segment; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs index f748fd555c..f67256bb6b 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs @@ -10,6 +10,9 @@ namespace Umbraco.Web.PublishedCache.NuCache { base.Compose(composition); + // register the NuCache NestedContentData serializer + composition.Register(); + // register the NuCache database data source composition.Register(); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index a39e26e2b1..b14deb7959 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -48,6 +48,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly IPublishedModelFactory _publishedModelFactory; private readonly IDefaultCultureAccessor _defaultCultureAccessor; private readonly UrlSegmentProviderCollection _urlSegmentProviders; + private readonly IContentNestedDataSerializer _contentNestedDataSerializer; // volatile because we read it with no lock private volatile bool _isReady; @@ -81,7 +82,7 @@ namespace Umbraco.Web.PublishedCache.NuCache IDataSource dataSource, IGlobalSettings globalSettings, IEntityXmlSerializer entitySerializer, IPublishedModelFactory publishedModelFactory, - UrlSegmentProviderCollection urlSegmentProviders) + UrlSegmentProviderCollection urlSegmentProviders, IContentNestedDataSerializer contentNestedDataSerializer) : base(publishedSnapshotAccessor, variationContextAccessor) { //if (Interlocked.Increment(ref _singletonCheck) > 1) @@ -98,6 +99,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _defaultCultureAccessor = defaultCultureAccessor; _globalSettings = globalSettings; _urlSegmentProviders = urlSegmentProviders; + _contentNestedDataSerializer = contentNestedDataSerializer; // we need an Xml serializer here so that the member cache can support XPath, // for members this is done by navigating the serialized-to-xml member @@ -1457,7 +1459,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // note that numeric values (which are Int32) are serialized without their // type (eg "value":1234) and JsonConvert by default deserializes them as Int64 - Data = JsonConvert.SerializeObject(nestedData) + Data = _contentNestedDataSerializer.Serialize(nestedData) }; //Core.Composing.Current.Logger.Debug(dto.Data); diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index b1cbff0fef..01f81cfc83 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -245,6 +245,8 @@ + + From 7ad019c18a38265187bd1bb0ac9f745ce3f8d440 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Wed, 1 Jul 2020 17:49:34 +1200 Subject: [PATCH 002/289] remove example nested content serializer as it is not production ready. --- ...opertiesJsonContentNestedDataSerializer.cs | 157 ------------------ 1 file changed, 157 deletions(-) delete mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/MappedPropertiesJsonContentNestedDataSerializer.cs diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MappedPropertiesJsonContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MappedPropertiesJsonContentNestedDataSerializer.cs deleted file mode 100644 index 4919404e11..0000000000 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MappedPropertiesJsonContentNestedDataSerializer.cs +++ /dev/null @@ -1,157 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using Umbraco.Core.Serialization; - -namespace Umbraco.Web.PublishedCache.NuCache.DataSource -{ - public class MappedPropertiesJsonContentNestedDataSerializer : IContentNestedDataSerializer - { - private readonly IDictionary _serializeMap; - private readonly IDictionary _deserializeMap; - - - /// - /// Constructor - /// - /// Map for PropertData properties - /// - public MappedPropertiesJsonContentNestedDataSerializer(IDictionary serializeMap, IDictionary deserializeMap) - { - _serializeMap = serializeMap; - _deserializeMap = deserializeMap; - } - - public (string mappedName, bool isCompressed) ToSerializedProperty(string name) - { - if(_serializeMap.TryGetValue(name,out PropertyMap map)) - { - return (map.To,map.IsCompressed); - } - return (name,false); - } - public (string mappedName, bool isCompressed) ToDeserializedProperty(string name) - { - if (_deserializeMap.TryGetValue(name, out PropertyMap map)) - { - return (map.To, map.IsCompressed); - } - return (name, false); - } - - public ContentNestedData Deserialize(string data) - { - // by default JsonConvert will deserialize our numeric values as Int64 - // which is bad, because they were Int32 in the database - take care - - var settings = new JsonSerializerSettings - { - Converters = new List { new ForceInt32Converter() }, - - }; - - return JsonConvert.DeserializeObject(data, settings); - } - - public string Serialize(ContentNestedData nestedData) - { - return JsonConvert.SerializeObject(nestedData); - } - } - - public class MappedPropertyDataContractResolver : DefaultContractResolver - { - private readonly IDictionary _serializeMap; - private readonly IDictionary _deserializeMap; - - - /// - /// Constructor - /// - /// Map for PropertData properties - /// - public MappedPropertyDataContractResolver(IDictionary serializeMap, IDictionary deserializeMap) - { - _serializeMap = serializeMap; - _deserializeMap = deserializeMap; - } - - public (string mappedName, bool isCompressed) ToSerializedProperty(string name) - { - if (_serializeMap.TryGetValue(name, out PropertyMap map)) - { - return (map.To, map.IsCompressed); - } - return (name, false); - } - public (string mappedName, bool isCompressed) ToDeserializedProperty(string name) - { - if (_deserializeMap.TryGetValue(name, out PropertyMap map)) - { - return (map.To, map.IsCompressed); - } - return (name, false); - } - - private readonly Type _type = typeof(PropertyData); - protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) - { - var property = base.CreateProperty(member, memberSerialization); - if (property.DeclaringType == _type) - { - if (property.PropertyName.Equals("LongPropertyName", StringComparison.OrdinalIgnoreCase)) - { - property.PropertyName = "Short"; - } - } - return property; - } - protected override string ResolvePropertyName(string propertyName) - { - return base.ResolvePropertyName(propertyName); - } - protected override JsonDictionaryContract CreateDictionaryContract(Type objectType) - { - JsonDictionaryContract contract = base.CreateDictionaryContract(objectType); - - contract.DictionaryKeyResolver = propertyName => propertyName; - - return contract; - } - } - - public class MappedNamingStrategy : NamingStrategy - { - public MappedNamingStrategy() - { - ProcessDictionaryKeys = true; - } - public override string GetDictionaryKey(string key) - { - return key; - } - - protected override string ResolvePropertyName(string name) - { - return name; - } - } - - - public class PropertyMap - { - /// - /// PropertyName - /// - public string To { get; set; } - /// - /// Whether the property is compressed - /// - public bool IsCompressed { get; set; } - } -} From 3735b6d3919e2d778c53a4b44aa6a4ac173f7555 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 2 Jul 2020 16:21:54 +1000 Subject: [PATCH 003/289] Fixes paging size for querying the nucache table so it doesn't timeout --- .../NuCache/PublishedSnapshotService.cs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index a39e26e2b1..fcea6e6227 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Configuration; using System.Diagnostics; using System.Globalization; using System.IO; @@ -1469,6 +1470,14 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Rebuild Database PreCache + private const int DefaultSqlPagingSize = 1000; + + private static int GetSqlPagingSize() + { + var appSetting = ConfigurationManager.AppSettings["Umbraco.Web.PublishedCache.NuCache.PublishedSnapshotService.SqlPageSize"]; + return appSetting != null && int.TryParse(appSetting, out var size) ? size : DefaultSqlPagingSize; + } + public override void Rebuild() { _logger.Debug("Rebuilding..."); @@ -1477,14 +1486,14 @@ namespace Umbraco.Web.PublishedCache.NuCache scope.ReadLock(Constants.Locks.ContentTree); scope.ReadLock(Constants.Locks.MediaTree); scope.ReadLock(Constants.Locks.MemberTree); - RebuildContentDbCacheLocked(scope, 5000, null); - RebuildMediaDbCacheLocked(scope, 5000, null); - RebuildMemberDbCacheLocked(scope, 5000, null); + RebuildContentDbCacheLocked(scope, GetSqlPagingSize(), null); + RebuildMediaDbCacheLocked(scope, GetSqlPagingSize(), null); + RebuildMemberDbCacheLocked(scope, GetSqlPagingSize(), null); scope.Complete(); } } - public void RebuildContentDbCache(int groupSize = 5000, IEnumerable contentTypeIds = null) + public void RebuildContentDbCache(int groupSize = DefaultSqlPagingSize, IEnumerable contentTypeIds = null) { using (var scope = _scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) { @@ -1556,7 +1565,7 @@ WHERE cmsContentNu.nodeId IN ( } while (processed < total); } - public void RebuildMediaDbCache(int groupSize = 5000, IEnumerable contentTypeIds = null) + public void RebuildMediaDbCache(int groupSize = DefaultSqlPagingSize, IEnumerable contentTypeIds = null) { using (var scope = _scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) { @@ -1615,7 +1624,7 @@ WHERE cmsContentNu.nodeId IN ( } while (processed < total); } - public void RebuildMemberDbCache(int groupSize = 5000, IEnumerable contentTypeIds = null) + public void RebuildMemberDbCache(int groupSize = DefaultSqlPagingSize, IEnumerable contentTypeIds = null) { using (var scope = _scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) { From bb2fe5d2d6f41e06872bf2e806ed131afe9c52e7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 2 Jul 2020 20:56:42 +1000 Subject: [PATCH 004/289] Adds notes about SQL call --- .../Repositories/Implement/ContentRepositoryBase.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 845006891d..d56724db9f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -630,6 +630,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } if (versions.Count == 0) return new Dictionary(); + // TODO: This is a bugger of a query and I believe is the main issue with regards to SQL performance drain when querying content + // which is done when rebuilding caches/indexes/etc... in bulk. We are using an "IN" query on umbracoPropertyData.VersionId + // which then performs a Clustered Index Scan on PK_umbracoPropertyData which means it iterates the entire table which can be enormous! + // especially if there are both a lot of content but worse if there is a lot of versions of that content. + // So is it possible to return this property data without doing an index scan on PK_umbracoPropertyData and without iterating every row + // in the table? + // get all PropertyDataDto for all definitions / versions var allPropertyDataDtos = Database.FetchByGroups(versions, 2000, batch => SqlContext.Sql() From d2042e28e13f427eb38d1b3f0b3eb9571db15252 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 2 Jul 2020 21:27:48 +1000 Subject: [PATCH 005/289] Adds string interning for reading of property alias, culture and segment when reading from the content cache --- .../DataSource/BTree.DictionaryOfPropertyDataSerializer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs index aa5dc9eb30..15c6a9f1bd 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs @@ -19,7 +19,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource for (var i = 0; i < pcount; i++) { // read property alias - var key = PrimitiveSerializer.String.ReadFrom(stream); + var key = string.Intern(PrimitiveSerializer.String.ReadFrom(stream)); // read values count var vcount = PrimitiveSerializer.Int32.ReadFrom(stream); @@ -38,8 +38,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // the 'current' value, and string.Empty should be used to represent the invariant or // neutral values - PropertyData throws when getting nulls, so falling back to // string.Empty here - what else? - pdata.Culture = ReadStringObject(stream) ?? string.Empty; - pdata.Segment = ReadStringObject(stream) ?? string.Empty; + pdata.Culture = string.Intern(ReadStringObject(stream)) ?? string.Empty; + pdata.Segment = string.Intern(ReadStringObject(stream)) ?? string.Empty; pdata.Value = ReadObject(stream); } From e75c9d2273232e7c4419172c0763cc18b483b494 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jul 2020 00:26:55 +1000 Subject: [PATCH 006/289] Interns strings for aliases, etc... for when content is deserialized from the contentNu table so we aren't duplicating strings when cold booting. --- .../AutoInterningStringConverter.cs | 38 +++++++++++ ...ngKeyCaseInsensitiveDictionaryConverter.cs | 54 +++++++++++++++ .../CaseInsensitiveDictionaryConverter.cs | 14 +++- src/Umbraco.Core/Umbraco.Core.csproj | 2 + .../AutoInterningStringConverterTests.cs | 67 +++++++++++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + ....DictionaryOfCultureVariationSerializer.cs | 9 ++- ...Tree.DictionaryOfPropertyDataSerializer.cs | 4 +- .../NuCache/DataSource/ContentNestedData.cs | 4 +- .../NuCache/DataSource/PropertyData.cs | 4 +- .../NuCache/DataSource/SerializerBase.cs | 6 +- 11 files changed, 193 insertions(+), 10 deletions(-) create mode 100644 src/Umbraco.Core/Serialization/AutoInterningStringConverter.cs create mode 100644 src/Umbraco.Core/Serialization/AutoInterningStringKeyCaseInsensitiveDictionaryConverter.cs create mode 100644 src/Umbraco.Tests/Serialization/AutoInterningStringConverterTests.cs diff --git a/src/Umbraco.Core/Serialization/AutoInterningStringConverter.cs b/src/Umbraco.Core/Serialization/AutoInterningStringConverter.cs new file mode 100644 index 0000000000..2d1f12f5da --- /dev/null +++ b/src/Umbraco.Core/Serialization/AutoInterningStringConverter.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Umbraco.Core.Serialization +{ + + /// + /// When applied to a string or string collection field will ensure the deserialized strings are interned + /// + /// + /// Borrowed from https://stackoverflow.com/a/34906004/694494 + /// On the same page an interesting approach of using a local intern pool https://stackoverflow.com/a/39605620/694494 which re-uses .NET System.Xml.NameTable + /// + internal class AutoInterningStringConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + // CanConvert is not called when a converter is applied directly to a property. + throw new NotImplementedException($"{nameof(AutoInterningStringConverter)} should not be used globally"); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + return null; + // Check is in case the value is a non-string literal such as an integer. + var s = reader.TokenType == JsonToken.String + ? string.Intern((string)reader.Value) + : string.Intern((string)JToken.Load(reader)); + return s; + } + + public override bool CanWrite => false; + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException(); + } +} diff --git a/src/Umbraco.Core/Serialization/AutoInterningStringKeyCaseInsensitiveDictionaryConverter.cs b/src/Umbraco.Core/Serialization/AutoInterningStringKeyCaseInsensitiveDictionaryConverter.cs new file mode 100644 index 0000000000..2076462f0c --- /dev/null +++ b/src/Umbraco.Core/Serialization/AutoInterningStringKeyCaseInsensitiveDictionaryConverter.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Umbraco.Core.Serialization +{ + /// + /// When applied to a dictionary with a string key, will ensure the deserialized string keys are interned + /// + /// + /// + /// borrowed from https://stackoverflow.com/a/36116462/694494 + /// + internal class AutoInterningStringKeyCaseInsensitiveDictionaryConverter : CaseInsensitiveDictionaryConverter + { + public AutoInterningStringKeyCaseInsensitiveDictionaryConverter() + { + } + public AutoInterningStringKeyCaseInsensitiveDictionaryConverter(StringComparer comparer) : base(comparer) + { + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.StartObject) + { + var dictionary = new Dictionary(); + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonToken.PropertyName: + var key = string.Intern(reader.Value.ToString()); + + if (!reader.Read()) + throw new Exception("Unexpected end when reading object."); + + var v = serializer.Deserialize(reader); + dictionary[key] = v; + break; + case JsonToken.Comment: + break; + case JsonToken.EndObject: + return dictionary; + } + } + } + return null; + } + + } +} diff --git a/src/Umbraco.Core/Serialization/CaseInsensitiveDictionaryConverter.cs b/src/Umbraco.Core/Serialization/CaseInsensitiveDictionaryConverter.cs index a92d562a52..d5cbc0da31 100644 --- a/src/Umbraco.Core/Serialization/CaseInsensitiveDictionaryConverter.cs +++ b/src/Umbraco.Core/Serialization/CaseInsensitiveDictionaryConverter.cs @@ -14,12 +14,24 @@ namespace Umbraco.Core.Serialization /// public class CaseInsensitiveDictionaryConverter : CustomCreationConverter { + private readonly StringComparer _comparer; + + public CaseInsensitiveDictionaryConverter() + : this(StringComparer.OrdinalIgnoreCase) + { + } + + public CaseInsensitiveDictionaryConverter(StringComparer comparer) + { + _comparer = comparer ?? throw new ArgumentNullException(nameof(comparer)); + } + public override bool CanWrite => false; public override bool CanRead => true; public override bool CanConvert(Type objectType) => typeof(IDictionary).IsAssignableFrom(objectType); - public override IDictionary Create(Type objectType) => new Dictionary(StringComparer.OrdinalIgnoreCase); + public override IDictionary Create(Type objectType) => new Dictionary(_comparer); } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index b2a8ea2a6d..c5a953631a 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -139,6 +139,8 @@ + + diff --git a/src/Umbraco.Tests/Serialization/AutoInterningStringConverterTests.cs b/src/Umbraco.Tests/Serialization/AutoInterningStringConverterTests.cs new file mode 100644 index 0000000000..bf99b68077 --- /dev/null +++ b/src/Umbraco.Tests/Serialization/AutoInterningStringConverterTests.cs @@ -0,0 +1,67 @@ +using Newtonsoft.Json; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Serialization; +using Umbraco.Core.Serialization; + +namespace Umbraco.Tests.Serialization +{ + [TestFixture] + public class AutoInterningStringConverterTests + { + [Test] + public void Intern_Property_String() + { + var str1 = "Hello"; + var obj = new Test + { + Name = str1 + " " + "there" + }; + + // ensure the raw value is not interned + Assert.IsNull(string.IsInterned(obj.Name)); + + var serialized = JsonConvert.SerializeObject(obj); + obj = JsonConvert.DeserializeObject(serialized); + + Assert.IsNotNull(string.IsInterned(obj.Name)); + } + + [Test] + public void Intern_Property_Dictionary() + { + var str1 = "key"; + var obj = new Test + { + Values = new Dictionary + { + [str1 + "1"] = 0, + [str1 + "2"] = 1 + } + }; + + // ensure the raw value is not interned + Assert.IsNull(string.IsInterned(obj.Values.Keys.First())); + Assert.IsNull(string.IsInterned(obj.Values.Keys.Last())); + + var serialized = JsonConvert.SerializeObject(obj); + obj = JsonConvert.DeserializeObject(serialized); + + Assert.IsNotNull(string.IsInterned(obj.Values.Keys.First())); + Assert.IsNotNull(string.IsInterned(obj.Values.Keys.Last())); + } + + public class Test + { + [JsonConverter(typeof(AutoInterningStringConverter))] + public string Name { get; set; } + + [JsonConverter(typeof(AutoInterningStringKeyCaseInsensitiveDictionaryConverter))] + public Dictionary Values = new Dictionary(); + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 731dc05363..d2ee8e83df 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -158,6 +158,7 @@ + diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs index 4521311302..1fcbdbdae7 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs @@ -18,8 +18,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource var dict = new Dictionary(StringComparer.InvariantCultureIgnoreCase); for (var i = 0; i < pcount; i++) { - var languageId = PrimitiveSerializer.String.ReadFrom(stream); - var cultureVariation = new CultureVariation { Name = ReadStringObject(stream), UrlSegment = ReadStringObject(stream), Date = ReadDateTime(stream) }; + var languageId = string.Intern(PrimitiveSerializer.String.ReadFrom(stream)); + var cultureVariation = new CultureVariation + { + Name = ReadStringObject(stream), + UrlSegment = ReadStringObject(stream), + Date = ReadDateTime(stream) + }; dict[languageId] = cultureVariation; } return dict; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs index 15c6a9f1bd..e3fa6597e4 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs @@ -38,8 +38,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // the 'current' value, and string.Empty should be used to represent the invariant or // neutral values - PropertyData throws when getting nulls, so falling back to // string.Empty here - what else? - pdata.Culture = string.Intern(ReadStringObject(stream)) ?? string.Empty; - pdata.Segment = string.Intern(ReadStringObject(stream)) ?? string.Empty; + pdata.Culture = ReadStringObject(stream, true) ?? string.Empty; + pdata.Segment = ReadStringObject(stream, true) ?? string.Empty; pdata.Value = ReadObject(stream); } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentNestedData.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentNestedData.cs index ec5424ad9a..751644c715 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentNestedData.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentNestedData.cs @@ -11,11 +11,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { //dont serialize empty properties [JsonProperty("pd")] - [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] + [JsonConverter(typeof(AutoInterningStringKeyCaseInsensitiveDictionaryConverter))] public Dictionary PropertyData { get; set; } [JsonProperty("cd")] - [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] + [JsonConverter(typeof(AutoInterningStringKeyCaseInsensitiveDictionaryConverter))] public Dictionary CultureData { get; set; } [JsonProperty("us")] diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/PropertyData.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/PropertyData.cs index 4abcbc7e6f..6ccb1dc210 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/PropertyData.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/PropertyData.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using Newtonsoft.Json; +using Umbraco.Core.Serialization; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { @@ -9,6 +10,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource private string _culture; private string _segment; + [JsonConverter(typeof(AutoInterningStringConverter))] [DefaultValue("")] [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, PropertyName = "c")] public string Culture @@ -17,6 +19,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource set => _culture = value ?? throw new ArgumentNullException(nameof(value)); // TODO: or fallback to string.Empty? CANNOT be null } + [JsonConverter(typeof(AutoInterningStringConverter))] [DefaultValue("")] [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, PropertyName = "s")] public string Segment @@ -28,7 +31,6 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource [JsonProperty("v")] public object Value { get; set; } - //Legacy properties used to deserialize existing nucache db entries [JsonProperty("culture")] private string LegacyCulture diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs index ed17420645..6196be9a3a 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs @@ -23,13 +23,15 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return read(stream); } - protected string ReadStringObject(Stream stream) // required 'cos string is not a struct + protected string ReadStringObject(Stream stream, bool intern = false) // required 'cos string is not a struct { var type = PrimitiveSerializer.Char.ReadFrom(stream); if (type == 'N') return null; if (type != 'S') throw new NotSupportedException($"Cannot deserialize type '{type}', expected 'S'."); - return PrimitiveSerializer.String.ReadFrom(stream); + return intern + ? string.Intern(PrimitiveSerializer.String.ReadFrom(stream)) + : PrimitiveSerializer.String.ReadFrom(stream); } protected int? ReadIntObject(Stream stream) => ReadObject(stream, 'I', ReadInt); From c63bfb866bdf84600a607549f1f6a4b351c56db4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jul 2020 12:11:05 +1000 Subject: [PATCH 007/289] Adds MessagePack serialization for nucache --- .../ContentSerializationTests.cs | 103 ++++++++++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 3 +- .../JsonContentNestedDataSerializer.cs | 14 ++- .../MsgPackContentNestedDataSerializer.cs | 42 +++++++ .../NuCache/DataSource/SerializerBase.cs | 71 ++++++++---- .../PublishedCache/NuCache/NuCacheComposer.cs | 3 +- .../NuCache/PublishedSnapshotService.cs | 4 - src/Umbraco.Web/Umbraco.Web.csproj | 6 +- 8 files changed, 215 insertions(+), 31 deletions(-) create mode 100644 src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs diff --git a/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs b/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs new file mode 100644 index 0000000000..debfddef98 --- /dev/null +++ b/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs @@ -0,0 +1,103 @@ +using NUnit.Framework; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Web.PublishedCache.NuCache.DataSource; + +namespace Umbraco.Tests.PublishedContent +{ + [TestFixture] + public class ContentSerializationTests + { + [Test] + public void Ensure_Same_Results() + { + var jsonSerializer = new JsonContentNestedDataSerializer(); + var msgPackSerializer = new MsgPackContentNestedDataSerializer(); + + var now = DateTime.Now; + var content = new ContentNestedData + { + PropertyData = new Dictionary + { + ["propertyOne"] = new[] + { + new PropertyData + { + Culture = "en-US", + Segment = "test", + Value = "hello world" + } + }, + ["propertyTwo"] = new[] + { + new PropertyData + { + Culture = "en-US", + Segment = "test", + Value = "Lorem ipsum" + } + } + }, + CultureData = new Dictionary + { + ["en-US"] = new CultureVariation + { + Date = now, + IsDraft = false, + Name = "Home", + UrlSegment = "home" + } + }, + UrlSegment = "home" + }; + + var json = jsonSerializer.Serialize(content); + var msgPack = msgPackSerializer.Serialize(content); + + Console.WriteLine(json); + Console.WriteLine(msgPackSerializer.ToJson(msgPack)); + + var jsonContent = jsonSerializer.Deserialize(json); + var msgPackContent = msgPackSerializer.Deserialize(msgPack); + + + CollectionAssert.AreEqual(jsonContent.CultureData.Keys, msgPackContent.CultureData.Keys); + CollectionAssert.AreEqual(jsonContent.PropertyData.Keys, msgPackContent.PropertyData.Keys); + CollectionAssert.AreEqual(jsonContent.CultureData.Values, msgPackContent.CultureData.Values, new CultureVariationComparer()); + CollectionAssert.AreEqual(jsonContent.PropertyData.Values, msgPackContent.PropertyData.Values, new PropertyDataComparer()); + Assert.AreEqual(jsonContent.UrlSegment, msgPackContent.UrlSegment); + } + + public class CultureVariationComparer : Comparer + { + public override int Compare(CultureVariation x, CultureVariation y) + { + if (x == null && y == null) return 0; + if (x == null && y != null) return -1; + if (x != null && y == null) return 1; + + return x.Date.CompareTo(y.Date) | x.IsDraft.CompareTo(y.IsDraft) | x.Name.CompareTo(y.Name) | x.UrlSegment.CompareTo(y.UrlSegment); + } + } + + public class PropertyDataComparer : Comparer + { + public override int Compare(PropertyData x, PropertyData y) + { + if (x == null && y == null) return 0; + if (x == null && y != null) return -1; + if (x != null && y == null) return 1; + + var xVal = x.Value?.ToString() ?? string.Empty; + var yVal = y.Value?.ToString() ?? string.Empty; + + return x.Culture.CompareTo(y.Culture) | x.Segment.CompareTo(y.Segment) | xVal.CompareTo(yVal); + } + } + + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index d2ee8e83df..c65110ab6b 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -109,7 +109,7 @@ - + @@ -149,6 +149,7 @@ + diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs index 4ef63c09fb..6635fa7090 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs @@ -1,13 +1,14 @@ using Newtonsoft.Json; -using System; using System.Collections.Generic; using System.Linq; +using System.Reflection.Emit; using System.Text; using System.Threading.Tasks; using Umbraco.Core.Serialization; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { + internal class JsonContentNestedDataSerializer : IContentNestedDataSerializer { public ContentNestedData Deserialize(string data) @@ -17,7 +18,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource var settings = new JsonSerializerSettings { - Converters = new List { new ForceInt32Converter() } + Converters = new List { new ForceInt32Converter() }, + + // Explicitly specify date handling so that it's consistent and follows the same date handling as MessagePack + DateParseHandling = DateParseHandling.DateTime, + DateFormatHandling = DateFormatHandling.IsoDateFormat, + DateTimeZoneHandling = DateTimeZoneHandling.Utc, + DateFormatString = "o" }; return JsonConvert.DeserializeObject(data, settings); @@ -25,6 +32,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public string Serialize(ContentNestedData nestedData) { + // note that numeric values (which are Int32) are serialized without their + // type (eg "value":1234) and JsonConvert by default deserializes them as Int64 + return JsonConvert.SerializeObject(nestedData); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs new file mode 100644 index 0000000000..8ff49a9544 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -0,0 +1,42 @@ +using MessagePack; +using System; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + internal class MsgPackContentNestedDataSerializer : IContentNestedDataSerializer + { + private MessagePackSerializerOptions _options; + + public MsgPackContentNestedDataSerializer() + { + _options = MessagePack.Resolvers.ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.Lz4BlockArray); + } + + public string ToJson(string serialized) + { + var bin = Convert.FromBase64String(serialized); + var json = MessagePackSerializer.ConvertToJson(bin, _options); + return json; + } + + // TODO: Instead of returning base64 it would be more ideal to avoid that translation entirely and just store/retrieve raw bytes + + // TODO: We need to write tests to serialize/deserialize between either of these serializers to ensure we end up with the same object + // i think this one is a bit quirky so far :) + + public ContentNestedData Deserialize(string data) + { + var bin = Convert.FromBase64String(data); + var obj = MessagePackSerializer.Deserialize(bin, _options); + return obj; + } + + public string Serialize(ContentNestedData nestedData) + { + var bin = MessagePackSerializer.Serialize( + nestedData, + _options); + return Convert.ToBase64String(bin); + } + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs index 6196be9a3a..0c89419b51 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs @@ -6,6 +6,16 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { internal abstract class SerializerBase { + private const char PrefixNull = 'N'; + private const char PrefixString = 'S'; + private const char PrefixInt32 = 'I'; + private const char PrefixUInt16 = 'H'; + private const char PrefixLong = 'L'; + private const char PrefixFloat = 'F'; + private const char PrefixDouble = 'B'; + private const char PrefixDateTime = 'D'; + private const char PrefixByte = 'O'; + protected string ReadString(Stream stream) => PrimitiveSerializer.String.ReadFrom(stream); protected int ReadInt(Stream stream) => PrimitiveSerializer.Int32.ReadFrom(stream); protected long ReadLong(Stream stream) => PrimitiveSerializer.Int64.ReadFrom(stream); @@ -17,7 +27,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource where T : struct { var type = PrimitiveSerializer.Char.ReadFrom(stream); - if (type == 'N') return null; + if (type == PrefixNull) return null; if (type != t) throw new NotSupportedException($"Cannot deserialize type '{type}', expected '{t}'."); return read(stream); @@ -26,40 +36,47 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource protected string ReadStringObject(Stream stream, bool intern = false) // required 'cos string is not a struct { var type = PrimitiveSerializer.Char.ReadFrom(stream); - if (type == 'N') return null; - if (type != 'S') + if (type == PrefixNull) return null; + if (type != PrefixString) throw new NotSupportedException($"Cannot deserialize type '{type}', expected 'S'."); return intern ? string.Intern(PrimitiveSerializer.String.ReadFrom(stream)) : PrimitiveSerializer.String.ReadFrom(stream); } - protected int? ReadIntObject(Stream stream) => ReadObject(stream, 'I', ReadInt); - protected long? ReadLongObject(Stream stream) => ReadObject(stream, 'L', ReadLong); - protected float? ReadFloatObject(Stream stream) => ReadObject(stream, 'F', ReadFloat); - protected double? ReadDoubleObject(Stream stream) => ReadObject(stream, 'B', ReadDouble); - protected DateTime? ReadDateTimeObject(Stream stream) => ReadObject(stream, 'D', ReadDateTime); + protected int? ReadIntObject(Stream stream) => ReadObject(stream, PrefixInt32, ReadInt); + protected long? ReadLongObject(Stream stream) => ReadObject(stream, PrefixLong, ReadLong); + protected float? ReadFloatObject(Stream stream) => ReadObject(stream, PrefixFloat, ReadFloat); + protected double? ReadDoubleObject(Stream stream) => ReadObject(stream, PrefixDouble, ReadDouble); + protected DateTime? ReadDateTimeObject(Stream stream) => ReadObject(stream, PrefixDateTime, ReadDateTime); protected object ReadObject(Stream stream) => ReadObject(PrimitiveSerializer.Char.ReadFrom(stream), stream); protected object ReadObject(char type, Stream stream) { + // NOTE: There is going to be a ton of boxing going on here, but i'm not sure we can avoid that because innevitably with our + // current model structure the value will need to end up being 'object' at some point anyways. + switch (type) { - case 'N': + case PrefixNull: return null; - case 'S': + case PrefixString: return PrimitiveSerializer.String.ReadFrom(stream); - case 'I': + case PrefixInt32: return PrimitiveSerializer.Int32.ReadFrom(stream); - case 'L': + case PrefixUInt16: + return PrimitiveSerializer.UInt16.ReadFrom(stream); + case PrefixByte: + return PrimitiveSerializer.Byte.ReadFrom(stream); + case PrefixLong: return PrimitiveSerializer.Int64.ReadFrom(stream); - case 'F': + case PrefixFloat: return PrimitiveSerializer.Float.ReadFrom(stream); - case 'B': + case PrefixDouble: return PrimitiveSerializer.Double.ReadFrom(stream); - case 'D': + case PrefixDateTime: return PrimitiveSerializer.DateTime.ReadFrom(stream); default: throw new NotSupportedException($"Cannot deserialize unknown type '{type}'."); @@ -70,36 +87,46 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { if (value == null) { - PrimitiveSerializer.Char.WriteTo('N', stream); + PrimitiveSerializer.Char.WriteTo(PrefixNull, stream); } else if (value is string stringValue) { - PrimitiveSerializer.Char.WriteTo('S', stream); + PrimitiveSerializer.Char.WriteTo(PrefixString, stream); PrimitiveSerializer.String.WriteTo(stringValue, stream); } else if (value is int intValue) { - PrimitiveSerializer.Char.WriteTo('I', stream); + PrimitiveSerializer.Char.WriteTo(PrefixInt32, stream); PrimitiveSerializer.Int32.WriteTo(intValue, stream); } + else if (value is byte byteValue) + { + PrimitiveSerializer.Char.WriteTo(PrefixByte, stream); + PrimitiveSerializer.Byte.WriteTo(byteValue, stream); + } + else if (value is ushort ushortValue) + { + PrimitiveSerializer.Char.WriteTo(PrefixUInt16, stream); + PrimitiveSerializer.UInt16.WriteTo(ushortValue, stream); + } else if (value is long longValue) { - PrimitiveSerializer.Char.WriteTo('L', stream); + PrimitiveSerializer.Char.WriteTo(PrefixLong, stream); PrimitiveSerializer.Int64.WriteTo(longValue, stream); } else if (value is float floatValue) { - PrimitiveSerializer.Char.WriteTo('F', stream); + PrimitiveSerializer.Char.WriteTo(PrefixFloat, stream); PrimitiveSerializer.Float.WriteTo(floatValue, stream); } else if (value is double doubleValue) { - PrimitiveSerializer.Char.WriteTo('B', stream); + PrimitiveSerializer.Char.WriteTo(PrefixDouble, stream); PrimitiveSerializer.Double.WriteTo(doubleValue, stream); } else if (value is DateTime dateValue) { - PrimitiveSerializer.Char.WriteTo('D', stream); + PrimitiveSerializer.Char.WriteTo(PrefixDateTime, stream); PrimitiveSerializer.DateTime.WriteTo(dateValue, stream); } else diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs index f67256bb6b..6d01b34a76 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs @@ -11,7 +11,8 @@ namespace Umbraco.Web.PublishedCache.NuCache base.Compose(composition); // register the NuCache NestedContentData serializer - composition.Register(); + //composition.Register(); + composition.Register(); // register the NuCache database data source composition.Register(); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 5ce62271a7..6289667d3b 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1456,10 +1456,6 @@ namespace Umbraco.Web.PublishedCache.NuCache { NodeId = content.Id, Published = published, - - // note that numeric values (which are Int32) are serialized without their - // type (eg "value":1234) and JsonConvert by default deserializes them as Int64 - Data = _contentNestedDataSerializer.Serialize(nestedData) }; diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 01f81cfc83..088fb0eeb3 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -73,6 +73,9 @@ + + 2.1.152 + @@ -247,6 +250,7 @@ + @@ -1266,7 +1270,7 @@ - + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - + + + - - + + @@ -94,7 +94,11 @@ - - + + + + + + diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 5518fb5678..cd0868532c 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -78,7 +78,7 @@ - + 1.8.14 @@ -100,7 +100,9 @@ - + + 4.14.5 + @@ -109,7 +111,6 @@ - diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentNestedData.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentNestedData.cs index 0ff24980fe..1a49aaaf62 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentNestedData.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentNestedData.cs @@ -29,17 +29,17 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public string UrlSegment { get; set; } //Legacy properties used to deserialize existing nucache db entries - [DataMember(Order = 3)] + [IgnoreDataMember] [JsonProperty("properties")] [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] private Dictionary LegacyPropertyData { set { PropertyData = value; } } - [DataMember(Order = 4)] + [IgnoreDataMember] [JsonProperty("cultureData")] [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] private Dictionary LegacyCultureData { set { CultureData = value; } } - [DataMember(Order = 5)] + [IgnoreDataMember] [JsonProperty("urlSegment")] private string LegacyUrlSegment { set { UrlSegment = value; } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/CultureVariation.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/CultureVariation.cs index dd3323fa0c..daf60b66ec 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/CultureVariation.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/CultureVariation.cs @@ -27,19 +27,19 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public bool IsDraft { get; set; } //Legacy properties used to deserialize existing nucache db entries - [DataMember(Order = 4)] + [IgnoreDataMember] [JsonProperty("name")] private string LegacyName { set { Name = value; } } - [DataMember(Order = 5)] + [IgnoreDataMember] [JsonProperty("urlSegment")] private string LegacyUrlSegment { set { UrlSegment = value; } } - [DataMember(Order = 6)] + [IgnoreDataMember] [JsonProperty("date")] private DateTime LegacyDate { set { Date = value; } } - [DataMember(Order = 7)] + [IgnoreDataMember] [JsonProperty("isDraft")] private bool LegacyIsDraft { set { IsDraft = value; } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/PropertyData.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/PropertyData.cs index b49a781e0c..7a3cb67d50 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/PropertyData.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/PropertyData.cs @@ -38,21 +38,21 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public object Value { get; set; } //Legacy properties used to deserialize existing nucache db entries - [DataMember(Order = 3)] + [IgnoreDataMember] [JsonProperty("culture")] private string LegacyCulture { set => Culture = value; } - [DataMember(Order = 4)] + [IgnoreDataMember] [JsonProperty("seg")] private string LegacySegment { set => Segment = value; } - [DataMember(Order = 5)] + [IgnoreDataMember] [JsonProperty("val")] private object LegacyValue { From 40184c0c3c6723c3083d835c9e414aa6eec1ce64 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Fri, 3 Jul 2020 16:56:08 +1200 Subject: [PATCH 010/289] support uInt32 --- .../PublishedCache/NuCache/DataSource/SerializerBase.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs index 0c89419b51..afc0827ed3 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs @@ -10,6 +10,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource private const char PrefixString = 'S'; private const char PrefixInt32 = 'I'; private const char PrefixUInt16 = 'H'; + private const char PrefixUInt32 = 'J'; private const char PrefixLong = 'L'; private const char PrefixFloat = 'F'; private const char PrefixDouble = 'B'; @@ -68,6 +69,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return PrimitiveSerializer.Int32.ReadFrom(stream); case PrefixUInt16: return PrimitiveSerializer.UInt16.ReadFrom(stream); + case PrefixUInt32: + return PrimitiveSerializer.UInt32.ReadFrom(stream); case PrefixByte: return PrimitiveSerializer.Byte.ReadFrom(stream); case PrefixLong: @@ -129,6 +132,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource PrimitiveSerializer.Char.WriteTo(PrefixDateTime, stream); PrimitiveSerializer.DateTime.WriteTo(dateValue, stream); } + else if (value is uint uInt32Value) + { + PrimitiveSerializer.Char.WriteTo(PrefixUInt32, stream); + PrimitiveSerializer.UInt32.WriteTo(uInt32Value, stream); + } else throw new NotSupportedException("Value type " + value.GetType().FullName + " cannot be serialized."); } From 39625d94ddeb6fab6e5a5ce64aa369ea17e7352d Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jul 2020 15:41:25 +1000 Subject: [PATCH 011/289] Adds migration for binary column on cmsContentNu --- .../Migrations/Upgrade/UmbracoPlan.cs | 3 ++- .../V_8_7_0/AddCmsContentNuByteColumn.cs | 21 ++++++++++++++++++ .../Persistence/Dtos/ContentNuDto.cs | 5 +++++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../NuCache/DataSource/ContentSourceDto.cs | 2 ++ .../NuCache/DataSource/DatabaseDataSource.cs | 22 ++++++++++++++----- .../IContentNestedDataSerializer.cs | 12 ++++++++-- .../MsgPackContentNestedDataSerializer.cs | 8 ++++--- .../NuCache/PublishedSnapshotService.cs | 6 +++-- 9 files changed, 66 insertions(+), 14 deletions(-) create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/AddCmsContentNuByteColumn.cs diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index f0cfc08281..4a4260b683 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -191,8 +191,9 @@ namespace Umbraco.Core.Migrations.Upgrade To("{EE288A91-531B-4995-8179-1D62D9AA3E2E}"); To("{2AB29964-02A1-474D-BD6B-72148D2A53A2}"); - + // to 8.7.0... To("{a78e3369-8ea3-40ec-ad3f-5f76929d2b20}"); + To("{8DDDCD0B-D7D5-4C97-BD6A-6B38CA65752F}"); //FINAL } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/AddCmsContentNuByteColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/AddCmsContentNuByteColumn.cs new file mode 100644 index 0000000000..3cb51afb99 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/AddCmsContentNuByteColumn.cs @@ -0,0 +1,21 @@ +using System.Linq; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0 +{ + public class AddCmsContentNuByteColumn : MigrationBase + { + public AddCmsContentNuByteColumn(IMigrationContext context) + : base(context) + { + + } + + public override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + + AddColumnIfNotExists(columns, "dataRaw"); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentNuDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentNuDto.cs index c6269d5317..664d188a10 100644 --- a/src/Umbraco.Core/Persistence/Dtos/ContentNuDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/ContentNuDto.cs @@ -25,8 +25,13 @@ namespace Umbraco.Core.Persistence.Dtos /// [Column("data")] [SpecialDbType(SpecialDbTypes.NTEXT)] + [NullSetting(NullSetting = NullSettings.Null)] public string Data { get; set; } + [Column("dataRaw")] + [NullSetting(NullSetting = NullSettings.Null)] + public byte[] RawData { get; set; } + [Column("rv")] public long Rv { get; set; } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index c5a953631a..706c2a3650 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -132,6 +132,7 @@ + diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentSourceDto.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentSourceDto.cs index 37fb9df8bb..be2f9921d1 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentSourceDto.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentSourceDto.cs @@ -27,6 +27,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public int EditWriterId { get; set; } public int EditTemplateId { get; set; } public string EditData { get; set; } + public byte[] EditDataRaw { get; set; } // published data public int PublishedVersionId { get; set; } @@ -35,5 +36,6 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public int PubWriterId { get; set; } public int PubTemplateId { get; set; } public string PubData { get; set; } + public byte[] PubDataRaw { get; set; } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index e39f649eaf..48ff3e558c 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -49,6 +49,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .AndSelect("nuEdit", x => Alias(x.Data, "EditData")) .AndSelect("nuPub", x => Alias(x.Data, "PubData")) + .AndSelect("nuEdit", x => Alias(x.RawData, "EditDataRaw")) + .AndSelect("nuPub", x => Alias(x.RawData, "PubDataRaw")) + .From(); if (joins != null) @@ -136,6 +139,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) .AndSelect(x => Alias(x.Id, "VersionId"), x => Alias(x.Text, "EditName"), x => Alias(x.VersionDate, "EditVersionDate"), x => Alias(x.UserId, "EditWriterId")) .AndSelect("nuEdit", x => Alias(x.Data, "EditData")) + .AndSelect("nuEdit", x => Alias(x.RawData, "EditDataRaw")) .From(); if (joins != null) @@ -211,7 +215,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource if (dto.Edited) { - if (dto.EditData == null) + if (dto.EditData == null && dto.EditDataRaw == null) { if (Debugger.IsAttached) throw new Exception("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding."); @@ -219,7 +223,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } else { - var nested = _contentNestedDataSerializer.Deserialize(dto.EditData); + var nested = _contentNestedDataSerializer is IContentNestedDataByteSerializer byteSerializer + ? byteSerializer.DeserializeBytes(dto.EditDataRaw) + : _contentNestedDataSerializer.Deserialize(dto.EditData); d = new ContentData { @@ -238,7 +244,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource if (dto.Published) { - if (dto.PubData == null) + if (dto.PubData == null && dto.PubDataRaw == null) { if (Debugger.IsAttached) throw new Exception("Missing cmsContentNu published content for node " + dto.Id + ", consider rebuilding."); @@ -246,7 +252,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } else { - var nested = _contentNestedDataSerializer.Deserialize(dto.PubData); + var nested = _contentNestedDataSerializer is IContentNestedDataByteSerializer byteSerializer + ? byteSerializer.DeserializeBytes(dto.PubDataRaw) + : _contentNestedDataSerializer.Deserialize(dto.PubData); p = new ContentData { @@ -279,10 +287,12 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource private ContentNodeKit CreateMediaNodeKit(ContentSourceDto dto) { - if (dto.EditData == null) + if (dto.EditData == null && dto.EditDataRaw == null) throw new Exception("No data for media " + dto.Id); - var nested = _contentNestedDataSerializer.Deserialize(dto.EditData); + var nested = _contentNestedDataSerializer is IContentNestedDataByteSerializer byteSerializer + ? byteSerializer.DeserializeBytes(dto.EditDataRaw) + : _contentNestedDataSerializer.Deserialize(dto.EditData); var p = new ContentData { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs index d9e2702d08..edd061fbe4 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs @@ -6,9 +6,17 @@ using System.Threading.Tasks; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { + // TODO: We need a better name, not sure why the class is called ContentNested in the first place + public interface IContentNestedDataByteSerializer : IContentNestedDataSerializer + { + ContentNestedData DeserializeBytes(byte[] data); + byte[] SerializeBytes(ContentNestedData nestedData); + } + + // TODO: We need a better name, not sure why the class is called ContentNested in the first place public interface IContentNestedDataSerializer { - ContentNestedData Deserialize(string data); - string Serialize(ContentNestedData nestedData); + ContentNestedData Deserialize(string data); + string Serialize(ContentNestedData nestedData); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index 99a306fecb..1a093292b5 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { - internal class MsgPackContentNestedDataSerializer : IContentNestedDataSerializer + internal class MsgPackContentNestedDataSerializer : IContentNestedDataByteSerializer { private MessagePackSerializerOptions _options; @@ -39,8 +39,6 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return json; } - // TODO: Instead of returning base64 it would be more ideal to avoid that translation entirely and just store/retrieve raw bytes - public ContentNestedData Deserialize(string data) { var bin = Convert.FromBase64String(data); @@ -54,6 +52,10 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return Convert.ToBase64String(bin); } + public ContentNestedData DeserializeBytes(byte[] data) => MessagePackSerializer.Deserialize(data, _options); + + public byte[] SerializeBytes(ContentNestedData nestedData) => MessagePackSerializer.Serialize(nestedData, _options); + //private class ContentNestedDataResolver : IFormatterResolver //{ // // GetFormatter's get cost should be minimized so use type cache. diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 6289667d3b..99396b45c9 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1322,9 +1322,10 @@ namespace Umbraco.Web.PublishedCache.NuCache var dto = GetDto(content, published); db.InsertOrUpdate(dto, - "SET data=@data, rv=rv+1 WHERE nodeId=@id AND published=@published", + "SET data=@data, dataRaw=@dataRaw, rv=rv+1 WHERE nodeId=@id AND published=@published", new { + dataRaw = dto.RawData, data = dto.Data, id = dto.NodeId, published = dto.Published @@ -1456,7 +1457,8 @@ namespace Umbraco.Web.PublishedCache.NuCache { NodeId = content.Id, Published = published, - Data = _contentNestedDataSerializer.Serialize(nestedData) + Data = !(_contentNestedDataSerializer is IContentNestedDataByteSerializer) ? _contentNestedDataSerializer.Serialize(nestedData) : null, + RawData = (_contentNestedDataSerializer is IContentNestedDataByteSerializer byteSerializer) ? byteSerializer.SerializeBytes(nestedData) : null }; //Core.Composing.Current.Logger.Debug(dto.Data); From c79b17cc48aa7f84c8c66ad06a36c7bd90a4c29a Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jul 2020 15:46:36 +1000 Subject: [PATCH 012/289] fixing tests --- .../Serialization/AutoInterningStringConverterTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Tests/Serialization/AutoInterningStringConverterTests.cs b/src/Umbraco.Tests/Serialization/AutoInterningStringConverterTests.cs index bf99b68077..f83ea940c9 100644 --- a/src/Umbraco.Tests/Serialization/AutoInterningStringConverterTests.cs +++ b/src/Umbraco.Tests/Serialization/AutoInterningStringConverterTests.cs @@ -19,7 +19,7 @@ namespace Umbraco.Tests.Serialization var str1 = "Hello"; var obj = new Test { - Name = str1 + " " + "there" + Name = str1 + Guid.NewGuid() }; // ensure the raw value is not interned @@ -39,8 +39,8 @@ namespace Umbraco.Tests.Serialization { Values = new Dictionary { - [str1 + "1"] = 0, - [str1 + "2"] = 1 + [str1 + Guid.NewGuid()] = 0, + [str1 + Guid.NewGuid()] = 1 } }; From e8717e6bf8b86230a4ba6d09f37444d9fcddce1c Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jul 2020 16:30:20 +1000 Subject: [PATCH 013/289] fixing tests and sqlce blob column --- .../Persistence/SqlSyntax/SqlCeSyntaxProvider.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs index 2ed0fb878c..8a42caaa03 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs @@ -14,6 +14,14 @@ namespace Umbraco.Core.Persistence.SqlSyntax /// public class SqlCeSyntaxProvider : MicrosoftSqlSyntaxProviderBase { + public SqlCeSyntaxProvider() + { + BlobColumnDefinition = "IMAGE"; + // This is silly to have to do this but the way these inherited classes are structured it's the easiest + // way without an overhaul in type map initialization + DbTypeMap.Set(DbType.Binary, BlobColumnDefinition); + } + public override Sql SelectTop(Sql sql, int top) { return new Sql(sql.SqlContext, sql.SQL.Insert(sql.SQL.IndexOf(' '), " TOP " + top), sql.Arguments); @@ -227,8 +235,6 @@ where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault() } } - - public override string DropIndex { get { return "DROP INDEX {1}.{0}"; } } } From 1280235dcfe98651123db9ed14c9ca613cae6f03 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 6 Jul 2020 16:25:15 +1000 Subject: [PATCH 014/289] Adds custom mapper for SqlCe for Image column, hopefully tests pass now. --- .../Persistence/SqlCeImageMapper.cs | 59 +++++++++++++++++++ .../Persistence/UmbracoDatabase.cs | 17 ++++-- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../NuCache/PublishedSnapshotService.cs | 2 +- 4 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 src/Umbraco.Core/Persistence/SqlCeImageMapper.cs diff --git a/src/Umbraco.Core/Persistence/SqlCeImageMapper.cs b/src/Umbraco.Core/Persistence/SqlCeImageMapper.cs new file mode 100644 index 0000000000..33303c68f0 --- /dev/null +++ b/src/Umbraco.Core/Persistence/SqlCeImageMapper.cs @@ -0,0 +1,59 @@ +using System; +using System.Data; +using System.Data.Common; +using System.Data.SqlServerCe; +using System.Linq; +using System.Reflection; +using NPoco; +using Umbraco.Core.Composing; + +namespace Umbraco.Core.Persistence +{ + /// + /// Custom NPoco mapper for SqlCe + /// + /// + /// Work arounds to handle special columns + /// + internal class SqlCeImageMapper : DefaultMapper + { + public override Func GetToDbConverter(Type destType, MemberInfo sourceMemberInfo) + { + if (sourceMemberInfo.GetMemberInfoType() == typeof(byte[])) + { + return x => + { + var pd = Current.SqlContext.PocoDataFactory.ForType(sourceMemberInfo.DeclaringType); + if (pd == null) return null; + var col = pd.AllColumns.FirstOrDefault(x => x.MemberInfoData.MemberInfo == sourceMemberInfo); + if (col == null) return null; + + return new SqlCeParameter + { + SqlDbType = SqlDbType.Image, + Value = x ?? Array.Empty() + }; + }; + } + return base.GetToDbConverter(destType, sourceMemberInfo); + } + + public override Func GetParameterConverter(DbCommand dbCommand, Type sourceType) + { + if (sourceType == typeof(byte[])) + { + return x => + { + var param = new SqlCeParameter + { + SqlDbType = SqlDbType.Image, + Value = x + }; + return param; + }; + + } + return base.GetParameterConverter(dbCommand, sourceType); + } + } +} diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs index a95d95ea08..ec09db690f 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs @@ -10,6 +10,7 @@ using Umbraco.Core.Persistence.FaultHandling; namespace Umbraco.Core.Persistence { + /// /// Extends NPoco Database for Umbraco. /// @@ -38,14 +39,10 @@ namespace Umbraco.Core.Persistence : base(connectionString, sqlContext.DatabaseType, provider, sqlContext.SqlSyntax.DefaultIsolationLevel) { SqlContext = sqlContext; - _logger = logger; _connectionRetryPolicy = connectionRetryPolicy; _commandRetryPolicy = commandRetryPolicy; - - EnableSqlTrace = EnableSqlTraceDefault; - - NPocoDatabaseExtensions.ConfigureNPocoBulkExtensions(); + Init(); } /// @@ -57,10 +54,17 @@ namespace Umbraco.Core.Persistence { SqlContext = sqlContext; _logger = logger; + Init(); + } + private void Init() + { EnableSqlTrace = EnableSqlTraceDefault; - NPocoDatabaseExtensions.ConfigureNPocoBulkExtensions(); + if (SqlContext.DatabaseType == DatabaseType.SQLCe) + { + Mappers.Add(new SqlCeImageMapper()); + } } #endregion @@ -257,5 +261,6 @@ namespace Umbraco.Core.Persistence } #endregion + } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 706c2a3650..5e2411db09 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -140,6 +140,7 @@ + diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 99396b45c9..c87f035589 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1325,7 +1325,7 @@ namespace Umbraco.Web.PublishedCache.NuCache "SET data=@data, dataRaw=@dataRaw, rv=rv+1 WHERE nodeId=@id AND published=@published", new { - dataRaw = dto.RawData, + dataRaw = dto.RawData ?? Array.Empty(), data = dto.Data, id = dto.NodeId, published = dto.Published From c1c189d47f73af319e473c08b38bb79b7ec0951b Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Thu, 9 Jul 2020 00:17:31 +1200 Subject: [PATCH 015/289] Wip support for compressing/decompressing nucache documents on a per property basis. Option for compressing the properties in sql/nucache.db. Option for immediate/lazy decompression of properties. Mapping support for shorter property alias. TODO: config file for property map TODO: HasValue and IsValue on propertyvalueconverterbase --- .../AppSettingsNucachePropertyMapFactory.cs | 47 +++++ .../DataSource/BTree.ContentDataSerializer.cs | 13 +- .../BTree.ContentNodeKitSerializer.cs | 20 +- ...Tree.DictionaryOfPropertyDataSerializer.cs | 2 +- .../NuCache/DataSource/BTree.cs | 4 +- .../IDictionaryOfPropertyDataSerializer.cs | 11 ++ .../INucachePropertyOptionsFactory.cs | 13 ++ .../DataSource/LazyCompressedString.cs | 31 +++ .../Lz4DictionaryOfPropertyDataSerializer.cs | 178 ++++++++++++++++++ .../MsgPackContentNestedDataSerializer.cs | 41 +++- .../DataSource/NucachePropertyOptions.cs | 20 ++ .../NuCache/DataSource/SerializerBase.cs | 21 ++- .../PublishedCache/NuCache/NuCacheComposer.cs | 22 ++- .../NuCache/PublishedSnapshotService.cs | 8 +- src/Umbraco.Web/Umbraco.Web.csproj | 9 + 15 files changed, 422 insertions(+), 18 deletions(-) create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/IDictionaryOfPropertyDataSerializer.cs create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/INucachePropertyOptionsFactory.cs create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyOptions.cs diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs new file mode 100644 index 0000000000..ddc9bc9b1a --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + public class AppSettingsNucachePropertyMapFactory : INucachePropertyOptionsFactory + { + public NucachePropertyOptions GetNucachePropertyOptions() + { + NucachePropertyOptions options = new NucachePropertyOptions + { + PropertyMap = GetPropertyMap(), + LZ4CompressionLevel = K4os.Compression.LZ4.LZ4Level.L10_OPT, + MinimumCompressibleStringLength = null + }; + return options; + } + + public IReadOnlyDictionary GetPropertyMap() + { + var propertyMap = new Dictionary(); + // TODO: Use xml/json/c# to define map + var propertyDictionarySerializerMap = ConfigurationManager.AppSettings["Umbraco.Web.PublishedCache.NuCache.PropertySerializationMap"]; + if (!string.IsNullOrWhiteSpace(propertyDictionarySerializerMap)) + { + //propertyAlias,CompressionLevel,DecompressionLevel,mappedAlias; + propertyDictionarySerializerMap.Split(';') + .Select(x => + { + var y = x.Split(','); + (string alias, NucachePropertyCompressionLevel compressionLevel, NucachePropertyDecompressionLevel decompressionLevel, string mappedAlias) v = (y[0], + (NucachePropertyCompressionLevel)System.Enum.Parse(typeof(NucachePropertyCompressionLevel), y[1]), + (NucachePropertyDecompressionLevel)System.Enum.Parse(typeof(NucachePropertyDecompressionLevel), y[2]), + y[3] + ); + return v; + }) + .ToList().ForEach(x => propertyMap.Add(x.alias, (x.compressionLevel, x.decompressionLevel, x.mappedAlias))); + } + return propertyMap; + } + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.ContentDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.ContentDataSerializer.cs index d02af375c6..9cc5d3a701 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.ContentDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.ContentDataSerializer.cs @@ -5,8 +5,17 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { class ContentDataSerializer : ISerializer { + public ContentDataSerializer(IDictionaryOfPropertyDataSerializer dictionaryOfPropertyDataSerializer = null) + { + _dictionaryOfPropertyDataSerializer = dictionaryOfPropertyDataSerializer; + if(_dictionaryOfPropertyDataSerializer == null) + { + _dictionaryOfPropertyDataSerializer = PropertiesSerializer; + } + } private static readonly DictionaryOfPropertyDataSerializer PropertiesSerializer = new DictionaryOfPropertyDataSerializer(); private static readonly DictionaryOfCultureVariationSerializer CultureVariationsSerializer = new DictionaryOfCultureVariationSerializer(); + private readonly IDictionaryOfPropertyDataSerializer _dictionaryOfPropertyDataSerializer; public ContentData ReadFrom(Stream stream) { @@ -19,7 +28,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource VersionDate = PrimitiveSerializer.DateTime.ReadFrom(stream), WriterId = PrimitiveSerializer.Int32.ReadFrom(stream), TemplateId = PrimitiveSerializer.Int32.ReadFrom(stream), - Properties = PropertiesSerializer.ReadFrom(stream), // TODO: We don't want to allocate empty arrays + Properties = _dictionaryOfPropertyDataSerializer.ReadFrom(stream), // TODO: We don't want to allocate empty arrays CultureInfos = CultureVariationsSerializer.ReadFrom(stream) // TODO: We don't want to allocate empty arrays }; } @@ -36,7 +45,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { PrimitiveSerializer.Int32.WriteTo(value.TemplateId.Value, stream); } - PropertiesSerializer.WriteTo(value.Properties, stream); + _dictionaryOfPropertyDataSerializer.WriteTo(value.Properties, stream); CultureVariationsSerializer.WriteTo(value.CultureInfos, stream); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.ContentNodeKitSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.ContentNodeKitSerializer.cs index f799869850..1d668ba4fd 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.ContentNodeKitSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.ContentNodeKitSerializer.cs @@ -5,7 +5,17 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { internal class ContentNodeKitSerializer : ISerializer { - static readonly ContentDataSerializer DataSerializer = new ContentDataSerializer(); + public ContentNodeKitSerializer(ContentDataSerializer contentDataSerializer = null) + { + _contentDataSerializer = contentDataSerializer; + if(_contentDataSerializer == null) + { + _contentDataSerializer = DefaultDataSerializer; + } + } + static readonly ContentDataSerializer DefaultDataSerializer = new ContentDataSerializer(); + private readonly ContentDataSerializer _contentDataSerializer; + //static readonly ListOfIntSerializer ChildContentIdsSerializer = new ListOfIntSerializer(); public ContentNodeKit ReadFrom(Stream stream) @@ -26,10 +36,10 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource }; var hasDraft = PrimitiveSerializer.Boolean.ReadFrom(stream); if (hasDraft) - kit.DraftData = DataSerializer.ReadFrom(stream); + kit.DraftData = _contentDataSerializer.ReadFrom(stream); var hasPublished = PrimitiveSerializer.Boolean.ReadFrom(stream); if (hasPublished) - kit.PublishedData = DataSerializer.ReadFrom(stream); + kit.PublishedData = _contentDataSerializer.ReadFrom(stream); return kit; } @@ -47,11 +57,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource PrimitiveSerializer.Boolean.WriteTo(value.DraftData != null, stream); if (value.DraftData != null) - DataSerializer.WriteTo(value.DraftData, stream); + _contentDataSerializer.WriteTo(value.DraftData, stream); PrimitiveSerializer.Boolean.WriteTo(value.PublishedData != null, stream); if (value.PublishedData != null) - DataSerializer.WriteTo(value.PublishedData, stream); + _contentDataSerializer.WriteTo(value.PublishedData, stream); } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs index e3fa6597e4..cb12164397 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs @@ -6,7 +6,7 @@ using Umbraco.Core; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { - internal class DictionaryOfPropertyDataSerializer : SerializerBase, ISerializer> + internal class DictionaryOfPropertyDataSerializer : SerializerBase, ISerializer>, IDictionaryOfPropertyDataSerializer { public IDictionary ReadFrom(Stream stream) { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs index 910c0ca737..80d8488495 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs @@ -6,10 +6,10 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { internal class BTree { - public static BPlusTree GetTree(string filepath, bool exists) + public static BPlusTree GetTree(string filepath, bool exists, ContentDataSerializer contentDataSerializer = null) { var keySerializer = new PrimitiveSerializer(); - var valueSerializer = new ContentNodeKitSerializer(); + var valueSerializer = new ContentNodeKitSerializer(contentDataSerializer); var options = new BPlusTree.OptionsV2(keySerializer, valueSerializer) { CreateFile = exists ? CreatePolicy.IfNeeded : CreatePolicy.Always, diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IDictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IDictionaryOfPropertyDataSerializer.cs new file mode 100644 index 0000000000..a086e3e2f3 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IDictionaryOfPropertyDataSerializer.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.IO; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + internal interface IDictionaryOfPropertyDataSerializer + { + IDictionary ReadFrom(Stream stream); + void WriteTo(IDictionary value, Stream stream); + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/INucachePropertyOptionsFactory.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/INucachePropertyOptionsFactory.cs new file mode 100644 index 0000000000..82df43a5bb --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/INucachePropertyOptionsFactory.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + public interface INucachePropertyOptionsFactory + { + NucachePropertyOptions GetNucachePropertyOptions(); + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs new file mode 100644 index 0000000000..99bc2f5859 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs @@ -0,0 +1,31 @@ +using K4os.Compression.LZ4; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + public class LazyCompressedString + { + private byte[] _bytes; + private string _str; + + public LazyCompressedString(byte[] bytes) + { + _bytes = bytes; + } + public override string ToString() + { + return LazyInitializer.EnsureInitialized(ref _str, () => + { + var str = Encoding.UTF8.GetString(LZ4Pickler.Unpickle(_bytes)); + _bytes = null; + return str; + }); + } + } + +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs new file mode 100644 index 0000000000..7ec2fb17b6 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using CSharpTest.Net.Serialization; +using Umbraco.Core; +using System.Text; +using K4os.Compression.LZ4; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + /// + /// If/where to compress custom properties for nucache + /// + public enum NucachePropertyCompressionLevel + { + None = 0, + SQLDatabase = 1, + NucacheDatabase = 2 + } + /// + /// If/where to decompress custom properties for nucache + /// + public enum NucachePropertyDecompressionLevel + { + NotCompressed = 0, + Immediate = 1, + Lazy = 2 + } + + + internal class Lz4DictionaryOfPropertyDataSerializer : SerializerBase, ISerializer>, IDictionaryOfPropertyDataSerializer + { + private readonly IReadOnlyDictionary _compressProperties; + private readonly LZ4Level _compressionLevel; + private readonly long? _minimumStringLengthForCompression; + private readonly IReadOnlyDictionary _uncompressProperties; + + + public Lz4DictionaryOfPropertyDataSerializer(NucachePropertyOptions nucachePropertyOptions) + { + _compressProperties = nucachePropertyOptions.PropertyMap.ToList().ToDictionary(x => string.Intern(x.Value.mappedAlias), x => (x.Value.compress,x.Value.decompressionLevel, string.Intern(x.Value.mappedAlias))); + _minimumStringLengthForCompression = nucachePropertyOptions.MinimumCompressibleStringLength; + _uncompressProperties = _compressProperties.ToList().ToDictionary(x => x.Value.mappedAlias, x => (x.Value.compress, x.Value.decompressionLevel, x.Value.mappedAlias)); + + _compressionLevel = nucachePropertyOptions.LZ4CompressionLevel; + } + + + public IDictionary ReadFrom(Stream stream) + { + var dict = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + // read properties count + var pcount = PrimitiveSerializer.Int32.ReadFrom(stream); + + // read each property + for (var i = 0; i < pcount; i++) + { + + // read property alias + var alias = PrimitiveSerializer.String.ReadFrom(stream); + var map = GetDeSerializationMap(alias); + var key = string.Intern(map.MappedAlias ?? alias); + + // read values count + var vcount = PrimitiveSerializer.Int32.ReadFrom(stream); + + // create pdata and add to the dictionary + var pdatas = new List(); + + // for each value, read and add to pdata + for (var j = 0; j < vcount; j++) + { + var pdata = new PropertyData(); + pdatas.Add(pdata); + + // everything that can be null is read/written as object + // even though - culture and segment should never be null here, as 'null' represents + // the 'current' value, and string.Empty should be used to represent the invariant or + // neutral values - PropertyData throws when getting nulls, so falling back to + // string.Empty here - what else? + pdata.Culture = ReadStringObject(stream, true) ?? string.Empty; + pdata.Segment = ReadStringObject(stream, true) ?? string.Empty; + pdata.Value = ReadObject(stream); + + var decompressionLevel = NucachePropertyDecompressionLevel.Immediate; + if ((map.Compress.Equals(NucachePropertyCompressionLevel.NucacheDatabase) || map.Compress.Equals(NucachePropertyCompressionLevel.SQLDatabase)) + && pdata.Value != null && pdata.Value is byte[] byteArrayValue) + { + //Compressed string + switch (decompressionLevel) + { + case NucachePropertyDecompressionLevel.Lazy: + pdata.Value = new LazyCompressedString(byteArrayValue); + break; + case NucachePropertyDecompressionLevel.NotCompressed: + break;//Shouldn't be any not compressed + case NucachePropertyDecompressionLevel.Immediate: + default: + pdata.Value = Encoding.UTF8.GetString(LZ4Pickler.Unpickle(byteArrayValue)); + break; + } + } + } + + dict[key] = pdatas.ToArray(); + } + return dict; + } + + public void WriteTo(IDictionary value, Stream stream) + { + // write properties count + PrimitiveSerializer.Int32.WriteTo(value.Count, stream); + + // write each property + foreach (var (alias, values) in value) + { + var map = GetSerializationMap(alias); + + // write alias + PrimitiveSerializer.String.WriteTo(map.MappedAlias ?? alias, stream); + + // write values count + PrimitiveSerializer.Int32.WriteTo(values.Length, stream); + + // write each value + foreach (var pdata in values) + { + // everything that can be null is read/written as object + // even though - culture and segment should never be null here, + // see note in ReadFrom() method above + WriteObject(pdata.Culture ?? string.Empty, stream); + WriteObject(pdata.Segment ?? string.Empty, stream); + + //Only compress strings + if (pdata.Value is string stringValue && pdata.Value != null && map.Compress.Equals(NucachePropertyCompressionLevel.NucacheDatabase) + && (_minimumStringLengthForCompression == null + || !_minimumStringLengthForCompression.HasValue + || stringValue.Length > _minimumStringLengthForCompression.Value)) + { + var stringBytes = Encoding.UTF8.GetBytes(stringValue); + var compressedBytes = LZ4Pickler.Pickle(stringBytes, _compressionLevel); + WriteObject(compressedBytes, stream); + } + WriteObject(pdata.Value, stream); + } + } + } + private readonly (NucachePropertyCompressionLevel Compress, NucachePropertyDecompressionLevel decompressionLevel, string MappedAlias) DEFAULT_MAP =(NucachePropertyCompressionLevel.None, NucachePropertyDecompressionLevel.NotCompressed, null); + public (NucachePropertyCompressionLevel Compress, NucachePropertyDecompressionLevel decompressionLevel, string MappedAlias) GetSerializationMap(string propertyAlias) + { + if (_compressProperties == null) + { + return DEFAULT_MAP; + } + if (_compressProperties.TryGetValue(propertyAlias, out (NucachePropertyCompressionLevel compress, NucachePropertyDecompressionLevel decompressionLevel, string mappedAlias) map)) + { + return map; + } + + return DEFAULT_MAP; + } + public (NucachePropertyCompressionLevel Compress, NucachePropertyDecompressionLevel decompressionLevel, string MappedAlias) GetDeSerializationMap(string propertyAlias) + { + if (_uncompressProperties == null) + { + return DEFAULT_MAP; + } + if (_uncompressProperties.TryGetValue(propertyAlias, out (NucachePropertyCompressionLevel compress, NucachePropertyDecompressionLevel decompressionLevel, string mappedAlias) map)) + { + return map; + } + return DEFAULT_MAP; + } + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index 1a093292b5..be4e8a9832 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -1,16 +1,20 @@ -using MessagePack; +using K4os.Compression.LZ4; +using MessagePack; using MessagePack.Formatters; using MessagePack.Resolvers; using System; using System.Collections.Generic; +using System.Linq; +using System.Text; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { internal class MsgPackContentNestedDataSerializer : IContentNestedDataByteSerializer { private MessagePackSerializerOptions _options; + private readonly NucachePropertyOptions _propertyOptions; - public MsgPackContentNestedDataSerializer() + public MsgPackContentNestedDataSerializer(NucachePropertyOptions propertyOptions = null) { var defaultOptions = ContractlessStandardResolver.Options; @@ -30,6 +34,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource _options = defaultOptions .WithResolver(resolver) .WithCompression(MessagePackCompression.Lz4BlockArray); + _propertyOptions = propertyOptions ?? new NucachePropertyOptions(); } public string ToJson(string serialized) @@ -48,10 +53,42 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public string Serialize(ContentNestedData nestedData) { + Optimize(nestedData); + var bin = MessagePackSerializer.Serialize(nestedData, _options); return Convert.ToBase64String(bin); } + /// + /// Compress properties and map property names to shorter names + /// + /// + private void Optimize(ContentNestedData nestedData) + { + if (_propertyOptions.PropertyMap != null && _propertyOptions.PropertyMap.Any()) + { + foreach (var map in _propertyOptions.PropertyMap) + { + if (map.Value.compress.Equals(NucachePropertyCompressionLevel.SQLDatabase)) + { + if (nestedData.PropertyData.TryGetValue(map.Key, out PropertyData[] properties)) + { + foreach (var property in properties.Where(x => x.Value != null && x.Value is string)) + { + property.Value = LZ4Pickler.Pickle(Encoding.UTF8.GetBytes(property.Value as string), _propertyOptions.LZ4CompressionLevel); + } + } + } + if (map.Value.mappedAlias != null && !map.Key.Equals(map.Value.mappedAlias) + && nestedData.PropertyData.Remove(map.Key) && nestedData.PropertyData.TryGetValue(map.Key, out PropertyData[] properties2)) + { + nestedData.PropertyData.Remove(map.Key); + nestedData.PropertyData.Add(map.Value.mappedAlias, properties2); + } + } + } + } + public ContentNestedData DeserializeBytes(byte[] data) => MessagePackSerializer.Deserialize(data, _options); public byte[] SerializeBytes(ContentNestedData nestedData) => MessagePackSerializer.Serialize(nestedData, _options); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyOptions.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyOptions.cs new file mode 100644 index 0000000000..f10787646a --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyOptions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + public class NucachePropertyOptions + { + public IReadOnlyDictionary PropertyMap + { get; set; } = new Dictionary(); + + public K4os.Compression.LZ4.LZ4Level LZ4CompressionLevel { get; set; } = K4os.Compression.LZ4.LZ4Level.L00_FAST; + + public long? MinimumCompressibleStringLength { get; set; } + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs index afc0827ed3..885b5cf80e 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs @@ -16,6 +16,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource private const char PrefixDouble = 'B'; private const char PrefixDateTime = 'D'; private const char PrefixByte = 'O'; + private const char PrefixByteArray = 'A'; protected string ReadString(Stream stream) => PrimitiveSerializer.String.ReadFrom(stream); protected int ReadInt(Stream stream) => PrimitiveSerializer.Int32.ReadFrom(stream); @@ -23,6 +24,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource protected float ReadFloat(Stream stream) => PrimitiveSerializer.Float.ReadFrom(stream); protected double ReadDouble(Stream stream) => PrimitiveSerializer.Double.ReadFrom(stream); protected DateTime ReadDateTime(Stream stream) => PrimitiveSerializer.DateTime.ReadFrom(stream); + protected byte[] ReadByteArray(Stream stream) => PrimitiveSerializer.Bytes.ReadFrom(stream); private T? ReadObject(Stream stream, char t, Func read) where T : struct @@ -39,7 +41,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource var type = PrimitiveSerializer.Char.ReadFrom(stream); if (type == PrefixNull) return null; if (type != PrefixString) - throw new NotSupportedException($"Cannot deserialize type '{type}', expected 'S'."); + throw new NotSupportedException($"Cannot deserialize type '{type}', expected '{PrefixString}'."); return intern ? string.Intern(PrimitiveSerializer.String.ReadFrom(stream)) : PrimitiveSerializer.String.ReadFrom(stream); @@ -51,6 +53,16 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource protected double? ReadDoubleObject(Stream stream) => ReadObject(stream, PrefixDouble, ReadDouble); protected DateTime? ReadDateTimeObject(Stream stream) => ReadObject(stream, PrefixDateTime, ReadDateTime); + protected byte[] ReadByteArrayObject(Stream stream) // required 'cos byte[] is not a struct + { + var type = PrimitiveSerializer.Char.ReadFrom(stream); + if (type == PrefixNull) return null; + if (type != PrefixByteArray) + throw new NotSupportedException($"Cannot deserialize type '{type}', expected '{PrefixByteArray}'."); + return PrimitiveSerializer.Bytes.ReadFrom(stream); + } + + protected object ReadObject(Stream stream) => ReadObject(PrimitiveSerializer.Char.ReadFrom(stream), stream); @@ -81,6 +93,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return PrimitiveSerializer.Double.ReadFrom(stream); case PrefixDateTime: return PrimitiveSerializer.DateTime.ReadFrom(stream); + case PrefixByteArray: + return PrimitiveSerializer.Bytes.ReadFrom(stream); default: throw new NotSupportedException($"Cannot deserialize unknown type '{type}'."); } @@ -137,6 +151,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource PrimitiveSerializer.Char.WriteTo(PrefixUInt32, stream); PrimitiveSerializer.UInt32.WriteTo(uInt32Value, stream); } + else if (value is byte[] byteArrayValue) + { + PrimitiveSerializer.Char.WriteTo(PrefixByteArray, stream); + PrimitiveSerializer.Bytes.WriteTo(byteArrayValue, stream); + } else throw new NotSupportedException("Value type " + value.GetType().FullName + " cannot be serialized."); } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs index 205ac55cdc..95ff91304d 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs @@ -1,4 +1,6 @@ -using System.Configuration; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Web.PublishedCache.NuCache.DataSource; @@ -15,11 +17,26 @@ namespace Umbraco.Web.PublishedCache.NuCache if (serializer == "MsgPack") { - composition.Register(); + var propertyDictionarySerializer = ConfigurationManager.AppSettings["Umbraco.Web.PublishedCache.NuCache.DictionaryOfPropertiesSerializer"]; + if (propertyDictionarySerializer == "LZ4Map") + { + composition.Register(); + composition.Register(factory => + { + var lz4Serializer = factory.GetInstance(); + return new ContentDataSerializer(lz4Serializer); + }); + } + else + { + composition.Register(factory => new ContentDataSerializer(new DictionaryOfPropertyDataSerializer())); + } + composition.Register(); } else { composition.Register(); + composition.Register(factory => new ContentDataSerializer(new DictionaryOfPropertyDataSerializer())); } // register the NuCache database data source @@ -34,5 +51,6 @@ namespace Umbraco.Web.PublishedCache.NuCache // TODO: no NuCache health check yet //composition.HealthChecks().Add(); } + } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index c87f035589..0135f204c7 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -50,6 +50,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly IDefaultCultureAccessor _defaultCultureAccessor; private readonly UrlSegmentProviderCollection _urlSegmentProviders; private readonly IContentNestedDataSerializer _contentNestedDataSerializer; + private readonly ContentDataSerializer _contentDataSerializer; // volatile because we read it with no lock private volatile bool _isReady; @@ -83,7 +84,7 @@ namespace Umbraco.Web.PublishedCache.NuCache IDataSource dataSource, IGlobalSettings globalSettings, IEntityXmlSerializer entitySerializer, IPublishedModelFactory publishedModelFactory, - UrlSegmentProviderCollection urlSegmentProviders, IContentNestedDataSerializer contentNestedDataSerializer) + UrlSegmentProviderCollection urlSegmentProviders, IContentNestedDataSerializer contentNestedDataSerializer, ContentDataSerializer contentDataSerializer = null) : base(publishedSnapshotAccessor, variationContextAccessor) { //if (Interlocked.Increment(ref _singletonCheck) > 1) @@ -101,6 +102,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _globalSettings = globalSettings; _urlSegmentProviders = urlSegmentProviders; _contentNestedDataSerializer = contentNestedDataSerializer; + _contentDataSerializer = contentDataSerializer; // we need an Xml serializer here so that the member cache can support XPath, // for members this is done by navigating the serialized-to-xml member @@ -182,8 +184,8 @@ namespace Umbraco.Web.PublishedCache.NuCache _localMediaDbExists = File.Exists(localMediaDbPath); // if both local databases exist then GetTree will open them, else new databases will be created - _localContentDb = BTree.GetTree(localContentDbPath, _localContentDbExists); - _localMediaDb = BTree.GetTree(localMediaDbPath, _localMediaDbExists); + _localContentDb = BTree.GetTree(localContentDbPath, _localContentDbExists, _contentDataSerializer); + _localMediaDb = BTree.GetTree(localMediaDbPath, _localMediaDbExists, _contentDataSerializer); _logger.Info("Registered with MainDom, localContentDbExists? {LocalContentDbExists}, localMediaDbExists? {LocalMediaDbExists}", _localContentDbExists, _localMediaDbExists); } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 088fb0eeb3..6aae3f5ed9 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -68,6 +68,9 @@ 2.7.0.100 + + 1.1.11 + @@ -248,9 +251,15 @@ + + + + + + From d1449a0f5cb38a94ddb836085446826dc3aae8b4 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Thu, 9 Jul 2020 11:50:15 +1200 Subject: [PATCH 016/289] Fix key mapping --- .../DataSource/Lz4DictionaryOfPropertyDataSerializer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs index 7ec2fb17b6..b1e913bdec 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs @@ -39,9 +39,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public Lz4DictionaryOfPropertyDataSerializer(NucachePropertyOptions nucachePropertyOptions) { - _compressProperties = nucachePropertyOptions.PropertyMap.ToList().ToDictionary(x => string.Intern(x.Value.mappedAlias), x => (x.Value.compress,x.Value.decompressionLevel, string.Intern(x.Value.mappedAlias))); + _compressProperties = nucachePropertyOptions.PropertyMap.ToList().ToDictionary(x => string.Intern(x.Key), x => (x.Value.compress,x.Value.decompressionLevel, string.Intern(x.Value.mappedAlias))); _minimumStringLengthForCompression = nucachePropertyOptions.MinimumCompressibleStringLength; - _uncompressProperties = _compressProperties.ToList().ToDictionary(x => x.Value.mappedAlias, x => (x.Value.compress, x.Value.decompressionLevel, x.Value.mappedAlias)); + _uncompressProperties = _compressProperties.ToList().ToDictionary(x => x.Value.mappedAlias, x => (x.Value.compress, x.Value.decompressionLevel, x.Key)); _compressionLevel = nucachePropertyOptions.LZ4CompressionLevel; } From d4276dff58dd9ed69afbf1e3af34da775f80bde2 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Tue, 4 Aug 2020 18:01:06 +1200 Subject: [PATCH 017/289] Fix composition. Store compression options in map --- .../AppSettingsNucachePropertyMapFactory.cs | 4 ++-- .../INucachePropertyOptionsFactory.cs | 4 ++-- .../DataSource/LazyCompressedString.cs | 10 +++++++- .../Lz4DictionaryOfPropertyDataSerializer.cs | 21 ++++++++-------- .../MsgPackContentNestedDataSerializer.cs | 4 ++-- .../PublishedCache/NuCache/NuCacheComposer.cs | 5 ++-- src/Umbraco.Web/Umbraco.Web.csproj | 2 +- src/umbraco.sln | 24 +++++++++++++++++-- 8 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs index ddc9bc9b1a..cf44290bd4 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs @@ -7,9 +7,9 @@ using System.Threading.Tasks; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { - public class AppSettingsNucachePropertyMapFactory : INucachePropertyOptionsFactory + public class AppSettingsNuCachePropertyMapFactory : INuCachePropertyOptionsFactory { - public NucachePropertyOptions GetNucachePropertyOptions() + public NucachePropertyOptions GetNuCachePropertyOptions() { NucachePropertyOptions options = new NucachePropertyOptions { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/INucachePropertyOptionsFactory.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/INucachePropertyOptionsFactory.cs index 82df43a5bb..efc5e599ea 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/INucachePropertyOptionsFactory.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/INucachePropertyOptionsFactory.cs @@ -6,8 +6,8 @@ using System.Threading.Tasks; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { - public interface INucachePropertyOptionsFactory + public interface INuCachePropertyOptionsFactory { - NucachePropertyOptions GetNucachePropertyOptions(); + NucachePropertyOptions GetNuCachePropertyOptions(); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs index 99bc2f5859..3d6e70c7b2 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs @@ -8,15 +8,23 @@ using System.Threading.Tasks; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { - public class LazyCompressedString + /// + /// Lazily decompresses a LZ4 Pickler compressed UTF8 string + /// + internal class LazyCompressedString { private byte[] _bytes; private string _str; + /// + /// Constructor + /// + /// LZ4 Pickle compressed UTF8 String public LazyCompressedString(byte[] bytes) { _bytes = bytes; } + public override string ToString() { return LazyInitializer.EnsureInitialized(ref _str, () => diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs index b1e913bdec..a5e3034872 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs @@ -32,18 +32,16 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource internal class Lz4DictionaryOfPropertyDataSerializer : SerializerBase, ISerializer>, IDictionaryOfPropertyDataSerializer { private readonly IReadOnlyDictionary _compressProperties; - private readonly LZ4Level _compressionLevel; - private readonly long? _minimumStringLengthForCompression; private readonly IReadOnlyDictionary _uncompressProperties; - public Lz4DictionaryOfPropertyDataSerializer(NucachePropertyOptions nucachePropertyOptions) + public Lz4DictionaryOfPropertyDataSerializer(INuCachePropertyOptionsFactory nucachePropertyOptionsFactory) { + var nucachePropertyOptions = nucachePropertyOptionsFactory.GetNuCachePropertyOptions(); _compressProperties = nucachePropertyOptions.PropertyMap.ToList().ToDictionary(x => string.Intern(x.Key), x => (x.Value.compress,x.Value.decompressionLevel, string.Intern(x.Value.mappedAlias))); - _minimumStringLengthForCompression = nucachePropertyOptions.MinimumCompressibleStringLength; _uncompressProperties = _compressProperties.ToList().ToDictionary(x => x.Value.mappedAlias, x => (x.Value.compress, x.Value.decompressionLevel, x.Key)); - _compressionLevel = nucachePropertyOptions.LZ4CompressionLevel; + _nucachePropertyOptions = nucachePropertyOptions; } @@ -84,12 +82,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource pdata.Segment = ReadStringObject(stream, true) ?? string.Empty; pdata.Value = ReadObject(stream); - var decompressionLevel = NucachePropertyDecompressionLevel.Immediate; if ((map.Compress.Equals(NucachePropertyCompressionLevel.NucacheDatabase) || map.Compress.Equals(NucachePropertyCompressionLevel.SQLDatabase)) && pdata.Value != null && pdata.Value is byte[] byteArrayValue) { //Compressed string - switch (decompressionLevel) + switch (map.decompressionLevel) { case NucachePropertyDecompressionLevel.Lazy: pdata.Value = new LazyCompressedString(byteArrayValue); @@ -136,12 +133,12 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource //Only compress strings if (pdata.Value is string stringValue && pdata.Value != null && map.Compress.Equals(NucachePropertyCompressionLevel.NucacheDatabase) - && (_minimumStringLengthForCompression == null - || !_minimumStringLengthForCompression.HasValue - || stringValue.Length > _minimumStringLengthForCompression.Value)) + && (_nucachePropertyOptions.MinimumCompressibleStringLength == null + || !_nucachePropertyOptions.MinimumCompressibleStringLength.HasValue + || stringValue.Length > _nucachePropertyOptions.MinimumCompressibleStringLength.Value)) { var stringBytes = Encoding.UTF8.GetBytes(stringValue); - var compressedBytes = LZ4Pickler.Pickle(stringBytes, _compressionLevel); + var compressedBytes = LZ4Pickler.Pickle(stringBytes, _nucachePropertyOptions.LZ4CompressionLevel); WriteObject(compressedBytes, stream); } WriteObject(pdata.Value, stream); @@ -149,6 +146,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } } private readonly (NucachePropertyCompressionLevel Compress, NucachePropertyDecompressionLevel decompressionLevel, string MappedAlias) DEFAULT_MAP =(NucachePropertyCompressionLevel.None, NucachePropertyDecompressionLevel.NotCompressed, null); + private readonly NucachePropertyOptions _nucachePropertyOptions; + public (NucachePropertyCompressionLevel Compress, NucachePropertyDecompressionLevel decompressionLevel, string MappedAlias) GetSerializationMap(string propertyAlias) { if (_compressProperties == null) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index be4e8a9832..c83bc3e973 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -14,7 +14,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource private MessagePackSerializerOptions _options; private readonly NucachePropertyOptions _propertyOptions; - public MsgPackContentNestedDataSerializer(NucachePropertyOptions propertyOptions = null) + public MsgPackContentNestedDataSerializer(INuCachePropertyOptionsFactory propertyOptionsFactory = null) { var defaultOptions = ContractlessStandardResolver.Options; @@ -34,7 +34,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource _options = defaultOptions .WithResolver(resolver) .WithCompression(MessagePackCompression.Lz4BlockArray); - _propertyOptions = propertyOptions ?? new NucachePropertyOptions(); + _propertyOptions = propertyOptionsFactory?.GetNuCachePropertyOptions() ?? new NucachePropertyOptions(); } public string ToJson(string serialized) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs index 95ff91304d..1ee6b96eb1 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs @@ -14,13 +14,14 @@ namespace Umbraco.Web.PublishedCache.NuCache base.Compose(composition); var serializer = ConfigurationManager.AppSettings["Umbraco.Web.PublishedCache.NuCache.Serializer"]; - + composition.Register(); + composition.Register(); if (serializer == "MsgPack") { var propertyDictionarySerializer = ConfigurationManager.AppSettings["Umbraco.Web.PublishedCache.NuCache.DictionaryOfPropertiesSerializer"]; if (propertyDictionarySerializer == "LZ4Map") { - composition.Register(); + composition.Register(factory => { var lz4Serializer = factory.GetInstance(); diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 6aae3f5ed9..549492603b 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -251,7 +251,7 @@ - + diff --git a/src/umbraco.sln b/src/umbraco.sln index 63fb856b5d..da9d7ae557 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -58,8 +58,24 @@ Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.Client", "ht StartServerOnDebug = "false" EndProjectSection EndProject -Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTest\", "Umbraco.Tests.AcceptanceTest\", "{9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}" +Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTest", "Umbraco.Tests.AcceptanceTest\", "{9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}" ProjectSection(WebsiteProperties) = preProject + TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.0" + Debug.AspNetCompiler.VirtualPath = "/localhost_49800" + Debug.AspNetCompiler.PhysicalPath = "Umbraco.Tests.AcceptanceTest\" + Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_49800\" + Debug.AspNetCompiler.Updateable = "true" + Debug.AspNetCompiler.ForceOverwrite = "true" + Debug.AspNetCompiler.FixedNames = "false" + Debug.AspNetCompiler.Debug = "True" + Release.AspNetCompiler.VirtualPath = "/localhost_49800" + Release.AspNetCompiler.PhysicalPath = "Umbraco.Tests.AcceptanceTest\" + Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_49800\" + Release.AspNetCompiler.Updateable = "true" + Release.AspNetCompiler.ForceOverwrite = "true" + Release.AspNetCompiler.FixedNames = "false" + Release.AspNetCompiler.Debug = "False" + VWDPort = "49800" SlnRelativePath = "Umbraco.Tests.AcceptanceTest\" EndProjectSection EndProject @@ -123,6 +139,10 @@ Global {4C4C194C-B5E4-4991-8F87-4373E24CC19F}.Release|Any CPU.Build.0 = Release|Any CPU {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Release|Any CPU.Build.0 = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Debug|Any CPU.Build.0 = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -157,6 +177,7 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {227C3B55-80E5-4E7E-A802-BE16C5128B9D} = {2849E9D4-3B4E-40A3-A309-F3CB4F0E125F} + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {5D3B8245-ADA6-453F-A008-50ED04BFE770} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {E3F9F378-AFE1-40A5-90BD-82833375DBFE} = {227C3B55-80E5-4E7E-A802-BE16C5128B9D} {5B03EF4E-E0AC-4905-861B-8C3EC1A0D458} = {227C3B55-80E5-4E7E-A802-BE16C5128B9D} @@ -164,7 +185,6 @@ Global {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {C7311C00-2184-409B-B506-52A5FAEA8736} = {FD962632-184C-4005-A5F3-E705D92FC645} {FB5676ED-7A69-492C-B802-E7B24144C0FC} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC} From 451eacf7cbcee1c175fe676f5e1f9e237d406253 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Tue, 4 Aug 2020 18:03:08 +1200 Subject: [PATCH 018/289] Fix NuCache spelling --- .../DataSource/AppSettingsNucachePropertyMapFactory.cs | 4 ++-- .../NuCache/DataSource/INucachePropertyOptionsFactory.cs | 2 +- .../DataSource/Lz4DictionaryOfPropertyDataSerializer.cs | 2 +- .../NuCache/DataSource/MsgPackContentNestedDataSerializer.cs | 4 ++-- .../NuCache/DataSource/NucachePropertyOptions.cs | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs index cf44290bd4..6ca4c3e666 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs @@ -9,9 +9,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { public class AppSettingsNuCachePropertyMapFactory : INuCachePropertyOptionsFactory { - public NucachePropertyOptions GetNuCachePropertyOptions() + public NuCachePropertyOptions GetNuCachePropertyOptions() { - NucachePropertyOptions options = new NucachePropertyOptions + NuCachePropertyOptions options = new NuCachePropertyOptions { PropertyMap = GetPropertyMap(), LZ4CompressionLevel = K4os.Compression.LZ4.LZ4Level.L10_OPT, diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/INucachePropertyOptionsFactory.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/INucachePropertyOptionsFactory.cs index efc5e599ea..0cb694e1c1 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/INucachePropertyOptionsFactory.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/INucachePropertyOptionsFactory.cs @@ -8,6 +8,6 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { public interface INuCachePropertyOptionsFactory { - NucachePropertyOptions GetNuCachePropertyOptions(); + NuCachePropertyOptions GetNuCachePropertyOptions(); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs index a5e3034872..501ead8386 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs @@ -146,7 +146,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } } private readonly (NucachePropertyCompressionLevel Compress, NucachePropertyDecompressionLevel decompressionLevel, string MappedAlias) DEFAULT_MAP =(NucachePropertyCompressionLevel.None, NucachePropertyDecompressionLevel.NotCompressed, null); - private readonly NucachePropertyOptions _nucachePropertyOptions; + private readonly NuCachePropertyOptions _nucachePropertyOptions; public (NucachePropertyCompressionLevel Compress, NucachePropertyDecompressionLevel decompressionLevel, string MappedAlias) GetSerializationMap(string propertyAlias) { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index c83bc3e973..31421e7177 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource internal class MsgPackContentNestedDataSerializer : IContentNestedDataByteSerializer { private MessagePackSerializerOptions _options; - private readonly NucachePropertyOptions _propertyOptions; + private readonly NuCachePropertyOptions _propertyOptions; public MsgPackContentNestedDataSerializer(INuCachePropertyOptionsFactory propertyOptionsFactory = null) { @@ -34,7 +34,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource _options = defaultOptions .WithResolver(resolver) .WithCompression(MessagePackCompression.Lz4BlockArray); - _propertyOptions = propertyOptionsFactory?.GetNuCachePropertyOptions() ?? new NucachePropertyOptions(); + _propertyOptions = propertyOptionsFactory?.GetNuCachePropertyOptions() ?? new NuCachePropertyOptions(); } public string ToJson(string serialized) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyOptions.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyOptions.cs index f10787646a..be0118f563 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyOptions.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyOptions.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { - public class NucachePropertyOptions + public class NuCachePropertyOptions { public IReadOnlyDictionary PropertyMap diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 549492603b..a4e5640d36 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -254,12 +254,12 @@ - + - + From e65f3a7e80a91e5296e7f5e8e1ad5d70a8545ef0 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Tue, 4 Aug 2020 18:21:16 +1200 Subject: [PATCH 019/289] undo solution changes --- src/umbraco.sln | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/src/umbraco.sln b/src/umbraco.sln index da9d7ae557..63fb856b5d 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -58,24 +58,8 @@ Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.Client", "ht StartServerOnDebug = "false" EndProjectSection EndProject -Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTest", "Umbraco.Tests.AcceptanceTest\", "{9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}" +Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTest\", "Umbraco.Tests.AcceptanceTest\", "{9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}" ProjectSection(WebsiteProperties) = preProject - TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.0" - Debug.AspNetCompiler.VirtualPath = "/localhost_49800" - Debug.AspNetCompiler.PhysicalPath = "Umbraco.Tests.AcceptanceTest\" - Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_49800\" - Debug.AspNetCompiler.Updateable = "true" - Debug.AspNetCompiler.ForceOverwrite = "true" - Debug.AspNetCompiler.FixedNames = "false" - Debug.AspNetCompiler.Debug = "True" - Release.AspNetCompiler.VirtualPath = "/localhost_49800" - Release.AspNetCompiler.PhysicalPath = "Umbraco.Tests.AcceptanceTest\" - Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_49800\" - Release.AspNetCompiler.Updateable = "true" - Release.AspNetCompiler.ForceOverwrite = "true" - Release.AspNetCompiler.FixedNames = "false" - Release.AspNetCompiler.Debug = "False" - VWDPort = "49800" SlnRelativePath = "Umbraco.Tests.AcceptanceTest\" EndProjectSection EndProject @@ -139,10 +123,6 @@ Global {4C4C194C-B5E4-4991-8F87-4373E24CC19F}.Release|Any CPU.Build.0 = Release|Any CPU {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Release|Any CPU.Build.0 = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Debug|Any CPU.Build.0 = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -177,7 +157,6 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {227C3B55-80E5-4E7E-A802-BE16C5128B9D} = {2849E9D4-3B4E-40A3-A309-F3CB4F0E125F} - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {5D3B8245-ADA6-453F-A008-50ED04BFE770} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {E3F9F378-AFE1-40A5-90BD-82833375DBFE} = {227C3B55-80E5-4E7E-A802-BE16C5128B9D} {5B03EF4E-E0AC-4905-861B-8C3EC1A0D458} = {227C3B55-80E5-4E7E-A802-BE16C5128B9D} @@ -185,6 +164,7 @@ Global {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {C7311C00-2184-409B-B506-52A5FAEA8736} = {FD962632-184C-4005-A5F3-E705D92FC645} {FB5676ED-7A69-492C-B802-E7B24144C0FC} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC} From 0d159751b99eb5bc3946e69e58271ce1bb5d507a Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 13 Aug 2020 19:15:13 +1000 Subject: [PATCH 020/289] Cleans up code - removes tuples everywhere --- .../AppSettingsNucachePropertyMapFactory.cs | 9 ++-- .../Lz4DictionaryOfPropertyDataSerializer.cs | 42 ++++++++-------- .../MsgPackContentNestedDataSerializer.cs | 6 +-- .../DataSource/NuCacheCompressionOptions.cs | 50 +++++++++++++++++++ .../DataSource/NucachePropertyOptions.cs | 9 ++-- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 6 files changed, 83 insertions(+), 34 deletions(-) create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCacheCompressionOptions.cs diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs index 6ca4c3e666..271695d250 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs @@ -7,7 +7,8 @@ using System.Threading.Tasks; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { - public class AppSettingsNuCachePropertyMapFactory : INuCachePropertyOptionsFactory + // TODO: We'll remove this when the responsibility for compressing property data is at the property editor level + internal class AppSettingsNuCachePropertyMapFactory : INuCachePropertyOptionsFactory { public NuCachePropertyOptions GetNuCachePropertyOptions() { @@ -20,9 +21,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return options; } - public IReadOnlyDictionary GetPropertyMap() + public IReadOnlyDictionary GetPropertyMap() { - var propertyMap = new Dictionary(); + var propertyMap = new Dictionary(); // TODO: Use xml/json/c# to define map var propertyDictionarySerializerMap = ConfigurationManager.AppSettings["Umbraco.Web.PublishedCache.NuCache.PropertySerializationMap"]; if (!string.IsNullOrWhiteSpace(propertyDictionarySerializerMap)) @@ -39,7 +40,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource ); return v; }) - .ToList().ForEach(x => propertyMap.Add(x.alias, (x.compressionLevel, x.decompressionLevel, x.mappedAlias))); + .ToList().ForEach(x => propertyMap.Add(x.alias, new NuCacheCompressionOptions(x.compressionLevel, x.decompressionLevel, x.mappedAlias))); } return propertyMap; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs index 501ead8386..c62af1293f 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs @@ -16,7 +16,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { None = 0, SQLDatabase = 1, - NucacheDatabase = 2 + NuCacheDatabase = 2 } /// /// If/where to decompress custom properties for nucache @@ -28,22 +28,22 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource Lazy = 2 } - + internal class Lz4DictionaryOfPropertyDataSerializer : SerializerBase, ISerializer>, IDictionaryOfPropertyDataSerializer { - private readonly IReadOnlyDictionary _compressProperties; - private readonly IReadOnlyDictionary _uncompressProperties; + private readonly IReadOnlyDictionary _compressProperties; + private readonly IReadOnlyDictionary _uncompressProperties; public Lz4DictionaryOfPropertyDataSerializer(INuCachePropertyOptionsFactory nucachePropertyOptionsFactory) { var nucachePropertyOptions = nucachePropertyOptionsFactory.GetNuCachePropertyOptions(); - _compressProperties = nucachePropertyOptions.PropertyMap.ToList().ToDictionary(x => string.Intern(x.Key), x => (x.Value.compress,x.Value.decompressionLevel, string.Intern(x.Value.mappedAlias))); - _uncompressProperties = _compressProperties.ToList().ToDictionary(x => x.Value.mappedAlias, x => (x.Value.compress, x.Value.decompressionLevel, x.Key)); + _compressProperties = nucachePropertyOptions.PropertyMap.ToList().ToDictionary(x => string.Intern(x.Key), x => new NuCacheCompressionOptions(x.Value.CompressLevel, x.Value.DecompressLevel, string.Intern(x.Value.MappedAlias))); + _uncompressProperties = _compressProperties.ToList().ToDictionary(x => x.Value.MappedAlias, x => new NuCacheCompressionOptions(x.Value.CompressLevel, x.Value.DecompressLevel, x.Key)); _nucachePropertyOptions = nucachePropertyOptions; } - + public IDictionary ReadFrom(Stream stream) { @@ -82,11 +82,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource pdata.Segment = ReadStringObject(stream, true) ?? string.Empty; pdata.Value = ReadObject(stream); - if ((map.Compress.Equals(NucachePropertyCompressionLevel.NucacheDatabase) || map.Compress.Equals(NucachePropertyCompressionLevel.SQLDatabase)) + if ((map.CompressLevel.Equals(NucachePropertyCompressionLevel.NuCacheDatabase) || map.CompressLevel.Equals(NucachePropertyCompressionLevel.SQLDatabase)) && pdata.Value != null && pdata.Value is byte[] byteArrayValue) { //Compressed string - switch (map.decompressionLevel) + switch (map.DecompressLevel) { case NucachePropertyDecompressionLevel.Lazy: pdata.Value = new LazyCompressedString(byteArrayValue); @@ -132,7 +132,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource WriteObject(pdata.Segment ?? string.Empty, stream); //Only compress strings - if (pdata.Value is string stringValue && pdata.Value != null && map.Compress.Equals(NucachePropertyCompressionLevel.NucacheDatabase) + if (pdata.Value is string stringValue && pdata.Value != null && map.CompressLevel.Equals(NucachePropertyCompressionLevel.NuCacheDatabase) && (_nucachePropertyOptions.MinimumCompressibleStringLength == null || !_nucachePropertyOptions.MinimumCompressibleStringLength.HasValue || stringValue.Length > _nucachePropertyOptions.MinimumCompressibleStringLength.Value)) @@ -145,33 +145,33 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } } } - private readonly (NucachePropertyCompressionLevel Compress, NucachePropertyDecompressionLevel decompressionLevel, string MappedAlias) DEFAULT_MAP =(NucachePropertyCompressionLevel.None, NucachePropertyDecompressionLevel.NotCompressed, null); + private static readonly NuCacheCompressionOptions DefaultMap = new NuCacheCompressionOptions(NucachePropertyCompressionLevel.None, NucachePropertyDecompressionLevel.NotCompressed, null); private readonly NuCachePropertyOptions _nucachePropertyOptions; - public (NucachePropertyCompressionLevel Compress, NucachePropertyDecompressionLevel decompressionLevel, string MappedAlias) GetSerializationMap(string propertyAlias) + public NuCacheCompressionOptions GetSerializationMap(string propertyAlias) { if (_compressProperties == null) { - return DEFAULT_MAP; + return DefaultMap; } - if (_compressProperties.TryGetValue(propertyAlias, out (NucachePropertyCompressionLevel compress, NucachePropertyDecompressionLevel decompressionLevel, string mappedAlias) map)) + if (_compressProperties.TryGetValue(propertyAlias, out var map1)) { - return map; + return map1; } - return DEFAULT_MAP; + return DefaultMap; } - public (NucachePropertyCompressionLevel Compress, NucachePropertyDecompressionLevel decompressionLevel, string MappedAlias) GetDeSerializationMap(string propertyAlias) + public NuCacheCompressionOptions GetDeSerializationMap(string propertyAlias) { if (_uncompressProperties == null) { - return DEFAULT_MAP; + return DefaultMap; } - if (_uncompressProperties.TryGetValue(propertyAlias, out (NucachePropertyCompressionLevel compress, NucachePropertyDecompressionLevel decompressionLevel, string mappedAlias) map)) + if (_uncompressProperties.TryGetValue(propertyAlias, out var map2)) { - return map; + return map2; } - return DEFAULT_MAP; + return DefaultMap; } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index 31421e7177..133586382c 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -69,7 +69,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { foreach (var map in _propertyOptions.PropertyMap) { - if (map.Value.compress.Equals(NucachePropertyCompressionLevel.SQLDatabase)) + if (map.Value.CompressLevel.Equals(NucachePropertyCompressionLevel.SQLDatabase)) { if (nestedData.PropertyData.TryGetValue(map.Key, out PropertyData[] properties)) { @@ -79,11 +79,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } } } - if (map.Value.mappedAlias != null && !map.Key.Equals(map.Value.mappedAlias) + if (map.Value.MappedAlias != null && !map.Key.Equals(map.Value.MappedAlias) && nestedData.PropertyData.Remove(map.Key) && nestedData.PropertyData.TryGetValue(map.Key, out PropertyData[] properties2)) { nestedData.PropertyData.Remove(map.Key); - nestedData.PropertyData.Add(map.Value.mappedAlias, properties2); + nestedData.PropertyData.Add(map.Value.MappedAlias, properties2); } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCacheCompressionOptions.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCacheCompressionOptions.cs new file mode 100644 index 0000000000..64f1b675bc --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCacheCompressionOptions.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + public struct NuCacheCompressionOptions : IEquatable + { + public NuCacheCompressionOptions(NucachePropertyCompressionLevel compressLevel, NucachePropertyDecompressionLevel decompressLevel, string mappedAlias) + { + CompressLevel = compressLevel; + DecompressLevel = decompressLevel; + MappedAlias = mappedAlias ?? throw new ArgumentNullException(nameof(mappedAlias)); + } + + public NucachePropertyCompressionLevel CompressLevel { get; private set; } + public NucachePropertyDecompressionLevel DecompressLevel { get; private set; } + public string MappedAlias { get; private set; } + + public override bool Equals(object obj) + { + return obj is NuCacheCompressionOptions options && Equals(options); + } + + public bool Equals(NuCacheCompressionOptions other) + { + return CompressLevel == other.CompressLevel && + DecompressLevel == other.DecompressLevel && + MappedAlias == other.MappedAlias; + } + + public override int GetHashCode() + { + var hashCode = 961370163; + hashCode = hashCode * -1521134295 + CompressLevel.GetHashCode(); + hashCode = hashCode * -1521134295 + DecompressLevel.GetHashCode(); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(MappedAlias); + return hashCode; + } + + public static bool operator ==(NuCacheCompressionOptions left, NuCacheCompressionOptions right) + { + return left.Equals(right); + } + + public static bool operator !=(NuCacheCompressionOptions left, NuCacheCompressionOptions right) + { + return !(left == right); + } + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyOptions.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyOptions.cs index be0118f563..f88f30ccd8 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyOptions.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyOptions.cs @@ -1,17 +1,14 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { + public class NuCachePropertyOptions { - public IReadOnlyDictionary PropertyMap - { get; set; } = new Dictionary(); + public IReadOnlyDictionary PropertyMap { get; set; } = new Dictionary(); public K4os.Compression.LZ4.LZ4Level LZ4CompressionLevel { get; set; } = K4os.Compression.LZ4.LZ4Level.L00_FAST; diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index a4e5640d36..18c4a8ed93 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -259,6 +259,7 @@ + From cb691926787c57fc87d009f120409248d3525921 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Thu, 13 Aug 2020 23:11:03 +1200 Subject: [PATCH 021/289] Support include in sql server indexes --- .../DatabaseAnnotations/IndexAttribute.cs | 5 +++++ .../DefinitionFactory.cs | 8 ++++++++ .../IndexDefinition.cs | 1 + .../SqlSyntax/SqlServerSyntaxProvider.cs | 19 +++++++++++++++++++ 4 files changed, 33 insertions(+) diff --git a/src/Umbraco.Core/Persistence/DatabaseAnnotations/IndexAttribute.cs b/src/Umbraco.Core/Persistence/DatabaseAnnotations/IndexAttribute.cs index 138dceff09..5aaabbfa6f 100644 --- a/src/Umbraco.Core/Persistence/DatabaseAnnotations/IndexAttribute.cs +++ b/src/Umbraco.Core/Persistence/DatabaseAnnotations/IndexAttribute.cs @@ -31,5 +31,10 @@ namespace Umbraco.Core.Persistence.DatabaseAnnotations /// Gets or sets the column name(s) for the current index /// public string ForColumns { get; set; } + + /// + /// Gets or sets the column name(s) for the columns to include in the index + /// + public string IncludeColumns { get; set; } } } diff --git a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs index 5925e58afc..129b200c17 100644 --- a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs +++ b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs @@ -166,6 +166,14 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions definition.Columns.Add(new IndexColumnDefinition {Name = column, Direction = Direction.Ascending}); } } + if (string.IsNullOrEmpty(attribute.IncludeColumns) == false) + { + var columns = attribute.IncludeColumns.Split(',').Select(p => p.Trim()); + foreach (var column in columns) + { + definition.IncludeColumns.Add(new IndexColumnDefinition { Name = column, Direction = Direction.Ascending }); + } + } return definition; } } diff --git a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs index 582f9a40f7..8efb795948 100644 --- a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs +++ b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs @@ -17,6 +17,7 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions public virtual string ColumnName { get; set; } public virtual ICollection Columns { get; set; } + public virtual ICollection IncludeColumns { get; set; } public IndexTypes IndexType { get; set; } } } diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index bb50fa98a1..6c53a94f90 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -336,5 +336,24 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) public override string DropIndex => "DROP INDEX {0} ON {1}"; public override string RenameColumn => "sp_rename '{0}.{1}', '{2}', 'COLUMN'"; + + public override string CreateIndex => "CREATE {0}{1}INDEX {2} ON {3} ({4}){5}"; + public override string Format(IndexDefinition index) + { + var name = string.IsNullOrEmpty(index.Name) + ? $"IX_{index.TableName}_{index.ColumnName}" + : index.Name; + + var columns = index.Columns.Any() + ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) + : GetQuotedColumnName(index.ColumnName); + + var includeColumns = index.IncludeColumns.Any() + ? " INCLUDE " + string.Join(",", index.IncludeColumns.Select(x => GetQuotedColumnName(x.Name))) + : GetQuotedColumnName(index.ColumnName); + + return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), + GetQuotedTableName(index.TableName), columns, includeColumns); + } } } From 07de0876e776bca8ac1b43a46d3ea662d10125c2 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 13 Aug 2020 22:15:09 +1000 Subject: [PATCH 022/289] Cleans up some code, adds code comments --- .../DataSource/BTree.ContentDataSerializer.cs | 15 +-- ....DictionaryOfCultureVariationSerializer.cs | 3 + ...Tree.DictionaryOfPropertyDataSerializer.cs | 3 + .../NuCache/DataSource/ContentData.cs | 4 +- .../IContentNestedDataSerializer.cs | 18 ++-- .../DataSource/LazyCompressedString.cs | 2 + .../Lz4DictionaryOfPropertyDataSerializer.cs | 102 +++++++++--------- .../MsgPackContentNestedDataSerializer.cs | 12 ++- .../DataSource/NuCacheCompressionOptions.cs | 7 ++ .../NucachePropertyCompressionLevel.cs | 23 ++++ .../NucachePropertyDecompressionLevel.cs | 12 +++ src/Umbraco.Web/Umbraco.Web.csproj | 2 + 12 files changed, 135 insertions(+), 68 deletions(-) create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyCompressionLevel.cs create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyDecompressionLevel.cs diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.ContentDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.ContentDataSerializer.cs index 9cc5d3a701..1192c892d4 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.ContentDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.ContentDataSerializer.cs @@ -3,18 +3,21 @@ using CSharpTest.Net.Serialization; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { - class ContentDataSerializer : ISerializer + /// + /// Serializes/Deserializes data to BTree data source for + /// + internal class ContentDataSerializer : ISerializer { public ContentDataSerializer(IDictionaryOfPropertyDataSerializer dictionaryOfPropertyDataSerializer = null) { _dictionaryOfPropertyDataSerializer = dictionaryOfPropertyDataSerializer; if(_dictionaryOfPropertyDataSerializer == null) { - _dictionaryOfPropertyDataSerializer = PropertiesSerializer; + _dictionaryOfPropertyDataSerializer = DefaultPropertiesSerializer; } } - private static readonly DictionaryOfPropertyDataSerializer PropertiesSerializer = new DictionaryOfPropertyDataSerializer(); - private static readonly DictionaryOfCultureVariationSerializer CultureVariationsSerializer = new DictionaryOfCultureVariationSerializer(); + private static readonly DictionaryOfPropertyDataSerializer DefaultPropertiesSerializer = new DictionaryOfPropertyDataSerializer(); + private static readonly DictionaryOfCultureVariationSerializer DefaultCultureVariationsSerializer = new DictionaryOfCultureVariationSerializer(); private readonly IDictionaryOfPropertyDataSerializer _dictionaryOfPropertyDataSerializer; public ContentData ReadFrom(Stream stream) @@ -29,7 +32,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource WriterId = PrimitiveSerializer.Int32.ReadFrom(stream), TemplateId = PrimitiveSerializer.Int32.ReadFrom(stream), Properties = _dictionaryOfPropertyDataSerializer.ReadFrom(stream), // TODO: We don't want to allocate empty arrays - CultureInfos = CultureVariationsSerializer.ReadFrom(stream) // TODO: We don't want to allocate empty arrays + CultureInfos = DefaultCultureVariationsSerializer.ReadFrom(stream) // TODO: We don't want to allocate empty arrays }; } @@ -46,7 +49,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource PrimitiveSerializer.Int32.WriteTo(value.TemplateId.Value, stream); } _dictionaryOfPropertyDataSerializer.WriteTo(value.Properties, stream); - CultureVariationsSerializer.WriteTo(value.CultureInfos, stream); + DefaultCultureVariationsSerializer.WriteTo(value.CultureInfos, stream); } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs index 1fcbdbdae7..b3a6ef2d05 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs @@ -6,6 +6,9 @@ using Umbraco.Core; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { + /// + /// Serializes/Deserializes culture variant data as a dictionary for BTree + /// internal class DictionaryOfCultureVariationSerializer : SerializerBase, ISerializer> { public IReadOnlyDictionary ReadFrom(Stream stream) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs index cb12164397..0b15c0ba4b 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs @@ -6,6 +6,9 @@ using Umbraco.Core; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { + /// + /// Serializes/Deserializes property data as a dictionary for BTree + /// internal class DictionaryOfPropertyDataSerializer : SerializerBase, ISerializer>, IDictionaryOfPropertyDataSerializer { public IDictionary ReadFrom(Stream stream) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentData.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentData.cs index 36586acd12..5f252591e0 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentData.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentData.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { - // represents everything that is specific to edited or published version + /// + /// Represents everything that is specific to an edited or published content version + /// internal class ContentData { public string Name { get; set; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs index edd061fbe4..32eb388bee 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs @@ -1,19 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.Web.PublishedCache.NuCache.DataSource +namespace Umbraco.Web.PublishedCache.NuCache.DataSource { - // TODO: We need a better name, not sure why the class is called ContentNested in the first place + // TODO: We need better names if possible, not sure why the class is called ContentNested in the first place + + /// + /// Serializes/Deserializes document to the SQL Database as bytes + /// public interface IContentNestedDataByteSerializer : IContentNestedDataSerializer { ContentNestedData DeserializeBytes(byte[] data); byte[] SerializeBytes(ContentNestedData nestedData); } - // TODO: We need a better name, not sure why the class is called ContentNested in the first place + /// + /// Serializes/Deserializes document to the SQL Database as a string + /// public interface IContentNestedDataSerializer { ContentNestedData Deserialize(string data); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs index 3d6e70c7b2..55469ad791 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs @@ -13,6 +13,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// internal class LazyCompressedString { + // TODO: This could be a struct + private byte[] _bytes; private string _str; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs index c62af1293f..31f7763e04 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs @@ -9,26 +9,10 @@ using K4os.Compression.LZ4; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { - /// - /// If/where to compress custom properties for nucache - /// - public enum NucachePropertyCompressionLevel - { - None = 0, - SQLDatabase = 1, - NuCacheDatabase = 2 - } - /// - /// If/where to decompress custom properties for nucache - /// - public enum NucachePropertyDecompressionLevel - { - NotCompressed = 0, - Immediate = 1, - Lazy = 2 - } - + /// + /// Serializes/Deserializes property data as a dictionary for BTree with Lz4 compression options + /// internal class Lz4DictionaryOfPropertyDataSerializer : SerializerBase, ISerializer>, IDictionaryOfPropertyDataSerializer { private readonly IReadOnlyDictionary _compressProperties; @@ -58,7 +42,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // read property alias var alias = PrimitiveSerializer.String.ReadFrom(stream); - var map = GetDeSerializationMap(alias); + var map = GetDeserializationMap(alias); var key = string.Intern(map.MappedAlias ?? alias); // read values count @@ -82,23 +66,30 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource pdata.Segment = ReadStringObject(stream, true) ?? string.Empty; pdata.Value = ReadObject(stream); - if ((map.CompressLevel.Equals(NucachePropertyCompressionLevel.NuCacheDatabase) || map.CompressLevel.Equals(NucachePropertyCompressionLevel.SQLDatabase)) - && pdata.Value != null && pdata.Value is byte[] byteArrayValue) - { - //Compressed string - switch (map.DecompressLevel) - { - case NucachePropertyDecompressionLevel.Lazy: - pdata.Value = new LazyCompressedString(byteArrayValue); - break; - case NucachePropertyDecompressionLevel.NotCompressed: - break;//Shouldn't be any not compressed - case NucachePropertyDecompressionLevel.Immediate: - default: - pdata.Value = Encoding.UTF8.GetString(LZ4Pickler.Unpickle(byteArrayValue)); - break; - } - } + switch (map.CompressLevel) + { + case NucachePropertyCompressionLevel.SQLDatabase: + case NucachePropertyCompressionLevel.NuCacheDatabase: + if (!(pdata.Value is null) && pdata.Value is byte[] byteArrayValue) + { + //Compressed string + switch (map.DecompressLevel) + { + case NucachePropertyDecompressionLevel.Lazy: + pdata.Value = new LazyCompressedString(byteArrayValue); + break; + case NucachePropertyDecompressionLevel.NotCompressed: + //Shouldn't be any not compressed + // TODO: Do we need to throw here? + break; + case NucachePropertyDecompressionLevel.Immediate: + default: + pdata.Value = Encoding.UTF8.GetString(LZ4Pickler.Unpickle(byteArrayValue)); + break; + } + } + break; + } } dict[key] = pdatas.ToArray(); @@ -131,17 +122,30 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource WriteObject(pdata.Culture ?? string.Empty, stream); WriteObject(pdata.Segment ?? string.Empty, stream); - //Only compress strings - if (pdata.Value is string stringValue && pdata.Value != null && map.CompressLevel.Equals(NucachePropertyCompressionLevel.NuCacheDatabase) - && (_nucachePropertyOptions.MinimumCompressibleStringLength == null - || !_nucachePropertyOptions.MinimumCompressibleStringLength.HasValue - || stringValue.Length > _nucachePropertyOptions.MinimumCompressibleStringLength.Value)) + //Only compress strings + switch (map.CompressLevel) { - var stringBytes = Encoding.UTF8.GetBytes(stringValue); - var compressedBytes = LZ4Pickler.Pickle(stringBytes, _nucachePropertyOptions.LZ4CompressionLevel); - WriteObject(compressedBytes, stream); + // If we're compressing into btree at the property level + case NucachePropertyCompressionLevel.NuCacheDatabase: + + if (pdata.Value is string stringValue && !(pdata.Value is null) + && (_nucachePropertyOptions.MinimumCompressibleStringLength is null + || !_nucachePropertyOptions.MinimumCompressibleStringLength.HasValue + || stringValue.Length > _nucachePropertyOptions.MinimumCompressibleStringLength.Value)) + { + var stringBytes = Encoding.UTF8.GetBytes(stringValue); + var compressedBytes = LZ4Pickler.Pickle(stringBytes, _nucachePropertyOptions.LZ4CompressionLevel); + WriteObject(compressedBytes, stream); + } + else + { + WriteObject(pdata.Value, stream); + } + break; + default: + WriteObject(pdata.Value, stream); + break; } - WriteObject(pdata.Value, stream); } } } @@ -150,7 +154,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public NuCacheCompressionOptions GetSerializationMap(string propertyAlias) { - if (_compressProperties == null) + if (_compressProperties is null) { return DefaultMap; } @@ -161,9 +165,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return DefaultMap; } - public NuCacheCompressionOptions GetDeSerializationMap(string propertyAlias) + public NuCacheCompressionOptions GetDeserializationMap(string propertyAlias) { - if (_uncompressProperties == null) + if (_uncompressProperties is null) { return DefaultMap; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index 133586382c..206051f839 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -9,6 +9,9 @@ using System.Text; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { + /// + /// Serializes/Deserializes document to the SQL Database as bytes using MessagePack + /// internal class MsgPackContentNestedDataSerializer : IContentNestedDataByteSerializer { private MessagePackSerializerOptions _options; @@ -65,7 +68,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// private void Optimize(ContentNestedData nestedData) { - if (_propertyOptions.PropertyMap != null && _propertyOptions.PropertyMap.Any()) + if (_propertyOptions.PropertyMap != null && _propertyOptions.PropertyMap.Count > 0) { foreach (var map in _propertyOptions.PropertyMap) { @@ -79,10 +82,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } } } + + // if there is an alias map for this property then use that instead of the real property alias + // (used to save memory, the mapped alias is normally a single char or at least a smaller string) if (map.Value.MappedAlias != null && !map.Key.Equals(map.Value.MappedAlias) - && nestedData.PropertyData.Remove(map.Key) && nestedData.PropertyData.TryGetValue(map.Key, out PropertyData[] properties2)) + && nestedData.PropertyData.Remove(map.Key) + && nestedData.PropertyData.TryGetValue(map.Key, out PropertyData[] properties2)) { - nestedData.PropertyData.Remove(map.Key); nestedData.PropertyData.Add(map.Value.MappedAlias, properties2); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCacheCompressionOptions.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCacheCompressionOptions.cs index 64f1b675bc..36f1606008 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCacheCompressionOptions.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCacheCompressionOptions.cs @@ -14,6 +14,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public NucachePropertyCompressionLevel CompressLevel { get; private set; } public NucachePropertyDecompressionLevel DecompressLevel { get; private set; } + + /// + /// Used to map a real property alias to a shorter moniker in memory + /// + /// + /// This is simply a memory saving mechanism + /// public string MappedAlias { get; private set; } public override bool Equals(object obj) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyCompressionLevel.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyCompressionLevel.cs new file mode 100644 index 0000000000..2f24a203ca --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyCompressionLevel.cs @@ -0,0 +1,23 @@ +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + /// + /// If/where to compress custom properties for nucache + /// + public enum NucachePropertyCompressionLevel + { + None = 0, + + /// + /// Compress property data at the nucache SQL DB table level + /// + /// + /// Only necessary if the document in the nucache SQL DB table isn't stored as compressed bytes + /// + SQLDatabase = 1, + + /// + /// Compress property data at the nucache BTree level + /// + NuCacheDatabase = 2 + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyDecompressionLevel.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyDecompressionLevel.cs new file mode 100644 index 0000000000..335ae6ff4a --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyDecompressionLevel.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + /// + /// If/where to decompress custom properties for nucache + /// + public enum NucachePropertyDecompressionLevel + { + NotCompressed = 0, + Immediate = 1, + Lazy = 2 + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 18c4a8ed93..5715a5e060 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -260,6 +260,8 @@ + + From 044585a4f3a0e77a3a73b2bd45e06abf2b0d2db3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 13 Aug 2020 22:22:06 +1000 Subject: [PATCH 023/289] removes the SQLDatabase enum value --- .../IContentNestedDataSerializer.cs | 2 +- .../Lz4DictionaryOfPropertyDataSerializer.cs | 1 - .../MsgPackContentNestedDataSerializer.cs | 50 ++----------------- .../NucachePropertyCompressionLevel.cs | 8 --- 4 files changed, 5 insertions(+), 56 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs index 32eb388bee..8b1b3e1496 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs @@ -5,7 +5,7 @@ /// /// Serializes/Deserializes document to the SQL Database as bytes /// - public interface IContentNestedDataByteSerializer : IContentNestedDataSerializer + public interface IContentNestedDataByteSerializer { ContentNestedData DeserializeBytes(byte[] data); byte[] SerializeBytes(ContentNestedData nestedData); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs index 31f7763e04..d71f0e2184 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs @@ -68,7 +68,6 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource switch (map.CompressLevel) { - case NucachePropertyCompressionLevel.SQLDatabase: case NucachePropertyCompressionLevel.NuCacheDatabase: if (!(pdata.Value is null) && pdata.Value is byte[] byteArrayValue) { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index 206051f839..33181ebd5e 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -1,23 +1,17 @@ -using K4os.Compression.LZ4; -using MessagePack; -using MessagePack.Formatters; +using MessagePack; using MessagePack.Resolvers; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { /// /// Serializes/Deserializes document to the SQL Database as bytes using MessagePack /// - internal class MsgPackContentNestedDataSerializer : IContentNestedDataByteSerializer + internal class MsgPackContentNestedDataSerializer : IContentNestedDataByteSerializer, IContentNestedDataSerializer { - private MessagePackSerializerOptions _options; - private readonly NuCachePropertyOptions _propertyOptions; + private readonly MessagePackSerializerOptions _options; - public MsgPackContentNestedDataSerializer(INuCachePropertyOptionsFactory propertyOptionsFactory = null) + public MsgPackContentNestedDataSerializer() { var defaultOptions = ContractlessStandardResolver.Options; @@ -37,7 +31,6 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource _options = defaultOptions .WithResolver(resolver) .WithCompression(MessagePackCompression.Lz4BlockArray); - _propertyOptions = propertyOptionsFactory?.GetNuCachePropertyOptions() ?? new NuCachePropertyOptions(); } public string ToJson(string serialized) @@ -56,45 +49,10 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public string Serialize(ContentNestedData nestedData) { - Optimize(nestedData); - var bin = MessagePackSerializer.Serialize(nestedData, _options); return Convert.ToBase64String(bin); } - /// - /// Compress properties and map property names to shorter names - /// - /// - private void Optimize(ContentNestedData nestedData) - { - if (_propertyOptions.PropertyMap != null && _propertyOptions.PropertyMap.Count > 0) - { - foreach (var map in _propertyOptions.PropertyMap) - { - if (map.Value.CompressLevel.Equals(NucachePropertyCompressionLevel.SQLDatabase)) - { - if (nestedData.PropertyData.TryGetValue(map.Key, out PropertyData[] properties)) - { - foreach (var property in properties.Where(x => x.Value != null && x.Value is string)) - { - property.Value = LZ4Pickler.Pickle(Encoding.UTF8.GetBytes(property.Value as string), _propertyOptions.LZ4CompressionLevel); - } - } - } - - // if there is an alias map for this property then use that instead of the real property alias - // (used to save memory, the mapped alias is normally a single char or at least a smaller string) - if (map.Value.MappedAlias != null && !map.Key.Equals(map.Value.MappedAlias) - && nestedData.PropertyData.Remove(map.Key) - && nestedData.PropertyData.TryGetValue(map.Key, out PropertyData[] properties2)) - { - nestedData.PropertyData.Add(map.Value.MappedAlias, properties2); - } - } - } - } - public ContentNestedData DeserializeBytes(byte[] data) => MessagePackSerializer.Deserialize(data, _options); public byte[] SerializeBytes(ContentNestedData nestedData) => MessagePackSerializer.Serialize(nestedData, _options); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyCompressionLevel.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyCompressionLevel.cs index 2f24a203ca..8a82dcb888 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyCompressionLevel.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyCompressionLevel.cs @@ -7,14 +7,6 @@ { None = 0, - /// - /// Compress property data at the nucache SQL DB table level - /// - /// - /// Only necessary if the document in the nucache SQL DB table isn't stored as compressed bytes - /// - SQLDatabase = 1, - /// /// Compress property data at the nucache BTree level /// From 9ac2c271105fffc2de0046f42a233073dfc2184b Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 13 Aug 2020 23:09:40 +1000 Subject: [PATCH 024/289] Revert "removes the SQLDatabase enum value" and renames some stuff --- .../AppSettingsNucachePropertyMapFactory.cs | 13 +++-- .../IContentNestedDataSerializer.cs | 2 +- .../INucachePropertyOptionsFactory.cs | 2 +- .../Lz4DictionaryOfPropertyDataSerializer.cs | 3 +- .../MsgPackContentNestedDataSerializer.cs | 50 +++++++++++++++++-- .../NuCachePropertyCompressionOptions.cs | 32 ++++++++++++ .../NucachePropertyCompressionLevel.cs | 8 +++ .../DataSource/NucachePropertyOptions.cs | 17 ------- src/Umbraco.Web/Umbraco.Web.csproj | 2 +- 9 files changed, 97 insertions(+), 32 deletions(-) create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCachePropertyCompressionOptions.cs delete mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyOptions.cs diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs index 271695d250..544231bd32 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs @@ -10,14 +10,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // TODO: We'll remove this when the responsibility for compressing property data is at the property editor level internal class AppSettingsNuCachePropertyMapFactory : INuCachePropertyOptionsFactory { - public NuCachePropertyOptions GetNuCachePropertyOptions() + public NuCachePropertyCompressionOptions GetNuCachePropertyOptions() { - NuCachePropertyOptions options = new NuCachePropertyOptions - { - PropertyMap = GetPropertyMap(), - LZ4CompressionLevel = K4os.Compression.LZ4.LZ4Level.L10_OPT, - MinimumCompressibleStringLength = null - }; + var options = new NuCachePropertyCompressionOptions( + GetPropertyMap(), + K4os.Compression.LZ4.LZ4Level.L10_OPT, + null); + return options; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs index 8b1b3e1496..32eb388bee 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs @@ -5,7 +5,7 @@ /// /// Serializes/Deserializes document to the SQL Database as bytes /// - public interface IContentNestedDataByteSerializer + public interface IContentNestedDataByteSerializer : IContentNestedDataSerializer { ContentNestedData DeserializeBytes(byte[] data); byte[] SerializeBytes(ContentNestedData nestedData); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/INucachePropertyOptionsFactory.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/INucachePropertyOptionsFactory.cs index 0cb694e1c1..d423499744 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/INucachePropertyOptionsFactory.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/INucachePropertyOptionsFactory.cs @@ -8,6 +8,6 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { public interface INuCachePropertyOptionsFactory { - NuCachePropertyOptions GetNuCachePropertyOptions(); + NuCachePropertyCompressionOptions GetNuCachePropertyOptions(); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs index d71f0e2184..752cedc18b 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs @@ -68,6 +68,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource switch (map.CompressLevel) { + case NucachePropertyCompressionLevel.SQLDatabase: case NucachePropertyCompressionLevel.NuCacheDatabase: if (!(pdata.Value is null) && pdata.Value is byte[] byteArrayValue) { @@ -149,7 +150,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } } private static readonly NuCacheCompressionOptions DefaultMap = new NuCacheCompressionOptions(NucachePropertyCompressionLevel.None, NucachePropertyDecompressionLevel.NotCompressed, null); - private readonly NuCachePropertyOptions _nucachePropertyOptions; + private readonly NuCachePropertyCompressionOptions _nucachePropertyOptions; public NuCacheCompressionOptions GetSerializationMap(string propertyAlias) { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index 33181ebd5e..24e2bc7b27 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -1,17 +1,23 @@ -using MessagePack; +using K4os.Compression.LZ4; +using MessagePack; +using MessagePack.Formatters; using MessagePack.Resolvers; using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { /// /// Serializes/Deserializes document to the SQL Database as bytes using MessagePack /// - internal class MsgPackContentNestedDataSerializer : IContentNestedDataByteSerializer, IContentNestedDataSerializer + internal class MsgPackContentNestedDataSerializer : IContentNestedDataByteSerializer { - private readonly MessagePackSerializerOptions _options; + private MessagePackSerializerOptions _options; + private readonly NuCachePropertyCompressionOptions _propertyOptions; - public MsgPackContentNestedDataSerializer() + public MsgPackContentNestedDataSerializer(INuCachePropertyOptionsFactory propertyOptionsFactory = null) { var defaultOptions = ContractlessStandardResolver.Options; @@ -31,6 +37,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource _options = defaultOptions .WithResolver(resolver) .WithCompression(MessagePackCompression.Lz4BlockArray); + _propertyOptions = propertyOptionsFactory?.GetNuCachePropertyOptions() ?? NuCachePropertyCompressionOptions.Empty; } public string ToJson(string serialized) @@ -49,10 +56,45 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public string Serialize(ContentNestedData nestedData) { + Optimize(nestedData); + var bin = MessagePackSerializer.Serialize(nestedData, _options); return Convert.ToBase64String(bin); } + /// + /// Compress properties and map property names to shorter names + /// + /// + private void Optimize(ContentNestedData nestedData) + { + if (_propertyOptions.PropertyMap != null && _propertyOptions.PropertyMap.Count > 0) + { + foreach (var map in _propertyOptions.PropertyMap) + { + if (map.Value.CompressLevel.Equals(NucachePropertyCompressionLevel.SQLDatabase)) + { + if (nestedData.PropertyData.TryGetValue(map.Key, out PropertyData[] properties)) + { + foreach (var property in properties.Where(x => x.Value != null && x.Value is string)) + { + property.Value = LZ4Pickler.Pickle(Encoding.UTF8.GetBytes(property.Value as string), _propertyOptions.LZ4CompressionLevel); + } + } + } + + // if there is an alias map for this property then use that instead of the real property alias + // (used to save memory, the mapped alias is normally a single char or at least a smaller string) + if (map.Value.MappedAlias != null && !map.Key.Equals(map.Value.MappedAlias) + && nestedData.PropertyData.Remove(map.Key) + && nestedData.PropertyData.TryGetValue(map.Key, out PropertyData[] properties2)) + { + nestedData.PropertyData.Add(map.Value.MappedAlias, properties2); + } + } + } + } + public ContentNestedData DeserializeBytes(byte[] data) => MessagePackSerializer.Deserialize(data, _options); public byte[] SerializeBytes(ContentNestedData nestedData) => MessagePackSerializer.Serialize(nestedData, _options); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCachePropertyCompressionOptions.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCachePropertyCompressionOptions.cs new file mode 100644 index 0000000000..50fb20cadc --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCachePropertyCompressionOptions.cs @@ -0,0 +1,32 @@ +using K4os.Compression.LZ4; +using System; +using System.Collections.Generic; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + + public class NuCachePropertyCompressionOptions + { + /// + /// Returns empty options + /// + public static NuCachePropertyCompressionOptions Empty { get; } = new NuCachePropertyCompressionOptions(); + + private NuCachePropertyCompressionOptions() + { + } + + public NuCachePropertyCompressionOptions(IReadOnlyDictionary propertyMap, LZ4Level lZ4CompressionLevel, long? minimumCompressibleStringLength) + { + PropertyMap = propertyMap ?? throw new ArgumentNullException(nameof(propertyMap)); + LZ4CompressionLevel = lZ4CompressionLevel; + MinimumCompressibleStringLength = minimumCompressibleStringLength; + } + + public IReadOnlyDictionary PropertyMap { get; } = new Dictionary(); + + public LZ4Level LZ4CompressionLevel { get; } = LZ4Level.L00_FAST; + + public long? MinimumCompressibleStringLength { get; } + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyCompressionLevel.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyCompressionLevel.cs index 8a82dcb888..2f24a203ca 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyCompressionLevel.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyCompressionLevel.cs @@ -7,6 +7,14 @@ { None = 0, + /// + /// Compress property data at the nucache SQL DB table level + /// + /// + /// Only necessary if the document in the nucache SQL DB table isn't stored as compressed bytes + /// + SQLDatabase = 1, + /// /// Compress property data at the nucache BTree level /// diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyOptions.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyOptions.cs deleted file mode 100644 index f88f30ccd8..0000000000 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyOptions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.Web.PublishedCache.NuCache.DataSource -{ - - public class NuCachePropertyOptions - { - public IReadOnlyDictionary PropertyMap { get; set; } = new Dictionary(); - - public K4os.Compression.LZ4.LZ4Level LZ4CompressionLevel { get; set; } = K4os.Compression.LZ4.LZ4Level.L00_FAST; - - public long? MinimumCompressibleStringLength { get; set; } - } -} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 5715a5e060..fd54f550da 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -262,7 +262,7 @@ - + From 7d689a6e1133628b700ee9b9ee5432e75542c9a8 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 13 Aug 2020 23:32:05 +1000 Subject: [PATCH 025/289] adds notes, ensures that we optimize property data when using msgpack binary serialization too! --- .../DataSource/Lz4DictionaryOfPropertyDataSerializer.cs | 6 +++--- .../DataSource/MsgPackContentNestedDataSerializer.cs | 7 ++++++- .../PublishedCache/NuCache/DataSource/SerializerBase.cs | 4 ++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs index 752cedc18b..b3d7f7dcbc 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs @@ -67,7 +67,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource pdata.Value = ReadObject(stream); switch (map.CompressLevel) - { + { + // If the property is compressed at either the DB or Nucache level, it means it's compressed here and we need to decompress case NucachePropertyCompressionLevel.SQLDatabase: case NucachePropertyCompressionLevel.NuCacheDatabase: if (!(pdata.Value is null) && pdata.Value is byte[] byteArrayValue) @@ -80,8 +81,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource break; case NucachePropertyDecompressionLevel.NotCompressed: //Shouldn't be any not compressed - // TODO: Do we need to throw here? - break; + throw new InvalidOperationException($"{NucachePropertyDecompressionLevel.NotCompressed} cannot be a decompression option for property {alias} since it's compresion option is {map.CompressLevel}"); case NucachePropertyDecompressionLevel.Immediate: default: pdata.Value = Encoding.UTF8.GetString(LZ4Pickler.Unpickle(byteArrayValue)); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index 24e2bc7b27..09ca1278b4 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -97,7 +97,12 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public ContentNestedData DeserializeBytes(byte[] data) => MessagePackSerializer.Deserialize(data, _options); - public byte[] SerializeBytes(ContentNestedData nestedData) => MessagePackSerializer.Serialize(nestedData, _options); + public byte[] SerializeBytes(ContentNestedData nestedData) + { + Optimize(nestedData); + + return MessagePackSerializer.Serialize(nestedData, _options); + } //private class ContentNestedDataResolver : IFormatterResolver //{ diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs index 885b5cf80e..4a11a8f0e5 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs @@ -16,6 +16,10 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource private const char PrefixDouble = 'B'; private const char PrefixDateTime = 'D'; private const char PrefixByte = 'O'; + + // TODO: It might make sense to have another prefix for an LZ4 compressed byte array. + // Would be an improvement for the SQLDatabase compression option because then you could mix compressed and decompressed properties with the same alias. + // For example, don't compress recent content, but compress older content. private const char PrefixByteArray = 'A'; protected string ReadString(Stream stream) => PrimitiveSerializer.String.ReadFrom(stream); From c619e5a96a1011c5c78a51ba9749297259dabead Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 14 Aug 2020 00:45:58 +1000 Subject: [PATCH 026/289] Fixes the property map, adds notes, updates tests data to include larger paragraphs --- src/Umbraco.TestData/UmbracoTestDataController.cs | 6 ++++-- .../DataSource/MsgPackContentNestedDataSerializer.cs | 2 +- .../DataSource/NucachePropertyCompressionLevel.cs | 10 +++++++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.TestData/UmbracoTestDataController.cs b/src/Umbraco.TestData/UmbracoTestDataController.cs index 02949d5345..35a578d5d9 100644 --- a/src/Umbraco.TestData/UmbracoTestDataController.cs +++ b/src/Umbraco.TestData/UmbracoTestDataController.cs @@ -208,7 +208,8 @@ namespace Umbraco.TestData var docType = GetOrCreateContentType(); var parent = Services.ContentService.Create(company, -1, docType.Alias); - parent.SetValue("review", faker.Rant.Review()); + // give it some reasonable data (100 reviews) + parent.SetValue("review", string.Join(" ", Enumerable.Range(0, 100).Select(x => faker.Rant.Review()))); parent.SetValue("desc", company); parent.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]); Services.ContentService.Save(parent); @@ -218,7 +219,8 @@ namespace Umbraco.TestData return CreateHierarchy(parent, count, depth, currParent => { var content = Services.ContentService.Create(faker.Commerce.ProductName(), currParent, docType.Alias); - content.SetValue("review", faker.Rant.Review()); + // give it some reasonable data (100 reviews) + content.SetValue("review", string.Join(" ", Enumerable.Range(0, 100).Select(x => faker.Rant.Review()))); content.SetValue("desc", string.Join(", ", Enumerable.Range(0, 5).Select(x => faker.Commerce.ProductAdjective()))); content.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index 09ca1278b4..eb17bed858 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -86,9 +86,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // if there is an alias map for this property then use that instead of the real property alias // (used to save memory, the mapped alias is normally a single char or at least a smaller string) if (map.Value.MappedAlias != null && !map.Key.Equals(map.Value.MappedAlias) - && nestedData.PropertyData.Remove(map.Key) && nestedData.PropertyData.TryGetValue(map.Key, out PropertyData[] properties2)) { + nestedData.PropertyData.Remove(map.Key); nestedData.PropertyData.Add(map.Value.MappedAlias, properties2); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyCompressionLevel.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyCompressionLevel.cs index 2f24a203ca..23826fd722 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyCompressionLevel.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyCompressionLevel.cs @@ -11,13 +11,21 @@ /// Compress property data at the nucache SQL DB table level /// /// - /// Only necessary if the document in the nucache SQL DB table isn't stored as compressed bytes + /// Idea being we only compress this once. + /// All the records in cmsContentNu need to be rebuilt when this gets enabled. + /// Good option as then we don't use up memory / cpu to compress at boot. /// SQLDatabase = 1, /// /// Compress property data at the nucache BTree level /// + /// + /// Compress the property when writing to nucache bplustree after reading from the database. + /// Idea being we compress this at rebuild / boot. + /// This option supports older items not being compressed already, at the expense of doing this compression at boot. + /// But it also means you can easily switch between None and NuCacheDatabase if performance is worse. + /// NuCacheDatabase = 2 } } From 23880b596f56257c8eb9fac63b974dce01493b61 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Fri, 14 Aug 2020 10:02:22 +1200 Subject: [PATCH 027/289] Fix formatting --- .../Persistence/SqlSyntax/SqlServerSyntaxProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index 6c53a94f90..db199ad9dc 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -349,8 +349,8 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) : GetQuotedColumnName(index.ColumnName); var includeColumns = index.IncludeColumns.Any() - ? " INCLUDE " + string.Join(",", index.IncludeColumns.Select(x => GetQuotedColumnName(x.Name))) - : GetQuotedColumnName(index.ColumnName); + ? $" INCLUDE ({string.Join(",", index.IncludeColumns.Select(x => GetQuotedColumnName(x.Name)))})" + : string.Empty; return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), GetQuotedTableName(index.TableName), columns, includeColumns); From 7379846597fe012cd43815ab825fecf8c18e235e Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Fri, 14 Aug 2020 18:03:51 +1200 Subject: [PATCH 028/289] SQL CE no support for includes. Add null check. --- .../Persistence/SqlSyntax/SqlCeSyntaxProvider.cs | 14 ++++++++++++++ .../SqlSyntax/SqlServerSyntaxProvider.cs | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs index 8a42caaa03..3ab4b884b6 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs @@ -236,6 +236,20 @@ where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault() } public override string DropIndex { get { return "DROP INDEX {1}.{0}"; } } + public override string CreateIndex => "CREATE {0}{1}INDEX {2} ON {3} ({4})"; + public override string Format(IndexDefinition index) + { + var name = string.IsNullOrEmpty(index.Name) + ? $"IX_{index.TableName}_{index.ColumnName}" + : index.Name; + var columns = index.Columns.Any() + ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) + : GetQuotedColumnName(index.ColumnName); + + + return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), + GetQuotedTableName(index.TableName), columns); + } } } diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index db199ad9dc..772d9e89c5 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -348,7 +348,7 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) : GetQuotedColumnName(index.ColumnName); - var includeColumns = index.IncludeColumns.Any() + var includeColumns = index.IncludeColumns?.Any() ?? false ? $" INCLUDE ({string.Join(",", index.IncludeColumns.Select(x => GetQuotedColumnName(x.Name)))})" : string.Empty; From 6f2dccc2e2d4278fcf748986f0f7beefffbd95a3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 25 Aug 2020 10:14:36 +1000 Subject: [PATCH 029/289] Uses a more optimized COUNT query when rebuilding the in memory cache --- src/Umbraco.Core/Constants-SqlTemplates.cs | 6 +++ .../Persistence/NPocoDatabaseExtensions.cs | 23 +++++++-- .../NuCache/DataSource/DatabaseDataSource.cs | 47 ++++++++++++++++--- 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Constants-SqlTemplates.cs b/src/Umbraco.Core/Constants-SqlTemplates.cs index 984bc495b0..6940539cb6 100644 --- a/src/Umbraco.Core/Constants-SqlTemplates.cs +++ b/src/Umbraco.Core/Constants-SqlTemplates.cs @@ -15,6 +15,12 @@ public const string GetReservedId = "Umbraco.Core.VersionableRepository.GetReservedId"; } + + internal static class NuCacheDatabaseDataSource + { + public const string ContentSourcesSelect1 = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesSelect1"; + public const string ContentSourcesCount = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesCount"; + } } } } diff --git a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions.cs b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions.cs index 152dcbe6d3..c2100d97ad 100644 --- a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions.cs +++ b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions.cs @@ -27,12 +27,13 @@ namespace Umbraco.Core.Persistence /// The number of rows to load per page /// /// + /// Specify a custom Sql command to get the total count, if null is specified than the auto-generated sql count will be used /// /// /// NPoco's normal Page returns a List{T} but sometimes we don't want all that in memory and instead want to /// iterate over each row with a reader using Query vs Fetch. /// - internal static IEnumerable QueryPaged(this IDatabase database, long pageSize, Sql sql) + internal static IEnumerable QueryPaged(this IDatabase database, long pageSize, Sql sql, Sql sqlCount) { var sqlString = sql.SQL; var sqlArgs = sql.Arguments; @@ -42,12 +43,12 @@ namespace Umbraco.Core.Persistence do { // Get the paged queries - database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlString, ref sqlArgs, out var sqlCount, out var sqlPage); + database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlString, ref sqlArgs, out var generatedSqlCount, out var sqlPage); // get the item count once if (itemCount == null) { - itemCount = database.ExecuteScalar(sqlCount, sqlArgs); + itemCount = database.ExecuteScalar(sqlCount?.SQL ?? generatedSqlCount, sqlCount?.Arguments ?? sqlArgs); } pageIndex++; @@ -60,6 +61,22 @@ namespace Umbraco.Core.Persistence } while ((pageIndex * pageSize) < itemCount); } + /// + /// Iterates over the result of a paged data set with a db reader + /// + /// + /// + /// + /// The number of rows to load per page + /// + /// + /// + /// + /// NPoco's normal Page returns a List{T} but sometimes we don't want all that in memory and instead want to + /// iterate over each row with a reader using Query vs Fetch. + /// + internal static IEnumerable QueryPaged(this IDatabase database, long pageSize, Sql sql) => database.QueryPaged(pageSize, sql, null); + // NOTE // // proper way to do it with TSQL and SQLCE diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index 19998c7956..77cb82b226 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -32,9 +32,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource private Sql ContentSourcesSelect(IScope scope, Func, Sql> joins = null) { - var sql = scope.SqlContext.Sql() - - .Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Uid"), + var sqlTemplate = scope.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.ContentSourcesSelect1, tsql => + tsql.Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Uid"), x => Alias(x.Level, "Level"), x => Alias(x.Path, "Path"), x => Alias(x.SortOrder, "SortOrder"), x => Alias(x.ParentId, "ParentId"), x => Alias(x.CreateDate, "CreateDate"), x => Alias(x.UserId, "CreatorId")) .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) @@ -52,7 +51,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .AndSelect("nuEdit", x => Alias(x.RawData, "EditDataRaw")) .AndSelect("nuPub", x => Alias(x.RawData, "PubDataRaw")) - .From(); + .From()); + + var sql = sqlTemplate.Sql(); + + // TODO: I'm unsure how we can format the below into SQL templates also because right.Current and right.Published end up being parameters if (joins != null) sql = joins(sql); @@ -74,6 +77,32 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return sql; } + /// + /// Returns a slightly more optimized query to use for the document counting when paging over the content sources + /// + /// + /// + private Sql ContentSourcesCount(IScope scope) + { + var sqlTemplate = scope.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.ContentSourcesCount, tsql => + tsql.Select(x => Alias(x.NodeId, "Id")) + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId)); + + var sql = sqlTemplate.Sql(); + + // TODO: We can't use a template with this one because of the 'right.Current' and 'right.Published' ends up being a parameter so not sure how we can do that + sql = sql + .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) + .InnerJoin().On((left, right) => left.Id == right.Id) + .LeftJoin(j => + j.InnerJoin("pdver").On((left, right) => left.Id == right.Id && right.Published, "pcver", "pdver"), "pcver") + .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcver"); + + return sql; + } + public ContentNodeKit GetContentSource(IScope scope, int id) { var sql = ContentSourcesSelect(scope) @@ -86,14 +115,20 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public IEnumerable GetAllContentSources(IScope scope) { + // Create a different query for the SQL vs the COUNT Sql since the auto-generated COUNT Sql will be inneficient var sql = ContentSourcesSelect(scope) .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + // create a more efficient COUNT query without the join on the cmsContentNu table + var sqlCountQuery = ContentSourcesCount(scope) + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed); + var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + foreach (var row in scope.Database.QueryPaged(PageSize, sql, sqlCount)) yield return CreateContentNodeKit(row); } @@ -319,6 +354,6 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return s; } - + } } From d84d305ae9abb864d8b6ab8efb928f2c458d1351 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 25 Aug 2020 10:38:49 +1000 Subject: [PATCH 030/289] Changes our Expression visitor to not output NOT for boolean values --- .../Persistence/Querying/ExpressionVisitorBase.cs | 7 +++++-- .../NuCache/DataSource/DatabaseDataSource.cs | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs index d04930fa92..4316023b03 100644 --- a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs +++ b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs @@ -328,8 +328,11 @@ namespace Umbraco.Core.Persistence.Querying { case ExpressionType.MemberAccess: // false property , i.e. x => !Trashed - SqlParameters.Add(true); - return Visited ? string.Empty : $"NOT ({o} = @{SqlParameters.Count - 1})"; + // BUT we don't want to do a NOT SQL statement since this generally results in indexes not being used + // so we want to do an == false + SqlParameters.Add(false); + return Visited ? string.Empty : $"({o} = @{SqlParameters.Count - 1})"; + //return Visited ? string.Empty : $"NOT ({o} = @{SqlParameters.Count - 1})"; default: // could be anything else, such as: x => !x.Path.StartsWith("-20") return Visited ? string.Empty : string.Concat("NOT (", o, ")"); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index 77cb82b226..3cad7171be 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -68,11 +68,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .InnerJoin().On((left, right) => left.Id == right.Id) .LeftJoin(j => - j.InnerJoin("pdver").On((left, right) => left.Id == right.Id && right.Published, "pcver", "pdver"), "pcver") + j.InnerJoin("pdver").On((left, right) => left.Id == right.Id && right.Published == true, "pcver", "pdver"), "pcver") .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcver") - .LeftJoin("nuEdit").On((left, right) => left.NodeId == right.NodeId && !right.Published, aliasRight: "nuEdit") - .LeftJoin("nuPub").On((left, right) => left.NodeId == right.NodeId && right.Published, aliasRight: "nuPub"); + .LeftJoin("nuEdit").On((left, right) => left.NodeId == right.NodeId && right.Published == false, aliasRight: "nuEdit") + .LeftJoin("nuPub").On((left, right) => left.NodeId == right.NodeId && right.Published == true, aliasRight: "nuPub"); return sql; } From 77e5a41077ea85ed1b8d9a9c2ace687275741d1e Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 25 Aug 2020 12:26:24 +1000 Subject: [PATCH 031/289] Updates indexes on umbracoNode and umbracoContentVersion which uses INCLUDE columns --- .../Migrations/Upgrade/UmbracoPlan.cs | 6 +- .../Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs | 56 +++++++++++++++++++ .../IndexDefinition.cs | 9 +-- .../Persistence/Dtos/ContentVersionDto.cs | 2 +- src/Umbraco.Core/Persistence/Dtos/NodeDto.cs | 4 +- src/Umbraco.Core/Umbraco.Core.csproj | 1 + 6 files changed, 66 insertions(+), 12 deletions(-) create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 0557312e90..a15dcfb23b 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -7,6 +7,7 @@ using Umbraco.Core.Migrations.Upgrade.V_8_0_0; using Umbraco.Core.Migrations.Upgrade.V_8_0_1; using Umbraco.Core.Migrations.Upgrade.V_8_1_0; using Umbraco.Core.Migrations.Upgrade.V_8_6_0; +using Umbraco.Core.Migrations.Upgrade.V_8_8_0; namespace Umbraco.Core.Migrations.Upgrade { @@ -194,7 +195,10 @@ namespace Umbraco.Core.Migrations.Upgrade // to 8.7.0... To("{a78e3369-8ea3-40ec-ad3f-5f76929d2b20}"); To("{8DDDCD0B-D7D5-4C97-BD6A-6B38CA65752F}"); - + + // to 8.8.0... + To("{4695D0C9-0729-4976-985B-048D503665D8}"); + //FINAL } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs new file mode 100644 index 0000000000..fb9e6e7f23 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs @@ -0,0 +1,56 @@ +using System.Linq; +using Umbraco.Core.Migrations.Expressions.Execute.Expressions; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_8_0 +{ + public class UpgradedIncludeIndexes : MigrationBase + { + public UpgradedIncludeIndexes(IMigrationContext context) + : base(context) + { + + } + + public override void Migrate() + { + // Rebuild keys and indexes for these tables, unfortunately we cannot use the Delete.KeysAndIndexes + // procedure since for some reason that tries to drop the PK and we don't want that and it would be a breaking + // change to add another parameter to that method so we'll just manually do it. + + var nodeDtoObjectTypeIndex = $"IX_{NodeDto.TableName}_ObjectType"; // this is the one we'll rebuild + // delete existing ones + DeleteIndexes($"IX_{NodeDto.TableName}_ParentId", $"IX_{NodeDto.TableName}_Trashed", nodeDtoObjectTypeIndex); + CreateIndexes(nodeDtoObjectTypeIndex); + + var contentVersionNodeIdIndex = $"IX_{ContentVersionDto.TableName}_NodeId"; + // delete existing ones + DeleteIndexes(contentVersionNodeIdIndex); + CreateIndexes(contentVersionNodeIdIndex); + } + + private void DeleteIndexes(params string[] toDelete) + { + var tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax); + + foreach (var i in toDelete) + Delete.Index(i).OnTable(tableDef.Name).Do(); + } + + private void CreateIndexes(params string[] toCreate) + { + var tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax); + + foreach(var c in toCreate) + { + // get the definition by name + var index = tableDef.Indexes.First(x => x.Name == c); + new ExecuteSqlStatementExpression(Context) { SqlStatement = Context.SqlContext.SqlSyntax.Format(index) }.Execute(); + } + + } + } +} diff --git a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs index 8efb795948..20f75d38c8 100644 --- a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs +++ b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs @@ -6,18 +6,13 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions { public class IndexDefinition { - public IndexDefinition() - { - Columns = new List(); - } - public virtual string Name { get; set; } public virtual string SchemaName { get; set; } public virtual string TableName { get; set; } public virtual string ColumnName { get; set; } - public virtual ICollection Columns { get; set; } - public virtual ICollection IncludeColumns { get; set; } + public virtual ICollection Columns { get; set; } = new List(); + public virtual ICollection IncludeColumns { get; set; } = new List(); public IndexTypes IndexType { get; set; } } } diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs index 4b203c128f..020b4a9e04 100644 --- a/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.Persistence.Dtos [Column("nodeId")] [ForeignKey(typeof(ContentDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,current")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,current", IncludeColumns = "id,versionDate,text,userId")] public int NodeId { get; set; } [Column("versionDate")] // TODO: db rename to 'updateDate' diff --git a/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs b/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs index 5800efb97a..3fd9a9f1d4 100644 --- a/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs @@ -26,7 +26,6 @@ namespace Umbraco.Core.Persistence.Dtos [Column("parentId")] [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ParentId")] public int ParentId { get; set; } [Column("level")] @@ -42,7 +41,6 @@ namespace Umbraco.Core.Persistence.Dtos [Column("trashed")] [Constraint(Default = "0")] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Trashed")] public bool Trashed { get; set; } [Column("nodeUser")] // TODO: db rename to 'createUserId' @@ -56,7 +54,7 @@ namespace Umbraco.Core.Persistence.Dtos [Column("nodeObjectType")] // TODO: db rename to 'objectType' [NullSetting(NullSetting = NullSettings.Null)] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType", ForColumns = "level,parentId,sortOrder,nodeObjectType,trashed", IncludeColumns = "nodeUser,path,uniqueId,createDate")] public Guid? NodeObjectType { get; set; } [Column("createDate")] diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 7756fd06f5..7bfbecc84d 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -133,6 +133,7 @@ + From aec828098d5e4086e1e1d54257a6801e8dd4f760 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 25 Aug 2020 12:33:47 +1000 Subject: [PATCH 032/289] adds MessagePack to nuspec --- build/NuSpecs/UmbracoCms.Web.nuspec | 1 + 1 file changed, 1 insertion(+) diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index 72619db02e..d45b65af09 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -42,6 +42,7 @@ + From a078a30990154f96d27955dc566acb32f49c5451 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 25 Aug 2020 14:19:44 +1000 Subject: [PATCH 033/289] Re-adds the indexes removed --- .../Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs | 19 +++++-------------- src/Umbraco.Core/Persistence/Dtos/NodeDto.cs | 8 ++++++-- .../PublishedCache/NuCache/NuCacheComposer.cs | 2 ++ 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs index fb9e6e7f23..50c0a48c91 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs @@ -2,8 +2,6 @@ using Umbraco.Core.Migrations.Expressions.Execute.Expressions; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; -using Umbraco.Core.Persistence.SqlSyntax; -using Umbraco.Core.PropertyEditors; namespace Umbraco.Core.Migrations.Upgrade.V_8_8_0 { @@ -16,20 +14,13 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_8_0 } public override void Migrate() - { - // Rebuild keys and indexes for these tables, unfortunately we cannot use the Delete.KeysAndIndexes - // procedure since for some reason that tries to drop the PK and we don't want that and it would be a breaking - // change to add another parameter to that method so we'll just manually do it. - - var nodeDtoObjectTypeIndex = $"IX_{NodeDto.TableName}_ObjectType"; // this is the one we'll rebuild - // delete existing ones - DeleteIndexes($"IX_{NodeDto.TableName}_ParentId", $"IX_{NodeDto.TableName}_Trashed", nodeDtoObjectTypeIndex); - CreateIndexes(nodeDtoObjectTypeIndex); + { + var nodeDtoLevelIndex = $"IX_{NodeDto.TableName}_Level"; + CreateIndexes(nodeDtoLevelIndex); // add the new definition var contentVersionNodeIdIndex = $"IX_{ContentVersionDto.TableName}_NodeId"; - // delete existing ones - DeleteIndexes(contentVersionNodeIdIndex); - CreateIndexes(contentVersionNodeIdIndex); + DeleteIndexes(contentVersionNodeIdIndex); // delete existing ones + CreateIndexes(contentVersionNodeIdIndex); // add the updated definition } private void DeleteIndexes(params string[] toDelete) diff --git a/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs b/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs index 3fd9a9f1d4..6797cd2c98 100644 --- a/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs @@ -26,9 +26,12 @@ namespace Umbraco.Core.Persistence.Dtos [Column("parentId")] [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ParentId")] public int ParentId { get; set; } + // NOTE: This index is primarily for the nucache data lookup, see https://github.com/umbraco/Umbraco-CMS/pull/8365#issuecomment-673404177 [Column("level")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Level", ForColumns = "level,parentId,sortOrder,nodeObjectType,trashed", IncludeColumns = "nodeUser,path,uniqueId,createDate")] public short Level { get; set; } [Column("path")] @@ -41,6 +44,7 @@ namespace Umbraco.Core.Persistence.Dtos [Column("trashed")] [Constraint(Default = "0")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Trashed")] public bool Trashed { get; set; } [Column("nodeUser")] // TODO: db rename to 'createUserId' @@ -53,8 +57,8 @@ namespace Umbraco.Core.Persistence.Dtos public string Text { get; set; } [Column("nodeObjectType")] // TODO: db rename to 'objectType' - [NullSetting(NullSetting = NullSettings.Null)] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType", ForColumns = "level,parentId,sortOrder,nodeObjectType,trashed", IncludeColumns = "nodeUser,path,uniqueId,createDate")] + [NullSetting(NullSetting = NullSettings.Null)] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType")] public Guid? NodeObjectType { get; set; } [Column("createDate")] diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs index 1ee6b96eb1..c098b516a0 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs @@ -16,6 +16,8 @@ namespace Umbraco.Web.PublishedCache.NuCache var serializer = ConfigurationManager.AppSettings["Umbraco.Web.PublishedCache.NuCache.Serializer"]; composition.Register(); composition.Register(); + + // TODO: Based on our findings it seems like this should not be configurable, we should just be using this because it's better if (serializer == "MsgPack") { var propertyDictionarySerializer = ConfigurationManager.AppSettings["Umbraco.Web.PublishedCache.NuCache.DictionaryOfPropertiesSerializer"]; From 68c4e4f36b1cbbb6fd5b4f190e6b9f86c77a7108 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 25 Aug 2020 14:49:13 +1000 Subject: [PATCH 034/289] ObjectType and UniqueId indexes now cover all fields since anytime these are queried we return all data. --- .../Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs | 12 ++++++++---- src/Umbraco.Core/Persistence/Dtos/NodeDto.cs | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs index 50c0a48c91..11de5bdf32 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs @@ -14,13 +14,17 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_8_0 } public override void Migrate() - { - var nodeDtoLevelIndex = $"IX_{NodeDto.TableName}_Level"; - CreateIndexes(nodeDtoLevelIndex); // add the new definition + { + var indexesToReplace = new[] { $"IX_{NodeDto.TableName}_UniqueId", $"IX_{NodeDto.TableName}_ObjectType" }; + DeleteIndexes(indexesToReplace); // delete existing ones + // add the new definitions + CreateIndexes($"IX_{NodeDto.TableName}_Level"); + CreateIndexes(indexesToReplace); + var contentVersionNodeIdIndex = $"IX_{ContentVersionDto.TableName}_NodeId"; DeleteIndexes(contentVersionNodeIdIndex); // delete existing ones - CreateIndexes(contentVersionNodeIdIndex); // add the updated definition + CreateIndexes(contentVersionNodeIdIndex); // add the updated definitions } private void DeleteIndexes(params string[] toDelete) diff --git a/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs b/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs index 6797cd2c98..62475af833 100644 --- a/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs @@ -20,7 +20,7 @@ namespace Umbraco.Core.Persistence.Dtos [Column("uniqueId")] [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_UniqueId")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_UniqueId", IncludeColumns = "parentId,level,path,sortOrder,trashed,nodeUser,text,createDate")] [Constraint(Default = SystemMethods.NewGuid)] public Guid UniqueId { get; set; } @@ -58,7 +58,7 @@ namespace Umbraco.Core.Persistence.Dtos [Column("nodeObjectType")] // TODO: db rename to 'objectType' [NullSetting(NullSetting = NullSettings.Null)] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType", IncludeColumns = "parentId,level,path,sortOrder,trashed,nodeUser,text,createDate")] public Guid? NodeObjectType { get; set; } [Column("createDate")] From 8ab5899db2c860752cc73f5fb3f1a85120988aad Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 25 Aug 2020 15:18:23 +1000 Subject: [PATCH 035/289] Adds another required DB index --- .../Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs | 13 ++++++------- .../Persistence/Dtos/ContentVersionDto.cs | 1 + 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs index 11de5bdf32..d4dd783876 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs @@ -16,15 +16,14 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_8_0 public override void Migrate() { var indexesToReplace = new[] { $"IX_{NodeDto.TableName}_UniqueId", $"IX_{NodeDto.TableName}_ObjectType" }; - DeleteIndexes(indexesToReplace); // delete existing ones - // add the new definitions - CreateIndexes($"IX_{NodeDto.TableName}_Level"); - CreateIndexes(indexesToReplace); - + DeleteIndexes(indexesToReplace); // delete existing ones + CreateIndexes(indexesToReplace); // replace + CreateIndexes($"IX_{NodeDto.TableName}_Level"); // add the new definitions var contentVersionNodeIdIndex = $"IX_{ContentVersionDto.TableName}_NodeId"; - DeleteIndexes(contentVersionNodeIdIndex); // delete existing ones - CreateIndexes(contentVersionNodeIdIndex); // add the updated definitions + DeleteIndexes(contentVersionNodeIdIndex); // delete existing ones + CreateIndexes(contentVersionNodeIdIndex); // replace + CreateIndexes($"IX_{ContentVersionDto.TableName}_Current"); // add the new definitions } private void DeleteIndexes(params string[] toDelete) diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs index 020b4a9e04..f9bf283be9 100644 --- a/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs @@ -32,6 +32,7 @@ namespace Umbraco.Core.Persistence.Dtos public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero [Column("current")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Current", IncludeColumns = "nodeId")] public bool Current { get; set; } // about current: From ba8da3850ede43d0c2706fa556f9d52aa403ff83 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 26 Aug 2020 11:43:43 +1000 Subject: [PATCH 036/289] This gets things working with all compression levels - going to cleanup/simplify --- .../DataSource/LazyCompressedString.cs | 14 ++-- .../Lz4DictionaryOfPropertyDataSerializer.cs | 6 +- .../MsgPackContentNestedDataSerializer.cs | 76 ++++++++++++++++--- .../NuCachePropertyCompressionOptions.cs | 1 + .../NucachePropertyDecompressionLevel.cs | 3 + .../NuCache/DataSource/SerializerBase.cs | 9 ++- 6 files changed, 88 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs index 55469ad791..9df40daf76 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs @@ -1,10 +1,7 @@ using K4os.Compression.LZ4; using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using System.Threading; -using System.Threading.Tasks; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { @@ -13,10 +10,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// internal class LazyCompressedString { - // TODO: This could be a struct - - private byte[] _bytes; private string _str; + private byte[] _bytes; /// /// Constructor @@ -27,6 +22,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource _bytes = bytes; } + public byte[] GetBytes() + { + if (_bytes == null) + throw new InvalidOperationException("The bytes have already been expanded"); + return _bytes; + } + public override string ToString() { return LazyInitializer.EnsureInitialized(ref _str, () => diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs index b3d7f7dcbc..1a2d26f4b1 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs @@ -88,8 +88,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource break; } } - break; - } + break; + } } dict[key] = pdatas.ToArray(); @@ -122,7 +122,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource WriteObject(pdata.Culture ?? string.Empty, stream); WriteObject(pdata.Segment ?? string.Empty, stream); - //Only compress strings + //Only compress strings (if NucachePropertyCompressionLevel.SQLDatabase has been specified then the property value will already be compressed) switch (map.CompressLevel) { // If we're compressing into btree at the property level diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index eb17bed858..0475810422 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -50,23 +50,36 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public ContentNestedData Deserialize(string data) { var bin = Convert.FromBase64String(data); - var obj = MessagePackSerializer.Deserialize(bin, _options); - return obj; + var nestedData = MessagePackSerializer.Deserialize(bin, _options); + Expand(nestedData); + return nestedData; } public string Serialize(ContentNestedData nestedData) { - Optimize(nestedData); - + Compress(nestedData); var bin = MessagePackSerializer.Serialize(nestedData, _options); return Convert.ToBase64String(bin); } + public ContentNestedData DeserializeBytes(byte[] data) + { + var nestedData = MessagePackSerializer.Deserialize(data, _options); + Expand(nestedData); + return nestedData; + } + + public byte[] SerializeBytes(ContentNestedData nestedData) + { + Compress(nestedData); + return MessagePackSerializer.Serialize(nestedData, _options); + } + /// - /// Compress properties and map property names to shorter names + /// Used during serialization to compress properties and map property names to shorter names /// /// - private void Optimize(ContentNestedData nestedData) + private void Compress(ContentNestedData nestedData) { if (_propertyOptions.PropertyMap != null && _propertyOptions.PropertyMap.Count > 0) { @@ -95,13 +108,54 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } } - public ContentNestedData DeserializeBytes(byte[] data) => MessagePackSerializer.Deserialize(data, _options); - - public byte[] SerializeBytes(ContentNestedData nestedData) + /// + /// Used during deserialization to map the property data as lazy or expand the value and re-map back to the true property aliases + /// + /// + private void Expand(ContentNestedData nestedData) { - Optimize(nestedData); + if (_propertyOptions.PropertyMap != null && _propertyOptions.PropertyMap.Count > 0) + { + foreach (var map in _propertyOptions.PropertyMap) + { + if (map.Value.CompressLevel.Equals(NucachePropertyCompressionLevel.SQLDatabase)) + { + // if there is an alias map for this property then re-map to the real property alias + if (map.Value.MappedAlias != null && !map.Key.Equals(map.Value.MappedAlias) + && nestedData.PropertyData.TryGetValue(map.Value.MappedAlias, out PropertyData[] properties2)) + { + nestedData.PropertyData.Remove(map.Value.MappedAlias); + nestedData.PropertyData.Add(map.Key, properties2); + } - return MessagePackSerializer.Serialize(nestedData, _options); + if (nestedData.PropertyData.TryGetValue(map.Key, out PropertyData[] properties)) + { + foreach (var pdata in properties) + { + if (!(pdata.Value is null) && pdata.Value is byte[] byteArrayValue) + { + //Compressed string + switch (map.Value.DecompressLevel) + { + case NucachePropertyDecompressionLevel.Lazy: + pdata.Value = new LazyCompressedString(byteArrayValue); + break; + case NucachePropertyDecompressionLevel.NotCompressed: + //Shouldn't be any not compressed + throw new InvalidOperationException($"{NucachePropertyDecompressionLevel.NotCompressed} cannot be a decompression option for property {map.Key} since it's compresion option is {map.Value.CompressLevel}"); + case NucachePropertyDecompressionLevel.Immediate: + default: + pdata.Value = Encoding.UTF8.GetString(LZ4Pickler.Unpickle(byteArrayValue)); + break; + } + } + } + } + } + + + } + } } //private class ContentNestedDataResolver : IFormatterResolver diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCachePropertyCompressionOptions.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCachePropertyCompressionOptions.cs index 50fb20cadc..55ab813783 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCachePropertyCompressionOptions.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCachePropertyCompressionOptions.cs @@ -27,6 +27,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public LZ4Level LZ4CompressionLevel { get; } = LZ4Level.L00_FAST; + // TODO: Unsure if we really want to keep this public long? MinimumCompressibleStringLength { get; } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyDecompressionLevel.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyDecompressionLevel.cs index 335ae6ff4a..f4d485be71 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyDecompressionLevel.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyDecompressionLevel.cs @@ -6,7 +6,10 @@ public enum NucachePropertyDecompressionLevel { NotCompressed = 0, + + // TODO: I'm unsure if this will ever be necessary, lazy seems good and deserialization would only occur once Immediate = 1, + Lazy = 2 } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs index 4a11a8f0e5..b90c418750 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs @@ -98,7 +98,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource case PrefixDateTime: return PrimitiveSerializer.DateTime.ReadFrom(stream); case PrefixByteArray: - return PrimitiveSerializer.Bytes.ReadFrom(stream); + // When it's a byte array always return as a LazyCompressedString + // TODO: Else we need to make a different prefix for lazy vs eager loading + return new LazyCompressedString(PrimitiveSerializer.Bytes.ReadFrom(stream)); default: throw new NotSupportedException($"Cannot deserialize unknown type '{type}'."); } @@ -160,6 +162,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource PrimitiveSerializer.Char.WriteTo(PrefixByteArray, stream); PrimitiveSerializer.Bytes.WriteTo(byteArrayValue, stream); } + else if (value is LazyCompressedString lazyCompressedString) + { + PrimitiveSerializer.Char.WriteTo(PrefixByteArray, stream); + PrimitiveSerializer.Bytes.WriteTo(lazyCompressedString.GetBytes(), stream); + } else throw new NotSupportedException("Value type " + value.GetType().FullName + " cannot be serialized."); } From 9a06b6291ae0a636108727cc4af6fe1f28009d3c Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 26 Aug 2020 15:57:13 +1000 Subject: [PATCH 037/289] Simplifies compression implementation, adds compression options to target all complex editors, ensures the options are per document type/property type --- .../ContentSerializationTests.cs | 8 +- ...ComplexEditorPropertyCompressionOptions.cs | 48 +++++ .../IPropertyCompressionOptions.cs | 10 + .../NoopPropertyCompressionOptions.cs | 10 + .../AppSettingsNucachePropertyMapFactory.cs | 47 ----- .../NuCache/DataSource/DatabaseDataSource.cs | 12 +- .../IContentNestedDataSerializer.cs | 8 +- .../INucachePropertyOptionsFactory.cs | 13 -- .../JsonContentNestedDataSerializer.cs | 4 +- .../DataSource/LazyCompressedString.cs | 22 ++- .../Lz4DictionaryOfPropertyDataSerializer.cs | 181 ------------------ .../MsgPackContentNestedDataSerializer.cs | 94 +++------ .../DataSource/NuCacheCompressionOptions.cs | 57 ------ .../NuCachePropertyCompressionOptions.cs | 33 ---- .../NucachePropertyCompressionLevel.cs | 31 --- .../NucachePropertyDecompressionLevel.cs | 15 -- .../NuCache/DataSource/SerializerBase.cs | 40 ++-- .../PublishedCache/NuCache/NuCacheComposer.cs | 33 +--- .../NuCache/PublishedSnapshotService.cs | 4 +- src/Umbraco.Web/Umbraco.Web.csproj | 10 +- 20 files changed, 155 insertions(+), 525 deletions(-) create mode 100644 src/Umbraco.Web/PropertyEditors/ComplexEditorPropertyCompressionOptions.cs create mode 100644 src/Umbraco.Web/PropertyEditors/IPropertyCompressionOptions.cs create mode 100644 src/Umbraco.Web/PropertyEditors/NoopPropertyCompressionOptions.cs delete mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs delete mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/INucachePropertyOptionsFactory.cs delete mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs delete mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCacheCompressionOptions.cs delete mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCachePropertyCompressionOptions.cs delete mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyCompressionLevel.cs delete mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyDecompressionLevel.cs diff --git a/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs b/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs index debfddef98..c85973f4b0 100644 --- a/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs +++ b/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs @@ -55,14 +55,14 @@ namespace Umbraco.Tests.PublishedContent UrlSegment = "home" }; - var json = jsonSerializer.Serialize(content); - var msgPack = msgPackSerializer.Serialize(content); + var json = jsonSerializer.Serialize(1, content); + var msgPack = msgPackSerializer.Serialize(1, content); Console.WriteLine(json); Console.WriteLine(msgPackSerializer.ToJson(msgPack)); - var jsonContent = jsonSerializer.Deserialize(json); - var msgPackContent = msgPackSerializer.Deserialize(msgPack); + var jsonContent = jsonSerializer.Deserialize(1, json); + var msgPackContent = msgPackSerializer.Deserialize(1, msgPack); CollectionAssert.AreEqual(jsonContent.CultureData.Keys, msgPackContent.CultureData.Keys); diff --git a/src/Umbraco.Web/PropertyEditors/ComplexEditorPropertyCompressionOptions.cs b/src/Umbraco.Web/PropertyEditors/ComplexEditorPropertyCompressionOptions.cs new file mode 100644 index 0000000000..f4776f652d --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ComplexEditorPropertyCompressionOptions.cs @@ -0,0 +1,48 @@ +using K4os.Compression.LZ4; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; + +namespace Umbraco.Web.PropertyEditors +{ + + /// + /// Ensures all property types that have an editor storing a complex value are compressed + /// + public class ComplexEditorPropertyCompressionOptions : IPropertyCompressionOptions + { + private readonly IContentTypeService _contentTypeService; + private readonly PropertyEditorCollection _propertyEditors; + private readonly ConcurrentDictionary<(int, string), string> _editorValueTypes = new ConcurrentDictionary<(int, string), string>(); + + public ComplexEditorPropertyCompressionOptions(IContentTypeService contentTypeService, PropertyEditorCollection propertyEditors) + { + _contentTypeService = contentTypeService; + _propertyEditors = propertyEditors; + } + + public bool IsCompressed(int contentTypeId, string alias) + { + var valueType = _editorValueTypes.GetOrAdd((contentTypeId, alias), x => + { + var ct = _contentTypeService.Get(contentTypeId); + if (ct == null) return null; + + var propertyType = ct.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == alias); + if (propertyType == null) return null; + + if (!_propertyEditors.TryGet(propertyType.PropertyEditorAlias, out var propertyEditor)) return null; + + var editor = propertyEditor.GetValueEditor(); + if (editor == null) return null; + + return editor.ValueType; + }); + + return valueType == ValueTypes.Json || valueType == ValueTypes.Xml || valueType == ValueTypes.Text; + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/IPropertyCompressionOptions.cs b/src/Umbraco.Web/PropertyEditors/IPropertyCompressionOptions.cs new file mode 100644 index 0000000000..70f3e7c6f0 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/IPropertyCompressionOptions.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Determines if a property type's value should be compressed + /// + public interface IPropertyCompressionOptions + { + bool IsCompressed(int contentTypeId, string alias); + } +} diff --git a/src/Umbraco.Web/PropertyEditors/NoopPropertyCompressionOptions.cs b/src/Umbraco.Web/PropertyEditors/NoopPropertyCompressionOptions.cs new file mode 100644 index 0000000000..638306d7ca --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/NoopPropertyCompressionOptions.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Disables all compression for all properties + /// + internal class NoopPropertyCompressionOptions : IPropertyCompressionOptions + { + public bool IsCompressed(int contentTypeId, string alias) => false; + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs deleted file mode 100644 index 544231bd32..0000000000 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/AppSettingsNucachePropertyMapFactory.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.Web.PublishedCache.NuCache.DataSource -{ - // TODO: We'll remove this when the responsibility for compressing property data is at the property editor level - internal class AppSettingsNuCachePropertyMapFactory : INuCachePropertyOptionsFactory - { - public NuCachePropertyCompressionOptions GetNuCachePropertyOptions() - { - var options = new NuCachePropertyCompressionOptions( - GetPropertyMap(), - K4os.Compression.LZ4.LZ4Level.L10_OPT, - null); - - return options; - } - - public IReadOnlyDictionary GetPropertyMap() - { - var propertyMap = new Dictionary(); - // TODO: Use xml/json/c# to define map - var propertyDictionarySerializerMap = ConfigurationManager.AppSettings["Umbraco.Web.PublishedCache.NuCache.PropertySerializationMap"]; - if (!string.IsNullOrWhiteSpace(propertyDictionarySerializerMap)) - { - //propertyAlias,CompressionLevel,DecompressionLevel,mappedAlias; - propertyDictionarySerializerMap.Split(';') - .Select(x => - { - var y = x.Split(','); - (string alias, NucachePropertyCompressionLevel compressionLevel, NucachePropertyDecompressionLevel decompressionLevel, string mappedAlias) v = (y[0], - (NucachePropertyCompressionLevel)System.Enum.Parse(typeof(NucachePropertyCompressionLevel), y[1]), - (NucachePropertyDecompressionLevel)System.Enum.Parse(typeof(NucachePropertyDecompressionLevel), y[2]), - y[3] - ); - return v; - }) - .ToList().ForEach(x => propertyMap.Add(x.alias, new NuCacheCompressionOptions(x.compressionLevel, x.decompressionLevel, x.mappedAlias))); - } - return propertyMap; - } - } -} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index 3cad7171be..a52fde2f0a 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -259,8 +259,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource else { var nested = _contentNestedDataSerializer is IContentNestedDataByteSerializer byteSerializer - ? byteSerializer.DeserializeBytes(dto.EditDataRaw) - : _contentNestedDataSerializer.Deserialize(dto.EditData); + ? byteSerializer.DeserializeBytes(dto.ContentTypeId, dto.EditDataRaw) + : _contentNestedDataSerializer.Deserialize(dto.ContentTypeId, dto.EditData); d = new ContentData { @@ -288,8 +288,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource else { var nested = _contentNestedDataSerializer is IContentNestedDataByteSerializer byteSerializer - ? byteSerializer.DeserializeBytes(dto.PubDataRaw) - : _contentNestedDataSerializer.Deserialize(dto.PubData); + ? byteSerializer.DeserializeBytes(dto.ContentTypeId, dto.PubDataRaw) + : _contentNestedDataSerializer.Deserialize(dto.ContentTypeId, dto.PubData); p = new ContentData { @@ -326,8 +326,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource throw new InvalidOperationException("No data for media " + dto.Id); var nested = _contentNestedDataSerializer is IContentNestedDataByteSerializer byteSerializer - ? byteSerializer.DeserializeBytes(dto.EditDataRaw) - : _contentNestedDataSerializer.Deserialize(dto.EditData); + ? byteSerializer.DeserializeBytes(dto.ContentTypeId, dto.EditDataRaw) + : _contentNestedDataSerializer.Deserialize(dto.ContentTypeId, dto.EditData); var p = new ContentData { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs index 32eb388bee..09933d735d 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs @@ -7,8 +7,8 @@ /// public interface IContentNestedDataByteSerializer : IContentNestedDataSerializer { - ContentNestedData DeserializeBytes(byte[] data); - byte[] SerializeBytes(ContentNestedData nestedData); + ContentNestedData DeserializeBytes(int contentTypeId, byte[] data); + byte[] SerializeBytes(int contentTypeId, ContentNestedData nestedData); } /// @@ -16,7 +16,7 @@ /// public interface IContentNestedDataSerializer { - ContentNestedData Deserialize(string data); - string Serialize(ContentNestedData nestedData); + ContentNestedData Deserialize(int contentTypeId, string data); + string Serialize(int contentTypeId, ContentNestedData nestedData); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/INucachePropertyOptionsFactory.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/INucachePropertyOptionsFactory.cs deleted file mode 100644 index d423499744..0000000000 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/INucachePropertyOptionsFactory.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.Web.PublishedCache.NuCache.DataSource -{ - public interface INuCachePropertyOptionsFactory - { - NuCachePropertyCompressionOptions GetNuCachePropertyOptions(); - } -} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs index 6635fa7090..d4f11591c1 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs @@ -11,7 +11,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource internal class JsonContentNestedDataSerializer : IContentNestedDataSerializer { - public ContentNestedData Deserialize(string data) + public ContentNestedData Deserialize(int contentTypeId, string data) { // by default JsonConvert will deserialize our numeric values as Int64 // which is bad, because they were Int32 in the database - take care @@ -30,7 +30,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return JsonConvert.DeserializeObject(data, settings); } - public string Serialize(ContentNestedData nestedData) + public string Serialize(int contentTypeId, ContentNestedData nestedData) { // note that numeric values (which are Int32) are serialized without their // type (eg "value":1234) and JsonConvert by default deserializes them as Int64 diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs index 9df40daf76..3e0e796d36 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs @@ -1,17 +1,18 @@ using K4os.Compression.LZ4; using System; using System.Text; -using System.Threading; +using Umbraco.Core.Exceptions; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { /// /// Lazily decompresses a LZ4 Pickler compressed UTF8 string /// - internal class LazyCompressedString + internal struct LazyCompressedString { - private string _str; private byte[] _bytes; + private string _str; + private readonly object _locker; /// /// Constructor @@ -19,7 +20,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// LZ4 Pickle compressed UTF8 String public LazyCompressedString(byte[] bytes) { + _locker = new object(); _bytes = bytes; + _str = null; } public byte[] GetBytes() @@ -31,13 +34,18 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public override string ToString() { - return LazyInitializer.EnsureInitialized(ref _str, () => + if (_str != null) return _str; + lock (_locker) { - var str = Encoding.UTF8.GetString(LZ4Pickler.Unpickle(_bytes)); + if (_str != null) return _str; // double check + if (_bytes == null) throw new PanicException("Bytes have already been cleared"); + _str = Encoding.UTF8.GetString(LZ4Pickler.Unpickle(_bytes)); _bytes = null; - return str; - }); + } + return _str; } + + public static implicit operator string(LazyCompressedString l) => l.ToString(); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs deleted file mode 100644 index 1a2d26f4b1..0000000000 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/Lz4DictionaryOfPropertyDataSerializer.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using CSharpTest.Net.Serialization; -using Umbraco.Core; -using System.Text; -using K4os.Compression.LZ4; - -namespace Umbraco.Web.PublishedCache.NuCache.DataSource -{ - - /// - /// Serializes/Deserializes property data as a dictionary for BTree with Lz4 compression options - /// - internal class Lz4DictionaryOfPropertyDataSerializer : SerializerBase, ISerializer>, IDictionaryOfPropertyDataSerializer - { - private readonly IReadOnlyDictionary _compressProperties; - private readonly IReadOnlyDictionary _uncompressProperties; - - - public Lz4DictionaryOfPropertyDataSerializer(INuCachePropertyOptionsFactory nucachePropertyOptionsFactory) - { - var nucachePropertyOptions = nucachePropertyOptionsFactory.GetNuCachePropertyOptions(); - _compressProperties = nucachePropertyOptions.PropertyMap.ToList().ToDictionary(x => string.Intern(x.Key), x => new NuCacheCompressionOptions(x.Value.CompressLevel, x.Value.DecompressLevel, string.Intern(x.Value.MappedAlias))); - _uncompressProperties = _compressProperties.ToList().ToDictionary(x => x.Value.MappedAlias, x => new NuCacheCompressionOptions(x.Value.CompressLevel, x.Value.DecompressLevel, x.Key)); - - _nucachePropertyOptions = nucachePropertyOptions; - } - - - public IDictionary ReadFrom(Stream stream) - { - var dict = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - - // read properties count - var pcount = PrimitiveSerializer.Int32.ReadFrom(stream); - - // read each property - for (var i = 0; i < pcount; i++) - { - - // read property alias - var alias = PrimitiveSerializer.String.ReadFrom(stream); - var map = GetDeserializationMap(alias); - var key = string.Intern(map.MappedAlias ?? alias); - - // read values count - var vcount = PrimitiveSerializer.Int32.ReadFrom(stream); - - // create pdata and add to the dictionary - var pdatas = new List(); - - // for each value, read and add to pdata - for (var j = 0; j < vcount; j++) - { - var pdata = new PropertyData(); - pdatas.Add(pdata); - - // everything that can be null is read/written as object - // even though - culture and segment should never be null here, as 'null' represents - // the 'current' value, and string.Empty should be used to represent the invariant or - // neutral values - PropertyData throws when getting nulls, so falling back to - // string.Empty here - what else? - pdata.Culture = ReadStringObject(stream, true) ?? string.Empty; - pdata.Segment = ReadStringObject(stream, true) ?? string.Empty; - pdata.Value = ReadObject(stream); - - switch (map.CompressLevel) - { - // If the property is compressed at either the DB or Nucache level, it means it's compressed here and we need to decompress - case NucachePropertyCompressionLevel.SQLDatabase: - case NucachePropertyCompressionLevel.NuCacheDatabase: - if (!(pdata.Value is null) && pdata.Value is byte[] byteArrayValue) - { - //Compressed string - switch (map.DecompressLevel) - { - case NucachePropertyDecompressionLevel.Lazy: - pdata.Value = new LazyCompressedString(byteArrayValue); - break; - case NucachePropertyDecompressionLevel.NotCompressed: - //Shouldn't be any not compressed - throw new InvalidOperationException($"{NucachePropertyDecompressionLevel.NotCompressed} cannot be a decompression option for property {alias} since it's compresion option is {map.CompressLevel}"); - case NucachePropertyDecompressionLevel.Immediate: - default: - pdata.Value = Encoding.UTF8.GetString(LZ4Pickler.Unpickle(byteArrayValue)); - break; - } - } - break; - } - } - - dict[key] = pdatas.ToArray(); - } - return dict; - } - - public void WriteTo(IDictionary value, Stream stream) - { - // write properties count - PrimitiveSerializer.Int32.WriteTo(value.Count, stream); - - // write each property - foreach (var (alias, values) in value) - { - var map = GetSerializationMap(alias); - - // write alias - PrimitiveSerializer.String.WriteTo(map.MappedAlias ?? alias, stream); - - // write values count - PrimitiveSerializer.Int32.WriteTo(values.Length, stream); - - // write each value - foreach (var pdata in values) - { - // everything that can be null is read/written as object - // even though - culture and segment should never be null here, - // see note in ReadFrom() method above - WriteObject(pdata.Culture ?? string.Empty, stream); - WriteObject(pdata.Segment ?? string.Empty, stream); - - //Only compress strings (if NucachePropertyCompressionLevel.SQLDatabase has been specified then the property value will already be compressed) - switch (map.CompressLevel) - { - // If we're compressing into btree at the property level - case NucachePropertyCompressionLevel.NuCacheDatabase: - - if (pdata.Value is string stringValue && !(pdata.Value is null) - && (_nucachePropertyOptions.MinimumCompressibleStringLength is null - || !_nucachePropertyOptions.MinimumCompressibleStringLength.HasValue - || stringValue.Length > _nucachePropertyOptions.MinimumCompressibleStringLength.Value)) - { - var stringBytes = Encoding.UTF8.GetBytes(stringValue); - var compressedBytes = LZ4Pickler.Pickle(stringBytes, _nucachePropertyOptions.LZ4CompressionLevel); - WriteObject(compressedBytes, stream); - } - else - { - WriteObject(pdata.Value, stream); - } - break; - default: - WriteObject(pdata.Value, stream); - break; - } - } - } - } - private static readonly NuCacheCompressionOptions DefaultMap = new NuCacheCompressionOptions(NucachePropertyCompressionLevel.None, NucachePropertyDecompressionLevel.NotCompressed, null); - private readonly NuCachePropertyCompressionOptions _nucachePropertyOptions; - - public NuCacheCompressionOptions GetSerializationMap(string propertyAlias) - { - if (_compressProperties is null) - { - return DefaultMap; - } - if (_compressProperties.TryGetValue(propertyAlias, out var map1)) - { - return map1; - } - - return DefaultMap; - } - public NuCacheCompressionOptions GetDeserializationMap(string propertyAlias) - { - if (_uncompressProperties is null) - { - return DefaultMap; - } - if (_uncompressProperties.TryGetValue(propertyAlias, out var map2)) - { - return map2; - } - return DefaultMap; - } - } -} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index 0475810422..0ea2b96fbe 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -2,10 +2,12 @@ using MessagePack; using MessagePack.Formatters; using MessagePack.Resolvers; +using NPoco.FluentMappings; using System; using System.Collections.Generic; using System.Linq; using System.Text; +using Umbraco.Web.PropertyEditors; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { @@ -15,9 +17,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource internal class MsgPackContentNestedDataSerializer : IContentNestedDataByteSerializer { private MessagePackSerializerOptions _options; - private readonly NuCachePropertyCompressionOptions _propertyOptions; + private readonly IPropertyCompressionOptions _propertyOptions; - public MsgPackContentNestedDataSerializer(INuCachePropertyOptionsFactory propertyOptionsFactory = null) + public MsgPackContentNestedDataSerializer(IPropertyCompressionOptions propertyOptions = null) { var defaultOptions = ContractlessStandardResolver.Options; @@ -37,7 +39,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource _options = defaultOptions .WithResolver(resolver) .WithCompression(MessagePackCompression.Lz4BlockArray); - _propertyOptions = propertyOptionsFactory?.GetNuCachePropertyOptions() ?? NuCachePropertyCompressionOptions.Empty; + _propertyOptions = propertyOptions ?? new NoopPropertyCompressionOptions(); } public string ToJson(string serialized) @@ -47,113 +49,69 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return json; } - public ContentNestedData Deserialize(string data) + public ContentNestedData Deserialize(int contentTypeId, string data) { var bin = Convert.FromBase64String(data); var nestedData = MessagePackSerializer.Deserialize(bin, _options); - Expand(nestedData); + Expand(contentTypeId, nestedData); return nestedData; } - public string Serialize(ContentNestedData nestedData) + public string Serialize(int contentTypeId, ContentNestedData nestedData) { - Compress(nestedData); + Compress(contentTypeId, nestedData); var bin = MessagePackSerializer.Serialize(nestedData, _options); return Convert.ToBase64String(bin); } - public ContentNestedData DeserializeBytes(byte[] data) + public ContentNestedData DeserializeBytes(int contentTypeId, byte[] data) { var nestedData = MessagePackSerializer.Deserialize(data, _options); - Expand(nestedData); + Expand(contentTypeId, nestedData); return nestedData; } - public byte[] SerializeBytes(ContentNestedData nestedData) + public byte[] SerializeBytes(int contentTypeId, ContentNestedData nestedData) { - Compress(nestedData); + Compress(contentTypeId, nestedData); return MessagePackSerializer.Serialize(nestedData, _options); } /// - /// Used during serialization to compress properties and map property names to shorter names + /// Used during serialization to compress properties /// /// - private void Compress(ContentNestedData nestedData) + private void Compress(int contentTypeId, ContentNestedData nestedData) { - if (_propertyOptions.PropertyMap != null && _propertyOptions.PropertyMap.Count > 0) + foreach(var propertyAliasToData in nestedData.PropertyData) { - foreach (var map in _propertyOptions.PropertyMap) + if (_propertyOptions.IsCompressed(contentTypeId, propertyAliasToData.Key)) { - if (map.Value.CompressLevel.Equals(NucachePropertyCompressionLevel.SQLDatabase)) + foreach(var property in propertyAliasToData.Value.Where(x => x.Value != null && x.Value is string)) { - if (nestedData.PropertyData.TryGetValue(map.Key, out PropertyData[] properties)) - { - foreach (var property in properties.Where(x => x.Value != null && x.Value is string)) - { - property.Value = LZ4Pickler.Pickle(Encoding.UTF8.GetBytes(property.Value as string), _propertyOptions.LZ4CompressionLevel); - } - } - } - - // if there is an alias map for this property then use that instead of the real property alias - // (used to save memory, the mapped alias is normally a single char or at least a smaller string) - if (map.Value.MappedAlias != null && !map.Key.Equals(map.Value.MappedAlias) - && nestedData.PropertyData.TryGetValue(map.Key, out PropertyData[] properties2)) - { - nestedData.PropertyData.Remove(map.Key); - nestedData.PropertyData.Add(map.Value.MappedAlias, properties2); + property.Value = LZ4Pickler.Pickle(Encoding.UTF8.GetBytes((string)property.Value), LZ4Level.L00_FAST); } } } } /// - /// Used during deserialization to map the property data as lazy or expand the value and re-map back to the true property aliases + /// Used during deserialization to map the property data as lazy or expand the value /// /// - private void Expand(ContentNestedData nestedData) + private void Expand(int contentTypeId, ContentNestedData nestedData) { - if (_propertyOptions.PropertyMap != null && _propertyOptions.PropertyMap.Count > 0) + foreach (var propertyAliasToData in nestedData.PropertyData) { - foreach (var map in _propertyOptions.PropertyMap) + if (_propertyOptions.IsCompressed(contentTypeId, propertyAliasToData.Key)) { - if (map.Value.CompressLevel.Equals(NucachePropertyCompressionLevel.SQLDatabase)) + foreach (var property in propertyAliasToData.Value.Where(x => x.Value != null)) { - // if there is an alias map for this property then re-map to the real property alias - if (map.Value.MappedAlias != null && !map.Key.Equals(map.Value.MappedAlias) - && nestedData.PropertyData.TryGetValue(map.Value.MappedAlias, out PropertyData[] properties2)) + if (property.Value is byte[] byteArrayValue) { - nestedData.PropertyData.Remove(map.Value.MappedAlias); - nestedData.PropertyData.Add(map.Key, properties2); - } - - if (nestedData.PropertyData.TryGetValue(map.Key, out PropertyData[] properties)) - { - foreach (var pdata in properties) - { - if (!(pdata.Value is null) && pdata.Value is byte[] byteArrayValue) - { - //Compressed string - switch (map.Value.DecompressLevel) - { - case NucachePropertyDecompressionLevel.Lazy: - pdata.Value = new LazyCompressedString(byteArrayValue); - break; - case NucachePropertyDecompressionLevel.NotCompressed: - //Shouldn't be any not compressed - throw new InvalidOperationException($"{NucachePropertyDecompressionLevel.NotCompressed} cannot be a decompression option for property {map.Key} since it's compresion option is {map.Value.CompressLevel}"); - case NucachePropertyDecompressionLevel.Immediate: - default: - pdata.Value = Encoding.UTF8.GetString(LZ4Pickler.Unpickle(byteArrayValue)); - break; - } - } - } + property.Value = new LazyCompressedString(byteArrayValue); } } - - } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCacheCompressionOptions.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCacheCompressionOptions.cs deleted file mode 100644 index 36f1606008..0000000000 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCacheCompressionOptions.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Umbraco.Web.PublishedCache.NuCache.DataSource -{ - public struct NuCacheCompressionOptions : IEquatable - { - public NuCacheCompressionOptions(NucachePropertyCompressionLevel compressLevel, NucachePropertyDecompressionLevel decompressLevel, string mappedAlias) - { - CompressLevel = compressLevel; - DecompressLevel = decompressLevel; - MappedAlias = mappedAlias ?? throw new ArgumentNullException(nameof(mappedAlias)); - } - - public NucachePropertyCompressionLevel CompressLevel { get; private set; } - public NucachePropertyDecompressionLevel DecompressLevel { get; private set; } - - /// - /// Used to map a real property alias to a shorter moniker in memory - /// - /// - /// This is simply a memory saving mechanism - /// - public string MappedAlias { get; private set; } - - public override bool Equals(object obj) - { - return obj is NuCacheCompressionOptions options && Equals(options); - } - - public bool Equals(NuCacheCompressionOptions other) - { - return CompressLevel == other.CompressLevel && - DecompressLevel == other.DecompressLevel && - MappedAlias == other.MappedAlias; - } - - public override int GetHashCode() - { - var hashCode = 961370163; - hashCode = hashCode * -1521134295 + CompressLevel.GetHashCode(); - hashCode = hashCode * -1521134295 + DecompressLevel.GetHashCode(); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(MappedAlias); - return hashCode; - } - - public static bool operator ==(NuCacheCompressionOptions left, NuCacheCompressionOptions right) - { - return left.Equals(right); - } - - public static bool operator !=(NuCacheCompressionOptions left, NuCacheCompressionOptions right) - { - return !(left == right); - } - } -} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCachePropertyCompressionOptions.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCachePropertyCompressionOptions.cs deleted file mode 100644 index 55ab813783..0000000000 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NuCachePropertyCompressionOptions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using K4os.Compression.LZ4; -using System; -using System.Collections.Generic; - -namespace Umbraco.Web.PublishedCache.NuCache.DataSource -{ - - public class NuCachePropertyCompressionOptions - { - /// - /// Returns empty options - /// - public static NuCachePropertyCompressionOptions Empty { get; } = new NuCachePropertyCompressionOptions(); - - private NuCachePropertyCompressionOptions() - { - } - - public NuCachePropertyCompressionOptions(IReadOnlyDictionary propertyMap, LZ4Level lZ4CompressionLevel, long? minimumCompressibleStringLength) - { - PropertyMap = propertyMap ?? throw new ArgumentNullException(nameof(propertyMap)); - LZ4CompressionLevel = lZ4CompressionLevel; - MinimumCompressibleStringLength = minimumCompressibleStringLength; - } - - public IReadOnlyDictionary PropertyMap { get; } = new Dictionary(); - - public LZ4Level LZ4CompressionLevel { get; } = LZ4Level.L00_FAST; - - // TODO: Unsure if we really want to keep this - public long? MinimumCompressibleStringLength { get; } - } -} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyCompressionLevel.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyCompressionLevel.cs deleted file mode 100644 index 23826fd722..0000000000 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyCompressionLevel.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Umbraco.Web.PublishedCache.NuCache.DataSource -{ - /// - /// If/where to compress custom properties for nucache - /// - public enum NucachePropertyCompressionLevel - { - None = 0, - - /// - /// Compress property data at the nucache SQL DB table level - /// - /// - /// Idea being we only compress this once. - /// All the records in cmsContentNu need to be rebuilt when this gets enabled. - /// Good option as then we don't use up memory / cpu to compress at boot. - /// - SQLDatabase = 1, - - /// - /// Compress property data at the nucache BTree level - /// - /// - /// Compress the property when writing to nucache bplustree after reading from the database. - /// Idea being we compress this at rebuild / boot. - /// This option supports older items not being compressed already, at the expense of doing this compression at boot. - /// But it also means you can easily switch between None and NuCacheDatabase if performance is worse. - /// - NuCacheDatabase = 2 - } -} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyDecompressionLevel.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyDecompressionLevel.cs deleted file mode 100644 index f4d485be71..0000000000 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/NucachePropertyDecompressionLevel.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Umbraco.Web.PublishedCache.NuCache.DataSource -{ - /// - /// If/where to decompress custom properties for nucache - /// - public enum NucachePropertyDecompressionLevel - { - NotCompressed = 0, - - // TODO: I'm unsure if this will ever be necessary, lazy seems good and deserialization would only occur once - Immediate = 1, - - Lazy = 2 - } -} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs index b90c418750..8b02946fc2 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs @@ -16,11 +16,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource private const char PrefixDouble = 'B'; private const char PrefixDateTime = 'D'; private const char PrefixByte = 'O'; - - // TODO: It might make sense to have another prefix for an LZ4 compressed byte array. - // Would be an improvement for the SQLDatabase compression option because then you could mix compressed and decompressed properties with the same alias. - // For example, don't compress recent content, but compress older content. private const char PrefixByteArray = 'A'; + private const char PrefixCompressedStringByteArray = 'C'; protected string ReadString(Stream stream) => PrimitiveSerializer.String.ReadFrom(stream); protected int ReadInt(Stream stream) => PrimitiveSerializer.Int32.ReadFrom(stream); @@ -30,7 +27,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource protected DateTime ReadDateTime(Stream stream) => PrimitiveSerializer.DateTime.ReadFrom(stream); protected byte[] ReadByteArray(Stream stream) => PrimitiveSerializer.Bytes.ReadFrom(stream); - private T? ReadObject(Stream stream, char t, Func read) + private T? ReadStruct(Stream stream, char t, Func read) where T : struct { var type = PrimitiveSerializer.Char.ReadFrom(stream); @@ -51,29 +48,19 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource : PrimitiveSerializer.String.ReadFrom(stream); } - protected int? ReadIntObject(Stream stream) => ReadObject(stream, PrefixInt32, ReadInt); - protected long? ReadLongObject(Stream stream) => ReadObject(stream, PrefixLong, ReadLong); - protected float? ReadFloatObject(Stream stream) => ReadObject(stream, PrefixFloat, ReadFloat); - protected double? ReadDoubleObject(Stream stream) => ReadObject(stream, PrefixDouble, ReadDouble); - protected DateTime? ReadDateTimeObject(Stream stream) => ReadObject(stream, PrefixDateTime, ReadDateTime); - - protected byte[] ReadByteArrayObject(Stream stream) // required 'cos byte[] is not a struct - { - var type = PrimitiveSerializer.Char.ReadFrom(stream); - if (type == PrefixNull) return null; - if (type != PrefixByteArray) - throw new NotSupportedException($"Cannot deserialize type '{type}', expected '{PrefixByteArray}'."); - return PrimitiveSerializer.Bytes.ReadFrom(stream); - } - + protected int? ReadIntObject(Stream stream) => ReadStruct(stream, PrefixInt32, ReadInt); + protected long? ReadLongObject(Stream stream) => ReadStruct(stream, PrefixLong, ReadLong); + protected float? ReadFloatObject(Stream stream) => ReadStruct(stream, PrefixFloat, ReadFloat); + protected double? ReadDoubleObject(Stream stream) => ReadStruct(stream, PrefixDouble, ReadDouble); + protected DateTime? ReadDateTimeObject(Stream stream) => ReadStruct(stream, PrefixDateTime, ReadDateTime); protected object ReadObject(Stream stream) => ReadObject(PrimitiveSerializer.Char.ReadFrom(stream), stream); protected object ReadObject(char type, Stream stream) { - // NOTE: There is going to be a ton of boxing going on here, but i'm not sure we can avoid that because innevitably with our - // current model structure the value will need to end up being 'object' at some point anyways. + // NOTE: This method is only called when reading property data, some boxing may occur but all other reads for structs are + // done with ReadStruct to reduce all boxing. switch (type) { @@ -98,8 +85,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource case PrefixDateTime: return PrimitiveSerializer.DateTime.ReadFrom(stream); case PrefixByteArray: - // When it's a byte array always return as a LazyCompressedString - // TODO: Else we need to make a different prefix for lazy vs eager loading + return PrimitiveSerializer.Bytes.ReadFrom(stream); + case PrefixCompressedStringByteArray: return new LazyCompressedString(PrimitiveSerializer.Bytes.ReadFrom(stream)); default: throw new NotSupportedException($"Cannot deserialize unknown type '{type}'."); @@ -108,6 +95,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource protected void WriteObject(object value, Stream stream) { + // NOTE: This method is only currently used to write 'string' information, all other writes are done directly with the PrimitiveSerializer + // so no boxing occurs. Though potentially we should write everything via this class just like we do for reads. + if (value == null) { PrimitiveSerializer.Char.WriteTo(PrefixNull, stream); @@ -164,7 +154,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } else if (value is LazyCompressedString lazyCompressedString) { - PrimitiveSerializer.Char.WriteTo(PrefixByteArray, stream); + PrimitiveSerializer.Char.WriteTo(PrefixCompressedStringByteArray, stream); PrimitiveSerializer.Bytes.WriteTo(lazyCompressedString.GetBytes(), stream); } else diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs index c098b516a0..c5109db027 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs @@ -3,6 +3,7 @@ using System.Configuration; using System.Linq; using Umbraco.Core; using Umbraco.Core.Composing; +using Umbraco.Web.PropertyEditors; using Umbraco.Web.PublishedCache.NuCache.DataSource; namespace Umbraco.Web.PublishedCache.NuCache @@ -14,36 +15,22 @@ namespace Umbraco.Web.PublishedCache.NuCache base.Compose(composition); var serializer = ConfigurationManager.AppSettings["Umbraco.Web.PublishedCache.NuCache.Serializer"]; - composition.Register(); - composition.Register(); - - // TODO: Based on our findings it seems like this should not be configurable, we should just be using this because it's better - if (serializer == "MsgPack") + if (serializer != "MsgPack") { - var propertyDictionarySerializer = ConfigurationManager.AppSettings["Umbraco.Web.PublishedCache.NuCache.DictionaryOfPropertiesSerializer"]; - if (propertyDictionarySerializer == "LZ4Map") - { - - composition.Register(factory => - { - var lz4Serializer = factory.GetInstance(); - return new ContentDataSerializer(lz4Serializer); - }); - } - else - { - composition.Register(factory => new ContentDataSerializer(new DictionaryOfPropertyDataSerializer())); - } - composition.Register(); + // TODO: This allows people to revert to the legacy serializer, by default it will be MessagePack + composition.RegisterUnique(); + composition.RegisterUnique(); } else { - composition.Register(); - composition.Register(factory => new ContentDataSerializer(new DictionaryOfPropertyDataSerializer())); + composition.RegisterUnique(); + composition.RegisterUnique(); } + + composition.RegisterUnique(factory => new ContentDataSerializer(new DictionaryOfPropertyDataSerializer())); // register the NuCache database data source - composition.Register(); + composition.RegisterUnique(); // register the NuCache published snapshot service // must register default options, required in the service ctor diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 0135f204c7..e670cb75f6 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1459,8 +1459,8 @@ namespace Umbraco.Web.PublishedCache.NuCache { NodeId = content.Id, Published = published, - Data = !(_contentNestedDataSerializer is IContentNestedDataByteSerializer) ? _contentNestedDataSerializer.Serialize(nestedData) : null, - RawData = (_contentNestedDataSerializer is IContentNestedDataByteSerializer byteSerializer) ? byteSerializer.SerializeBytes(nestedData) : null + Data = !(_contentNestedDataSerializer is IContentNestedDataByteSerializer) ? _contentNestedDataSerializer.Serialize(content.ContentTypeId, nestedData) : null, + RawData = (_contentNestedDataSerializer is IContentNestedDataByteSerializer byteSerializer) ? byteSerializer.SerializeBytes(content.ContentTypeId, nestedData) : null }; //Core.Composing.Current.Logger.Debug(dto.Data); diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index fac5a90b0d..cd689664a1 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -256,20 +256,16 @@ + + - - - - - - - + From 880e5179bfffb030e56a4207b5b7d17c10dc7860 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 26 Aug 2020 16:11:58 +1000 Subject: [PATCH 038/289] Fixing tests --- .../Persistence/Querying/ExpressionVisitorBase.cs | 2 +- src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTests.cs | 8 ++++---- .../Persistence/Querying/QueryBuilderTests.cs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs index 4316023b03..dd8ef75178 100644 --- a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs +++ b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs @@ -331,7 +331,7 @@ namespace Umbraco.Core.Persistence.Querying // BUT we don't want to do a NOT SQL statement since this generally results in indexes not being used // so we want to do an == false SqlParameters.Add(false); - return Visited ? string.Empty : $"({o} = @{SqlParameters.Count - 1})"; + return Visited ? string.Empty : $"{o} = @{SqlParameters.Count - 1}"; //return Visited ? string.Empty : $"NOT ({o} = @{SqlParameters.Count - 1})"; default: // could be anything else, such as: x => !x.Path.StartsWith("-20") diff --git a/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTests.cs b/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTests.cs index a04984eb64..b4210ee31a 100644 --- a/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTests.cs +++ b/src/Umbraco.Tests/Persistence/NPocoTests/NPocoSqlTests.cs @@ -82,9 +82,9 @@ namespace Umbraco.Tests.Persistence.NPocoTests var sql = Sql().SelectAll().From() .Where(x => x.Trashed == false); - Assert.AreEqual("SELECT * FROM [umbracoNode] WHERE (NOT ([umbracoNode].[trashed] = @0))", sql.SQL.Replace("\n", " ")); + Assert.AreEqual("SELECT * FROM [umbracoNode] WHERE ([umbracoNode].[trashed] = @0)", sql.SQL.Replace("\n", " ")); Assert.AreEqual(1, sql.Arguments.Length); - Assert.AreEqual(true, sql.Arguments[0]); + Assert.AreEqual(false, sql.Arguments[0]); } [Test] @@ -92,9 +92,9 @@ namespace Umbraco.Tests.Persistence.NPocoTests { var sql = Sql().SelectAll().From().Where(x => x.Trashed == false); - Assert.AreEqual("SELECT * FROM [umbracoNode] WHERE (NOT ([umbracoNode].[trashed] = @0))", sql.SQL.Replace("\n", " ")); + Assert.AreEqual("SELECT * FROM [umbracoNode] WHERE ([umbracoNode].[trashed] = @0)", sql.SQL.Replace("\n", " ")); Assert.AreEqual(1, sql.Arguments.Length); - Assert.AreEqual(true, sql.Arguments[0]); + Assert.AreEqual(false, sql.Arguments[0]); } [Test] diff --git a/src/Umbraco.Tests/Persistence/Querying/QueryBuilderTests.cs b/src/Umbraco.Tests/Persistence/Querying/QueryBuilderTests.cs index ca6b4cd5f0..2b43b13a79 100644 --- a/src/Umbraco.Tests/Persistence/Querying/QueryBuilderTests.cs +++ b/src/Umbraco.Tests/Persistence/Querying/QueryBuilderTests.cs @@ -110,7 +110,7 @@ namespace Umbraco.Tests.Persistence.Querying Assert.AreEqual("-1,1046,1076,1089%", result.Arguments[0]); Assert.AreEqual(1046, result.Arguments[1]); Assert.AreEqual(true, result.Arguments[2]); - Assert.AreEqual(true, result.Arguments[3]); + Assert.AreEqual(false, result.Arguments[3]); } } } From 6bd6d97bdaccacaf34fe83261e7fe434a11a64e7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 24 Sep 2020 18:37:24 +1000 Subject: [PATCH 039/289] WIP (fixes migrations) --- .../Migrations/Upgrade/UmbracoPlan.cs | 6 +- .../Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs | 50 -------------- .../AddCmsContentNuByteColumn.cs | 2 +- .../Upgrade/V_8_9_0/UpgradedIncludeIndexes.cs | 69 +++++++++++++++++++ .../CompressedStorageAttribute.cs | 21 ++++++ ...StoragePropertyEditorCompressionOptions.cs | 44 ++++++++++++ .../PropertyEditors/DataEditorAttribute.cs | 1 + .../IPropertyCompressionOptions.cs | 2 +- src/Umbraco.Core/Umbraco.Core.csproj | 7 +- ...ComplexEditorPropertyCompressionOptions.cs | 48 ------------- .../NoopPropertyCompressionOptions.cs | 4 +- .../NuCache/DataSource/DatabaseDataSource.cs | 6 ++ .../MsgPackContentNestedDataSerializer.cs | 1 + .../PublishedCache/NuCache/NuCacheComposer.cs | 3 +- .../NuCache/PublishedSnapshotService.cs | 5 +- src/Umbraco.Web/Umbraco.Web.csproj | 2 - 16 files changed, 159 insertions(+), 112 deletions(-) delete mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs rename src/Umbraco.Core/Migrations/Upgrade/{V_8_7_0 => V_8_9_0}/AddCmsContentNuByteColumn.cs (90%) create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_9_0/UpgradedIncludeIndexes.cs create mode 100644 src/Umbraco.Core/PropertyEditors/CompressedStorageAttribute.cs create mode 100644 src/Umbraco.Core/PropertyEditors/CompressedStoragePropertyEditorCompressionOptions.cs rename src/{Umbraco.Web => Umbraco.Core}/PropertyEditors/IPropertyCompressionOptions.cs (84%) delete mode 100644 src/Umbraco.Web/PropertyEditors/ComplexEditorPropertyCompressionOptions.cs diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index a15dcfb23b..cb35783ffa 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Migrations.Upgrade.V_8_0_0; using Umbraco.Core.Migrations.Upgrade.V_8_0_1; using Umbraco.Core.Migrations.Upgrade.V_8_1_0; using Umbraco.Core.Migrations.Upgrade.V_8_6_0; -using Umbraco.Core.Migrations.Upgrade.V_8_8_0; +using Umbraco.Core.Migrations.Upgrade.V_8_9_0; namespace Umbraco.Core.Migrations.Upgrade { @@ -194,9 +194,9 @@ namespace Umbraco.Core.Migrations.Upgrade // to 8.7.0... To("{a78e3369-8ea3-40ec-ad3f-5f76929d2b20}"); - To("{8DDDCD0B-D7D5-4C97-BD6A-6B38CA65752F}"); - // to 8.8.0... + // to 8.9.0... + To("{8DDDCD0B-D7D5-4C97-BD6A-6B38CA65752F}"); To("{4695D0C9-0729-4976-985B-048D503665D8}"); //FINAL diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs deleted file mode 100644 index d4dd783876..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_8_0/UpgradedIncludeIndexes.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Linq; -using Umbraco.Core.Migrations.Expressions.Execute.Expressions; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Persistence.Dtos; - -namespace Umbraco.Core.Migrations.Upgrade.V_8_8_0 -{ - public class UpgradedIncludeIndexes : MigrationBase - { - public UpgradedIncludeIndexes(IMigrationContext context) - : base(context) - { - - } - - public override void Migrate() - { - var indexesToReplace = new[] { $"IX_{NodeDto.TableName}_UniqueId", $"IX_{NodeDto.TableName}_ObjectType" }; - DeleteIndexes(indexesToReplace); // delete existing ones - CreateIndexes(indexesToReplace); // replace - CreateIndexes($"IX_{NodeDto.TableName}_Level"); // add the new definitions - - var contentVersionNodeIdIndex = $"IX_{ContentVersionDto.TableName}_NodeId"; - DeleteIndexes(contentVersionNodeIdIndex); // delete existing ones - CreateIndexes(contentVersionNodeIdIndex); // replace - CreateIndexes($"IX_{ContentVersionDto.TableName}_Current"); // add the new definitions - } - - private void DeleteIndexes(params string[] toDelete) - { - var tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax); - - foreach (var i in toDelete) - Delete.Index(i).OnTable(tableDef.Name).Do(); - } - - private void CreateIndexes(params string[] toCreate) - { - var tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax); - - foreach(var c in toCreate) - { - // get the definition by name - var index = tableDef.Indexes.First(x => x.Name == c); - new ExecuteSqlStatementExpression(Context) { SqlStatement = Context.SqlContext.SqlSyntax.Format(index) }.Execute(); - } - - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/AddCmsContentNuByteColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_9_0/AddCmsContentNuByteColumn.cs similarity index 90% rename from src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/AddCmsContentNuByteColumn.cs rename to src/Umbraco.Core/Migrations/Upgrade/V_8_9_0/AddCmsContentNuByteColumn.cs index 3cb51afb99..c295e13051 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/AddCmsContentNuByteColumn.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_9_0/AddCmsContentNuByteColumn.cs @@ -1,7 +1,7 @@ using System.Linq; using Umbraco.Core.Persistence.Dtos; -namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0 +namespace Umbraco.Core.Migrations.Upgrade.V_8_9_0 { public class AddCmsContentNuByteColumn : MigrationBase { diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_9_0/UpgradedIncludeIndexes.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_9_0/UpgradedIncludeIndexes.cs new file mode 100644 index 0000000000..083294d1a5 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_9_0/UpgradedIncludeIndexes.cs @@ -0,0 +1,69 @@ +using System.Linq; +using Umbraco.Core.Migrations.Expressions.Execute.Expressions; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_9_0 +{ + public class UpgradedIncludeIndexes : MigrationBase + { + public UpgradedIncludeIndexes(IMigrationContext context) + : base(context) + { + + } + + public override void Migrate() + { + // Need to drop the FK for the redirect table before modifying the unique id index + Delete.ForeignKey() + .FromTable(Constants.DatabaseSchema.Tables.RedirectUrl) + .ForeignColumn("contentKey") + .ToTable(NodeDto.TableName) + .PrimaryColumn("uniqueID") + .Do(); + var nodeDtoIndexes = new[] { $"IX_{NodeDto.TableName}_UniqueId", $"IX_{NodeDto.TableName}_ObjectType", $"IX_{NodeDto.TableName}_Level" }; + DeleteIndexes(nodeDtoIndexes); // delete existing ones + CreateIndexes(nodeDtoIndexes); // update/add + // Now re-create the FK for the redirect table + Create.ForeignKey() + .FromTable(Constants.DatabaseSchema.Tables.RedirectUrl) + .ForeignColumn("contentKey") + .ToTable(NodeDto.TableName) + .PrimaryColumn("uniqueID") + .Do(); + + + var contentVersionIndexes = new[] { $"IX_{ContentVersionDto.TableName}_NodeId", $"IX_{ContentVersionDto.TableName}_Current" }; + DeleteIndexes(contentVersionIndexes); // delete existing ones + CreateIndexes(contentVersionIndexes); // update/add + } + + private void DeleteIndexes(params string[] toDelete) + { + var tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax); + + foreach (var i in toDelete) + { + if (IndexExists(i)) + { + Delete.Index(i).OnTable(tableDef.Name).Do(); + } + } + + } + + private void CreateIndexes(params string[] toCreate) + { + var tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax); + + foreach (var c in toCreate) + { + // get the definition by name + var index = tableDef.Indexes.First(x => x.Name == c); + new ExecuteSqlStatementExpression(Context) { SqlStatement = Context.SqlContext.SqlSyntax.Format(index) }.Execute(); + } + + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/CompressedStorageAttribute.cs b/src/Umbraco.Core/PropertyEditors/CompressedStorageAttribute.cs new file mode 100644 index 0000000000..31689c4ee9 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/CompressedStorageAttribute.cs @@ -0,0 +1,21 @@ +using System; + +namespace Umbraco.Core.PropertyEditors +{ + /// + /// When assigned to a DataEditor it indicates that the values it generates can be compressed + /// + /// + /// Used in conjunction with + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class CompressedStorageAttribute : Attribute + { + public CompressedStorageAttribute(bool isCompressed = true) + { + IsCompressed = isCompressed; + } + + public bool IsCompressed { get; } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/CompressedStoragePropertyEditorCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/CompressedStoragePropertyEditorCompressionOptions.cs new file mode 100644 index 0000000000..15795bb61c --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/CompressedStoragePropertyEditorCompressionOptions.cs @@ -0,0 +1,44 @@ +using System.Collections.Concurrent; +using System.Linq; +using Umbraco.Core.Services; + +namespace Umbraco.Core.PropertyEditors +{ + + /// + /// Ensures all property types that have an editor storing a complex value are compressed + /// + public class CompressedStoragePropertyEditorCompressionOptions : IPropertyCompressionOptions + { + private readonly IContentTypeService _contentTypeService; + private readonly PropertyEditorCollection _propertyEditors; + private readonly ConcurrentDictionary<(int, string), string> _editorValueTypes = new ConcurrentDictionary<(int, string), string>(); + + public CompressedStoragePropertyEditorCompressionOptions(PropertyEditorCollection propertyEditors) + { + _propertyEditors = propertyEditors; + } + + public bool IsCompressed(int contentTypeId, string alias) + { + return false; + //var valueType = _editorValueTypes.GetOrAdd((contentTypeId, alias), x => + //{ + // var ct = _contentTypeService.Get(contentTypeId); + // if (ct == null) return null; + + // var propertyType = ct.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == alias); + // if (propertyType == null) return null; + + // if (!_propertyEditors.TryGet(propertyType.PropertyEditorAlias, out var propertyEditor)) return null; + + // var editor = propertyEditor.GetValueEditor(); + // if (editor == null) return null; + + // return editor.ValueType; + //}); + + //return valueType == ValueTypes.Json || valueType == ValueTypes.Xml || valueType == ValueTypes.Text; + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs index 7b3be7ea5f..0375509797 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs @@ -2,6 +2,7 @@ namespace Umbraco.Core.PropertyEditors { + /// /// Marks a class that represents a data editor. /// diff --git a/src/Umbraco.Web/PropertyEditors/IPropertyCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/IPropertyCompressionOptions.cs similarity index 84% rename from src/Umbraco.Web/PropertyEditors/IPropertyCompressionOptions.cs rename to src/Umbraco.Core/PropertyEditors/IPropertyCompressionOptions.cs index 70f3e7c6f0..f5aaf3dc57 100644 --- a/src/Umbraco.Web/PropertyEditors/IPropertyCompressionOptions.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyCompressionOptions.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Web.PropertyEditors +namespace Umbraco.Core.PropertyEditors { /// /// Determines if a property type's value should be compressed diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 4a25863158..444ef1a464 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -132,8 +132,8 @@ - - + + @@ -152,6 +152,9 @@ + + + diff --git a/src/Umbraco.Web/PropertyEditors/ComplexEditorPropertyCompressionOptions.cs b/src/Umbraco.Web/PropertyEditors/ComplexEditorPropertyCompressionOptions.cs deleted file mode 100644 index f4776f652d..0000000000 --- a/src/Umbraco.Web/PropertyEditors/ComplexEditorPropertyCompressionOptions.cs +++ /dev/null @@ -1,48 +0,0 @@ -using K4os.Compression.LZ4; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Services; - -namespace Umbraco.Web.PropertyEditors -{ - - /// - /// Ensures all property types that have an editor storing a complex value are compressed - /// - public class ComplexEditorPropertyCompressionOptions : IPropertyCompressionOptions - { - private readonly IContentTypeService _contentTypeService; - private readonly PropertyEditorCollection _propertyEditors; - private readonly ConcurrentDictionary<(int, string), string> _editorValueTypes = new ConcurrentDictionary<(int, string), string>(); - - public ComplexEditorPropertyCompressionOptions(IContentTypeService contentTypeService, PropertyEditorCollection propertyEditors) - { - _contentTypeService = contentTypeService; - _propertyEditors = propertyEditors; - } - - public bool IsCompressed(int contentTypeId, string alias) - { - var valueType = _editorValueTypes.GetOrAdd((contentTypeId, alias), x => - { - var ct = _contentTypeService.Get(contentTypeId); - if (ct == null) return null; - - var propertyType = ct.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == alias); - if (propertyType == null) return null; - - if (!_propertyEditors.TryGet(propertyType.PropertyEditorAlias, out var propertyEditor)) return null; - - var editor = propertyEditor.GetValueEditor(); - if (editor == null) return null; - - return editor.ValueType; - }); - - return valueType == ValueTypes.Json || valueType == ValueTypes.Xml || valueType == ValueTypes.Text; - } - } -} diff --git a/src/Umbraco.Web/PropertyEditors/NoopPropertyCompressionOptions.cs b/src/Umbraco.Web/PropertyEditors/NoopPropertyCompressionOptions.cs index 638306d7ca..6f626938bc 100644 --- a/src/Umbraco.Web/PropertyEditors/NoopPropertyCompressionOptions.cs +++ b/src/Umbraco.Web/PropertyEditors/NoopPropertyCompressionOptions.cs @@ -1,4 +1,6 @@ -namespace Umbraco.Web.PropertyEditors +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors { /// /// Disables all compression for all properties diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index a52fde2f0a..0b3003d7ec 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -129,7 +129,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. foreach (var row in scope.Database.QueryPaged(PageSize, sql, sqlCount)) + { yield return CreateContentNodeKit(row); + } } public IEnumerable GetBranchContentSources(IScope scope, int id) @@ -145,7 +147,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + { yield return CreateContentNodeKit(row); + } } public IEnumerable GetTypeContentSources(IScope scope, IEnumerable ids) @@ -161,7 +165,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + { yield return CreateContentNodeKit(row); + } } private Sql MediaSourcesSelect(IScope scope, Func, Sql> joins = null) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index 0ea2b96fbe..4965935fbf 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; +using Umbraco.Core.PropertyEditors; using Umbraco.Web.PropertyEditors; namespace Umbraco.Web.PublishedCache.NuCache.DataSource diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs index c5109db027..baa96a4a2e 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs @@ -3,6 +3,7 @@ using System.Configuration; using System.Linq; using Umbraco.Core; using Umbraco.Core.Composing; +using Umbraco.Core.PropertyEditors; using Umbraco.Web.PropertyEditors; using Umbraco.Web.PublishedCache.NuCache.DataSource; @@ -24,7 +25,7 @@ namespace Umbraco.Web.PublishedCache.NuCache else { composition.RegisterUnique(); - composition.RegisterUnique(); + composition.RegisterUnique(); } composition.RegisterUnique(factory => new ContentDataSerializer(new DictionaryOfPropertyDataSerializer())); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index e670cb75f6..41dfdd7a64 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -387,10 +387,9 @@ namespace Umbraco.Web.PublishedCache.NuCache // contentStore is wlocked (1 thread) // content (and types) are read-locked - var contentTypes = _serviceContext.ContentTypeService.GetAll() - .Select(x => _publishedContentTypeFactory.CreateContentType(x)); + var contentTypes = _serviceContext.ContentTypeService.GetAll().ToList(); - _contentStore.SetAllContentTypesLocked(contentTypes); + _contentStore.SetAllContentTypesLocked(contentTypes.Select(x => _publishedContentTypeFactory.CreateContentType(x))); using (_logger.TraceDuration("Loading content from database")) { diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 51f4d838af..25478bf626 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -262,7 +262,6 @@ - @@ -271,7 +270,6 @@ - From 780b2e573b991b7a3b59f7f8b56b7cfa19a6d4ed Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 25 Sep 2020 00:27:46 +1000 Subject: [PATCH 040/289] Fixes a startup issue that doesn't bubble the underlying boot failed exception if the container fails. --- src/Umbraco.Core/Composing/Current.cs | 13 ++++++++++++- src/Umbraco.Core/Runtime/CoreRuntime.cs | 8 +++++++- src/Umbraco.Web/Composing/ModuleInjector.cs | 2 +- src/Umbraco.Web/Runtime/WebRuntime.cs | 16 ++++++++++------ 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Composing/Current.cs b/src/Umbraco.Core/Composing/Current.cs index a06f09baf6..a4ccd59f9d 100644 --- a/src/Umbraco.Core/Composing/Current.cs +++ b/src/Umbraco.Core/Composing/Current.cs @@ -30,6 +30,7 @@ namespace Umbraco.Core.Composing public static class Current { private static IFactory _factory; + private static IRuntimeState _state; // TODO: get rid of these oddities // we don't want Umbraco tests to die because the container has not been properly initialized, @@ -125,7 +126,17 @@ namespace Umbraco.Core.Composing ?? new ProfilingLogger(Logger, Profiler); public static IRuntimeState RuntimeState - => Factory.GetInstance(); + { + get + { + return _state ?? Factory.GetInstance(); + } + internal set + { + // this is only used when the boot entirely fails, we need to manually set this so we can report + _state = value; + } + } public static TypeLoader TypeLoader => Factory.GetInstance(); diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index b852aff2ff..75cbaa6913 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -210,7 +210,13 @@ namespace Umbraco.Core.Runtime { _factory = Current.Factory = composition?.CreateFactory(); } - catch { /* yea */ } + catch + { + // In this case we are basically dead, we do not have a factory but we need + // to report on the state so we need to manually set that, this is the only time + // we ever do this. + Current.RuntimeState = _state; + } } Debugger.Break(); diff --git a/src/Umbraco.Web/Composing/ModuleInjector.cs b/src/Umbraco.Web/Composing/ModuleInjector.cs index 57ef766dea..ccc8b87719 100644 --- a/src/Umbraco.Web/Composing/ModuleInjector.cs +++ b/src/Umbraco.Web/Composing/ModuleInjector.cs @@ -30,7 +30,7 @@ namespace Umbraco.Web.Composing try { - runtimeState = Current.Factory.GetInstance(); + runtimeState = Current.RuntimeState; } catch { /* don't make it worse */ } diff --git a/src/Umbraco.Web/Runtime/WebRuntime.cs b/src/Umbraco.Web/Runtime/WebRuntime.cs index ffcd2343ed..e32f2f824f 100644 --- a/src/Umbraco.Web/Runtime/WebRuntime.cs +++ b/src/Umbraco.Web/Runtime/WebRuntime.cs @@ -54,12 +54,16 @@ namespace Umbraco.Web.Runtime var factory = base.Boot(register); - // now (and only now) is the time to switch over to perWebRequest scopes. - // up until that point we may not have a request, and scoped services would - // fail to resolve - but we run Initialize within a factory scope - and then, - // here, we switch the factory to bind scopes to requests - factory.EnablePerWebRequestScope(); - + // factory can be null if part of the boot process fails + if (factory != null) + { + // now (and only now) is the time to switch over to perWebRequest scopes. + // up until that point we may not have a request, and scoped services would + // fail to resolve - but we run Initialize within a factory scope - and then, + // here, we switch the factory to bind scopes to requests + factory.EnablePerWebRequestScope(); + } + return factory; } From 67a9b5bb9770908f431760f88783b00d64839fae Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 25 Sep 2020 00:32:11 +1000 Subject: [PATCH 041/289] Refactors the serialization of the content data that is stored in the nucache table. This had to change because we need to resolve content type data in order to check if the property should be compressed and we cannot do that data lookup while the data is being processed since we get an open data reader exception. This is fixed now by using a serializer factory instead so the Create method can do any initialization needed prior to running any serialization operation. Renames a few things so we dont have ContentNested (whatever that meant ) --- ...StoragePropertyEditorCompressionOptions.cs | 45 +++++----- .../IPropertyCompressionOptions.cs | 8 +- .../ContentSerializationTests.cs | 20 ++--- .../PublishedContent/NuCacheChildrenTests.cs | 6 +- .../PublishedContent/NuCacheTests.cs | 6 +- .../Scoping/ScopedNuCacheTests.cs | 6 +- .../ContentTypeServiceVariantsTests.cs | 6 +- .../Editors/NuCacheStatusController.cs | 5 ++ .../BlockListPropertyEditor.cs | 1 + .../PropertyEditors/GridPropertyEditor.cs | 1 + .../PropertyEditors/MarkdownPropertyEditor.cs | 1 + .../NestedContentPropertyEditor.cs | 1 + .../NoopPropertyCompressionOptions.cs | 12 --- .../PropertyEditors/RichTextPropertyEditor.cs | 1 + .../PropertyEditors/TextAreaPropertyEditor.cs | 1 + .../PublishedCache/NuCache/ContentNodeKit.cs | 1 + ...NestedData.cs => ContentCacheDataModel.cs} | 4 +- .../ContentCacheDataSerializationResult.cs | 47 +++++++++++ .../ContentCacheDataSerializerEntityType.cs | 13 +++ .../NuCache/DataSource/DatabaseDataSource.cs | 82 +++++++++++-------- .../DataSource/IContentCacheDataSerializer.cs | 18 ++++ .../IContentCacheDataSerializerFactory.cs | 16 ++++ .../IContentNestedDataSerializer.cs | 22 ----- .../JsonContentNestedDataSerializer.cs | 21 ++--- .../JsonContentNestedDataSerializerFactory.cs | 10 +++ .../MsgPackContentNestedDataSerializer.cs | 77 +++++++++-------- ...gPackContentNestedDataSerializerFactory.cs | 62 ++++++++++++++ .../PublishedCache/NuCache/NuCacheComposer.cs | 11 +-- .../NuCache/PublishedSnapshotService.cs | 68 ++++++++------- src/Umbraco.Web/Umbraco.Web.csproj | 10 ++- 30 files changed, 377 insertions(+), 205 deletions(-) delete mode 100644 src/Umbraco.Web/PropertyEditors/NoopPropertyCompressionOptions.cs rename src/Umbraco.Web/PublishedCache/NuCache/DataSource/{ContentNestedData.cs => ContentCacheDataModel.cs} (93%) create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataSerializationResult.cs create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataSerializerEntityType.cs create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializerFactory.cs delete mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializerFactory.cs create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs diff --git a/src/Umbraco.Core/PropertyEditors/CompressedStoragePropertyEditorCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/CompressedStoragePropertyEditorCompressionOptions.cs index 15795bb61c..a99452a5b1 100644 --- a/src/Umbraco.Core/PropertyEditors/CompressedStoragePropertyEditorCompressionOptions.cs +++ b/src/Umbraco.Core/PropertyEditors/CompressedStoragePropertyEditorCompressionOptions.cs @@ -1,44 +1,47 @@ using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; -using Umbraco.Core.Services; +using Umbraco.Core.Models; namespace Umbraco.Core.PropertyEditors { /// - /// Ensures all property types that have an editor storing a complex value are compressed + /// Ensures all property types that have a property editor attributed with use data compression /// - public class CompressedStoragePropertyEditorCompressionOptions : IPropertyCompressionOptions + internal class CompressedStoragePropertyEditorCompressionOptions : IPropertyCompressionOptions { - private readonly IContentTypeService _contentTypeService; + private readonly IReadOnlyDictionary _contentTypes; private readonly PropertyEditorCollection _propertyEditors; - private readonly ConcurrentDictionary<(int, string), string> _editorValueTypes = new ConcurrentDictionary<(int, string), string>(); + private readonly ConcurrentDictionary<(int, string), CompressedStorageAttribute> _compressedStoragePropertyEditorCache; - public CompressedStoragePropertyEditorCompressionOptions(PropertyEditorCollection propertyEditors) + public CompressedStoragePropertyEditorCompressionOptions( + IReadOnlyDictionary contentTypes, + PropertyEditorCollection propertyEditors, + ConcurrentDictionary<(int, string), CompressedStorageAttribute> compressedStoragePropertyEditorCache) { - _propertyEditors = propertyEditors; + _contentTypes = contentTypes ?? throw new System.ArgumentNullException(nameof(contentTypes)); + _propertyEditors = propertyEditors ?? throw new System.ArgumentNullException(nameof(propertyEditors)); + _compressedStoragePropertyEditorCache = compressedStoragePropertyEditorCache; } public bool IsCompressed(int contentTypeId, string alias) { - return false; - //var valueType = _editorValueTypes.GetOrAdd((contentTypeId, alias), x => - //{ - // var ct = _contentTypeService.Get(contentTypeId); - // if (ct == null) return null; + var compressedStorage = _compressedStoragePropertyEditorCache.GetOrAdd((contentTypeId, alias), x => + { + if (!_contentTypes.TryGetValue(contentTypeId, out var ct)) + return null; - // var propertyType = ct.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == alias); - // if (propertyType == null) return null; + var propertyType = ct.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == alias); + if (propertyType == null) return null; - // if (!_propertyEditors.TryGet(propertyType.PropertyEditorAlias, out var propertyEditor)) return null; + if (!_propertyEditors.TryGet(propertyType.PropertyEditorAlias, out var propertyEditor)) return null; - // var editor = propertyEditor.GetValueEditor(); - // if (editor == null) return null; + var attribute = propertyEditor.GetType().GetCustomAttribute(true); + return attribute; + }); - // return editor.ValueType; - //}); - - //return valueType == ValueTypes.Json || valueType == ValueTypes.Xml || valueType == ValueTypes.Text; + return compressedStorage?.IsCompressed ?? false; } } } diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/IPropertyCompressionOptions.cs index f5aaf3dc57..d1add38f19 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyCompressionOptions.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyCompressionOptions.cs @@ -1,10 +1,12 @@ -namespace Umbraco.Core.PropertyEditors +using Umbraco.Core.Models; + +namespace Umbraco.Core.PropertyEditors { /// /// Determines if a property type's value should be compressed /// public interface IPropertyCompressionOptions - { - bool IsCompressed(int contentTypeId, string alias); + { + bool IsCompressed(int contentTypeId, string propertyTypeAlias); } } diff --git a/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs b/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs index c85973f4b0..4be80083b8 100644 --- a/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs +++ b/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs @@ -1,10 +1,8 @@ -using NUnit.Framework; +using Moq; +using NUnit.Framework; using System; -using System.Collections; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Umbraco.Core.PropertyEditors; using Umbraco.Web.PublishedCache.NuCache.DataSource; namespace Umbraco.Tests.PublishedContent @@ -16,10 +14,10 @@ namespace Umbraco.Tests.PublishedContent public void Ensure_Same_Results() { var jsonSerializer = new JsonContentNestedDataSerializer(); - var msgPackSerializer = new MsgPackContentNestedDataSerializer(); + var msgPackSerializer = new MsgPackContentNestedDataSerializer(Mock.Of()); var now = DateTime.Now; - var content = new ContentNestedData + var content = new ContentCacheDataModel { PropertyData = new Dictionary { @@ -55,14 +53,14 @@ namespace Umbraco.Tests.PublishedContent UrlSegment = "home" }; - var json = jsonSerializer.Serialize(1, content); - var msgPack = msgPackSerializer.Serialize(1, content); + var json = jsonSerializer.Serialize(1, content).StringData; + var msgPack = msgPackSerializer.Serialize(1, content).ByteData; Console.WriteLine(json); Console.WriteLine(msgPackSerializer.ToJson(msgPack)); - var jsonContent = jsonSerializer.Deserialize(1, json); - var msgPackContent = msgPackSerializer.Deserialize(1, msgPack); + var jsonContent = jsonSerializer.Deserialize(1, json, null); + var msgPackContent = msgPackSerializer.Deserialize(1, null, msgPack); CollectionAssert.AreEqual(jsonContent.CultureData.Keys, msgPackContent.CultureData.Keys); diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index fef096498c..afba2dcc4f 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -37,7 +37,7 @@ namespace Umbraco.Tests.PublishedContent private ContentType _contentTypeInvariant; private ContentType _contentTypeVariant; private TestDataSource _source; - private IContentNestedDataSerializer _contentNestedDataSerializer; + private IContentCacheDataSerializerFactory _contentNestedDataSerializerFactory; [TearDown] public void Teardown() @@ -135,7 +135,7 @@ namespace Umbraco.Tests.PublishedContent // create a data source for NuCache _source = new TestDataSource(kits()); - _contentNestedDataSerializer = new JsonContentNestedDataSerializer(); + _contentNestedDataSerializerFactory = new JsonContentNestedDataSerializerFactory(); // at last, create the complete NuCache snapshot service! var options = new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }; @@ -158,7 +158,7 @@ namespace Umbraco.Tests.PublishedContent Mock.Of(), Mock.Of(), new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), - _contentNestedDataSerializer); + _contentNestedDataSerializerFactory); // invariant is the current default _variationAccesor.VariationContext = new VariationContext(); diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs index 792ccc8529..eee3500495 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs @@ -33,7 +33,7 @@ namespace Umbraco.Tests.PublishedContent { private IPublishedSnapshotService _snapshotService; private IVariationContextAccessor _variationAccesor; - private IContentNestedDataSerializer _contentNestedDataSerializer; + private IContentCacheDataSerializerFactory _contentNestedDataSerializerFactory; private ContentType _contentType; private PropertyType _propertyType; @@ -115,7 +115,7 @@ namespace Umbraco.Tests.PublishedContent // create a data source for NuCache var dataSource = new TestDataSource(kit); - _contentNestedDataSerializer = new JsonContentNestedDataSerializer(); + _contentNestedDataSerializerFactory = new JsonContentNestedDataSerializerFactory(); var runtime = Mock.Of(); Mock.Get(runtime).Setup(x => x.Level).Returns(RuntimeLevel.Run); @@ -204,7 +204,7 @@ namespace Umbraco.Tests.PublishedContent Mock.Of(), Mock.Of(), new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), - _contentNestedDataSerializer); + _contentNestedDataSerializerFactory); // invariant is the current default _variationAccesor.VariationContext = new VariationContext(); diff --git a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs index 5f72947382..be10db3a9d 100644 --- a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs @@ -82,7 +82,7 @@ namespace Umbraco.Tests.Scoping var mediaRepository = Mock.Of(); var memberRepository = Mock.Of(); - var nestedContentDataSerializer = new JsonContentNestedDataSerializer(); + var nestedContentDataSerializerFactory = new JsonContentNestedDataSerializerFactory(); return new PublishedSnapshotService( options, null, @@ -96,12 +96,12 @@ namespace Umbraco.Tests.Scoping ScopeProvider, documentRepository, mediaRepository, memberRepository, DefaultCultureAccessor, - new DatabaseDataSource(nestedContentDataSerializer), + new DatabaseDataSource(nestedContentDataSerializerFactory), Factory.GetInstance(), Factory.GetInstance(), Mock.Of(), new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), - nestedContentDataSerializer); + nestedContentDataSerializerFactory); } protected UmbracoContext GetUmbracoContextNu(string url, int templateId = 1234, RouteData routeData = null, bool setSingleton = false, IUmbracoSettingsSection umbracoSettings = null, IEnumerable urlProviders = null) diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index 938b14c3a9..aaad60f7e9 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -53,7 +53,7 @@ namespace Umbraco.Tests.Services var mediaRepository = Mock.Of(); var memberRepository = Mock.Of(); - var nestedContentDataSerializer = new JsonContentNestedDataSerializer(); + var nestedContentDataSerializerFactory = new JsonContentNestedDataSerializerFactory(); return new PublishedSnapshotService( options, @@ -68,12 +68,12 @@ namespace Umbraco.Tests.Services ScopeProvider, documentRepository, mediaRepository, memberRepository, DefaultCultureAccessor, - new DatabaseDataSource(nestedContentDataSerializer), + new DatabaseDataSource(nestedContentDataSerializerFactory), Factory.GetInstance(), Factory.GetInstance(), Mock.Of(), new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), - nestedContentDataSerializer); + nestedContentDataSerializerFactory); } public class LocalServerMessenger : ServerMessengerBase diff --git a/src/Umbraco.Web/Editors/NuCacheStatusController.cs b/src/Umbraco.Web/Editors/NuCacheStatusController.cs index 86dbdd4e01..589c763363 100644 --- a/src/Umbraco.Web/Editors/NuCacheStatusController.cs +++ b/src/Umbraco.Web/Editors/NuCacheStatusController.cs @@ -35,6 +35,11 @@ namespace Umbraco.Web.Editors service.RebuildContentDbCache(); service.RebuildMediaDbCache(); service.RebuildMemberDbCache(); + + // TODO: Shouldn't this just be ?? + // service.Rebuild(); + + return service.GetStatus(); } diff --git a/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs index 42023382f1..2c1221e99e 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs @@ -13,6 +13,7 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a block list property editor. /// + [CompressedStorage] [DataEditor( Constants.PropertyEditors.Aliases.BlockList, "Block List", diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index 862837381a..7ce312c516 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -17,6 +17,7 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a grid property and parameter editor. /// + [CompressedStorage] [DataEditor( Constants.PropertyEditors.Aliases.Grid, "Grid layout", diff --git a/src/Umbraco.Web/PropertyEditors/MarkdownPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MarkdownPropertyEditor.cs index 2d66da5461..6ce66aaa00 100644 --- a/src/Umbraco.Web/PropertyEditors/MarkdownPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MarkdownPropertyEditor.cs @@ -7,6 +7,7 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a markdown editor. /// + [CompressedStorage] [DataEditor( Constants.PropertyEditors.Aliases.MarkdownEditor, "Markdown editor", diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 8f25449f99..ffe0051607 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -22,6 +22,7 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a nested content property editor. /// + [CompressedStorage] [DataEditor( Constants.PropertyEditors.Aliases.NestedContent, "Nested Content", diff --git a/src/Umbraco.Web/PropertyEditors/NoopPropertyCompressionOptions.cs b/src/Umbraco.Web/PropertyEditors/NoopPropertyCompressionOptions.cs deleted file mode 100644 index 6f626938bc..0000000000 --- a/src/Umbraco.Web/PropertyEditors/NoopPropertyCompressionOptions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors -{ - /// - /// Disables all compression for all properties - /// - internal class NoopPropertyCompressionOptions : IPropertyCompressionOptions - { - public bool IsCompressed(int contentTypeId, string alias) => false; - } -} diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 42777f11ad..7c7a358bf3 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -17,6 +17,7 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a rich text property editor. /// + [CompressedStorage] [DataEditor( Constants.PropertyEditors.Aliases.TinyMce, "Rich Text Editor", diff --git a/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs index c7bc2efbda..878330820a 100644 --- a/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs @@ -7,6 +7,7 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a textarea property and parameter editor. /// + [CompressedStorage] [DataEditor( Constants.PropertyEditors.Aliases.TextArea, EditorType.PropertyValue | EditorType.MacroParameter, diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs index 61fb5c12a3..43aa9a14d7 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs @@ -15,6 +15,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public bool IsNull => ContentTypeId < 0; + public static ContentNodeKit Empty { get; } = new ContentNodeKit(); public static ContentNodeKit Null { get; } = new ContentNodeKit { ContentTypeId = -1 }; public void Build( diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentNestedData.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataModel.cs similarity index 93% rename from src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentNestedData.cs rename to src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataModel.cs index 1a49aaaf62..40acdfdb55 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentNestedData.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataModel.cs @@ -7,10 +7,10 @@ using Umbraco.Core.Serialization; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { /// - /// The content item 1:M data that is serialized to JSON + /// The content model stored in the content cache database table serialized as JSON /// [DataContract] // NOTE: Use DataContract annotations here to control how MessagePack serializes/deserializes the data to use INT keys - public class ContentNestedData + public class ContentCacheDataModel { // TODO: We don't want to allocate empty arrays //dont serialize empty properties diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataSerializationResult.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataSerializationResult.cs new file mode 100644 index 0000000000..7cd388d712 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataSerializationResult.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + public struct ContentCacheDataSerializationResult : IEquatable + { + public ContentCacheDataSerializationResult(string stringData, byte[] byteData) + { + StringData = stringData; + ByteData = byteData; + } + + public string StringData { get; } + public byte[] ByteData { get; } + + public override bool Equals(object obj) + { + return obj is ContentCacheDataSerializationResult result && Equals(result); + } + + public bool Equals(ContentCacheDataSerializationResult other) + { + return StringData == other.StringData && + EqualityComparer.Default.Equals(ByteData, other.ByteData); + } + + public override int GetHashCode() + { + var hashCode = 1910544615; + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(StringData); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(ByteData); + return hashCode; + } + + public static bool operator ==(ContentCacheDataSerializationResult left, ContentCacheDataSerializationResult right) + { + return left.Equals(right); + } + + public static bool operator !=(ContentCacheDataSerializationResult left, ContentCacheDataSerializationResult right) + { + return !(left == right); + } + } + +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataSerializerEntityType.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataSerializerEntityType.cs new file mode 100644 index 0000000000..e5b15f8dce --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataSerializerEntityType.cs @@ -0,0 +1,13 @@ +using System; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + [Flags] + public enum ContentCacheDataSerializerEntityType + { + Document = 1, + Media = 2, + Member = 4 + } + +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index 0b3003d7ec..e60fd6623e 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -2,14 +2,12 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using Newtonsoft.Json; using NPoco; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Scoping; -using Umbraco.Core.Serialization; using Umbraco.Web.Composing; using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; @@ -21,11 +19,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource internal class DatabaseDataSource : IDataSource { private const int PageSize = 500; - private readonly IContentNestedDataSerializer _contentNestedDataSerializer; + private readonly IContentCacheDataSerializerFactory _contentCacheDataSerializerFactory; - public DatabaseDataSource(IContentNestedDataSerializer contentNestedDataSerializer) + public DatabaseDataSource(IContentCacheDataSerializerFactory contentCacheDataSerializerFactory) { - _contentNestedDataSerializer = contentNestedDataSerializer; + _contentCacheDataSerializerFactory = contentCacheDataSerializerFactory; } // we want arrays, we want them all loaded, not an enumerable @@ -110,7 +108,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); var dto = scope.Database.Fetch(sql).FirstOrDefault(); - return dto == null ? new ContentNodeKit() : CreateContentNodeKit(dto); + + if (dto == null) return ContentNodeKit.Empty; + + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); + return CreateContentNodeKit(dto, serializer); } public IEnumerable GetAllContentSources(IScope scope) @@ -125,12 +127,14 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed); var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. foreach (var row in scope.Database.QueryPaged(PageSize, sql, sqlCount)) { - yield return CreateContentNodeKit(row); + yield return CreateContentNodeKit(row, serializer); } } @@ -143,12 +147,14 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .Where(x => x.NodeId == id, "x") .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. foreach (var row in scope.Database.QueryPaged(PageSize, sql)) { - yield return CreateContentNodeKit(row); + yield return CreateContentNodeKit(row, serializer); } } @@ -161,12 +167,14 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .WhereIn(x => x.ContentTypeId, ids) .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. foreach (var row in scope.Database.QueryPaged(PageSize, sql)) { - yield return CreateContentNodeKit(row); + yield return CreateContentNodeKit(row, serializer); } } @@ -201,7 +209,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); var dto = scope.Database.Fetch(sql).FirstOrDefault(); - return dto == null ? new ContentNodeKit() : CreateMediaNodeKit(dto); + + if (dto == null) return ContentNodeKit.Empty; + + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); + return CreateMediaNodeKit(dto, serializer); } public IEnumerable GetAllMediaSources(IScope scope) @@ -210,11 +222,15 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. foreach (var row in scope.Database.QueryPaged(PageSize, sql)) - yield return CreateMediaNodeKit(row); + { + yield return CreateMediaNodeKit(row, serializer); + } } public IEnumerable GetBranchMediaSources(IScope scope, int id) @@ -226,11 +242,15 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .Where(x => x.NodeId == id, "x") .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. foreach (var row in scope.Database.QueryPaged(PageSize, sql)) - yield return CreateMediaNodeKit(row); + { + yield return CreateMediaNodeKit(row, serializer); + } } public IEnumerable GetTypeMediaSources(IScope scope, IEnumerable ids) @@ -242,14 +262,18 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .WhereIn(x => x.ContentTypeId, ids) .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. foreach (var row in scope.Database.QueryPaged(PageSize, sql)) - yield return CreateMediaNodeKit(row); + { + yield return CreateMediaNodeKit(row, serializer); + } } - private ContentNodeKit CreateContentNodeKit(ContentSourceDto dto) + private ContentNodeKit CreateContentNodeKit(ContentSourceDto dto, IContentCacheDataSerializer serializer) { ContentData d = null; ContentData p = null; @@ -264,9 +288,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } else { - var nested = _contentNestedDataSerializer is IContentNestedDataByteSerializer byteSerializer - ? byteSerializer.DeserializeBytes(dto.ContentTypeId, dto.EditDataRaw) - : _contentNestedDataSerializer.Deserialize(dto.ContentTypeId, dto.EditData); + var deserializedContent = serializer.Deserialize(dto.ContentTypeId, dto.EditData, dto.EditDataRaw); d = new ContentData { @@ -276,9 +298,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource VersionId = dto.VersionId, VersionDate = dto.EditVersionDate, WriterId = dto.EditWriterId, - Properties = nested.PropertyData, // TODO: We don't want to allocate empty arrays - CultureInfos = nested.CultureData, - UrlSegment = nested.UrlSegment + Properties = deserializedContent.PropertyData, // TODO: We don't want to allocate empty arrays + CultureInfos = deserializedContent.CultureData, + UrlSegment = deserializedContent.UrlSegment }; } } @@ -293,21 +315,19 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } else { - var nested = _contentNestedDataSerializer is IContentNestedDataByteSerializer byteSerializer - ? byteSerializer.DeserializeBytes(dto.ContentTypeId, dto.PubDataRaw) - : _contentNestedDataSerializer.Deserialize(dto.ContentTypeId, dto.PubData); + var deserializedContent = serializer.Deserialize(dto.ContentTypeId, dto.PubData, dto.PubDataRaw); p = new ContentData { Name = dto.PubName, - UrlSegment = nested.UrlSegment, + UrlSegment = deserializedContent.UrlSegment, Published = true, TemplateId = dto.PubTemplateId, VersionId = dto.VersionId, VersionDate = dto.PubVersionDate, WriterId = dto.PubWriterId, - Properties = nested.PropertyData, // TODO: We don't want to allocate empty arrays - CultureInfos = nested.CultureData + Properties = deserializedContent.PropertyData, // TODO: We don't want to allocate empty arrays + CultureInfos = deserializedContent.CultureData }; } } @@ -326,14 +346,12 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return s; } - private ContentNodeKit CreateMediaNodeKit(ContentSourceDto dto) + private ContentNodeKit CreateMediaNodeKit(ContentSourceDto dto, IContentCacheDataSerializer serializer) { if (dto.EditData == null && dto.EditDataRaw == null) throw new InvalidOperationException("No data for media " + dto.Id); - var nested = _contentNestedDataSerializer is IContentNestedDataByteSerializer byteSerializer - ? byteSerializer.DeserializeBytes(dto.ContentTypeId, dto.EditDataRaw) - : _contentNestedDataSerializer.Deserialize(dto.ContentTypeId, dto.EditData); + var deserializedMedia = serializer.Deserialize(dto.ContentTypeId, dto.EditData, dto.EditDataRaw); var p = new ContentData { @@ -343,8 +361,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource VersionId = dto.VersionId, VersionDate = dto.EditVersionDate, WriterId = dto.CreatorId, // what-else? - Properties = nested.PropertyData, // TODO: We don't want to allocate empty arrays - CultureInfos = nested.CultureData + Properties = deserializedMedia.PropertyData, // TODO: We don't want to allocate empty arrays + CultureInfos = deserializedMedia.CultureData }; var n = new ContentNode(dto.Id, dto.Uid, diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs new file mode 100644 index 0000000000..87ac5af91e --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs @@ -0,0 +1,18 @@ +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + + /// + /// Serializes/Deserializes document to the SQL Database as a string + /// + /// + /// Resolved from the . This cannot be resolved from DI. + /// + public interface IContentCacheDataSerializer + { + ContentCacheDataModel Deserialize(int contentTypeId, string stringData, byte[] byteData); + ContentCacheDataSerializationResult Serialize(int contentTypeId, ContentCacheDataModel model); + } + +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializerFactory.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializerFactory.cs new file mode 100644 index 0000000000..14dfd7dc5b --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializerFactory.cs @@ -0,0 +1,16 @@ +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + public interface IContentCacheDataSerializerFactory + { + /// + /// Gets or creates a new instance of + /// + /// + /// + /// This method may return the same instance, however this depends on the state of the application and if any underlying data has changed. + /// This method may also be used to initialize anything before a serialization/deserialization session occurs. + /// + IContentCacheDataSerializer Create(ContentCacheDataSerializerEntityType types); + } + +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs deleted file mode 100644 index 09933d735d..0000000000 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentNestedDataSerializer.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Umbraco.Web.PublishedCache.NuCache.DataSource -{ - // TODO: We need better names if possible, not sure why the class is called ContentNested in the first place - - /// - /// Serializes/Deserializes document to the SQL Database as bytes - /// - public interface IContentNestedDataByteSerializer : IContentNestedDataSerializer - { - ContentNestedData DeserializeBytes(int contentTypeId, byte[] data); - byte[] SerializeBytes(int contentTypeId, ContentNestedData nestedData); - } - - /// - /// Serializes/Deserializes document to the SQL Database as a string - /// - public interface IContentNestedDataSerializer - { - ContentNestedData Deserialize(int contentTypeId, string data); - string Serialize(int contentTypeId, ContentNestedData nestedData); - } -} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs index d4f11591c1..2fa892a5e6 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs @@ -1,18 +1,18 @@ using Newtonsoft.Json; +using System; using System.Collections.Generic; -using System.Linq; -using System.Reflection.Emit; -using System.Text; -using System.Threading.Tasks; using Umbraco.Core.Serialization; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { - internal class JsonContentNestedDataSerializer : IContentNestedDataSerializer + public class JsonContentNestedDataSerializer : IContentCacheDataSerializer { - public ContentNestedData Deserialize(int contentTypeId, string data) + public ContentCacheDataModel Deserialize(int contentTypeId, string stringData, byte[] byteData) { + if (byteData != null) + throw new NotSupportedException($"{typeof(JsonContentNestedDataSerializer)} does not support byte[] serialization"); + // by default JsonConvert will deserialize our numeric values as Int64 // which is bad, because they were Int32 in the database - take care @@ -24,18 +24,19 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource DateParseHandling = DateParseHandling.DateTime, DateFormatHandling = DateFormatHandling.IsoDateFormat, DateTimeZoneHandling = DateTimeZoneHandling.Utc, - DateFormatString = "o" + DateFormatString = "o" }; - return JsonConvert.DeserializeObject(data, settings); + return JsonConvert.DeserializeObject(stringData, settings); } - public string Serialize(int contentTypeId, ContentNestedData nestedData) + public ContentCacheDataSerializationResult Serialize(int contentTypeId, ContentCacheDataModel model) { // note that numeric values (which are Int32) are serialized without their // type (eg "value":1234) and JsonConvert by default deserializes them as Int64 - return JsonConvert.SerializeObject(nestedData); + var json = JsonConvert.SerializeObject(model); + return new ContentCacheDataSerializationResult(json, null); } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializerFactory.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializerFactory.cs new file mode 100644 index 0000000000..e857eb8bf5 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializerFactory.cs @@ -0,0 +1,10 @@ +using System; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + internal class JsonContentNestedDataSerializerFactory : IContentCacheDataSerializerFactory + { + private Lazy _serializer = new Lazy(); + public IContentCacheDataSerializer Create(ContentCacheDataSerializerEntityType types) => _serializer.Value; + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index 4965935fbf..aad337b236 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -1,29 +1,27 @@ using K4os.Compression.LZ4; using MessagePack; -using MessagePack.Formatters; using MessagePack.Resolvers; -using NPoco.FluentMappings; using System; -using System.Collections.Generic; using System.Linq; using System.Text; using Umbraco.Core.PropertyEditors; -using Umbraco.Web.PropertyEditors; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { + /// - /// Serializes/Deserializes document to the SQL Database as bytes using MessagePack + /// Serializes/Deserializes document to the SQL Database as bytes using MessagePack /// - internal class MsgPackContentNestedDataSerializer : IContentNestedDataByteSerializer + public class MsgPackContentNestedDataSerializer : IContentCacheDataSerializer { - private MessagePackSerializerOptions _options; + private readonly MessagePackSerializerOptions _options; private readonly IPropertyCompressionOptions _propertyOptions; - public MsgPackContentNestedDataSerializer(IPropertyCompressionOptions propertyOptions = null) + public MsgPackContentNestedDataSerializer(IPropertyCompressionOptions propertyOptions) { - var defaultOptions = ContractlessStandardResolver.Options; + _propertyOptions = propertyOptions ?? throw new ArgumentNullException(nameof(propertyOptions)); + var defaultOptions = ContractlessStandardResolver.Options; var resolver = CompositeResolver.Create( // TODO: We want to be able to intern the strings for aliases when deserializing like we do for Newtonsoft but I'm unsure exactly how @@ -39,52 +37,51 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource _options = defaultOptions .WithResolver(resolver) - .WithCompression(MessagePackCompression.Lz4BlockArray); - _propertyOptions = propertyOptions ?? new NoopPropertyCompressionOptions(); + .WithCompression(MessagePackCompression.Lz4BlockArray); } - public string ToJson(string serialized) + public string ToJson(byte[] bin) { - var bin = Convert.FromBase64String(serialized); var json = MessagePackSerializer.ConvertToJson(bin, _options); return json; } - public ContentNestedData Deserialize(int contentTypeId, string data) + public ContentCacheDataModel Deserialize(int contentTypeId, string stringData, byte[] byteData) { - var bin = Convert.FromBase64String(data); - var nestedData = MessagePackSerializer.Deserialize(bin, _options); - Expand(contentTypeId, nestedData); - return nestedData; + if (stringData != null) + { + // NOTE: We don't really support strings but it's possible if manually used (i.e. tests) + var bin = Convert.FromBase64String(stringData); + var content = MessagePackSerializer.Deserialize(bin, _options); + Expand(contentTypeId, content); + return content; + } + else if (byteData != null) + { + var content = MessagePackSerializer.Deserialize(byteData, _options); + Expand(contentTypeId, content); + return content; + } + else + { + return null; + } } - public string Serialize(int contentTypeId, ContentNestedData nestedData) + public ContentCacheDataSerializationResult Serialize(int contentTypeId, ContentCacheDataModel model) { - Compress(contentTypeId, nestedData); - var bin = MessagePackSerializer.Serialize(nestedData, _options); - return Convert.ToBase64String(bin); - } - - public ContentNestedData DeserializeBytes(int contentTypeId, byte[] data) - { - var nestedData = MessagePackSerializer.Deserialize(data, _options); - Expand(contentTypeId, nestedData); - return nestedData; - } - - public byte[] SerializeBytes(int contentTypeId, ContentNestedData nestedData) - { - Compress(contentTypeId, nestedData); - return MessagePackSerializer.Serialize(nestedData, _options); + Compress(contentTypeId, model); + var bytes = MessagePackSerializer.Serialize(model, _options); + return new ContentCacheDataSerializationResult(null, bytes); } /// /// Used during serialization to compress properties /// - /// - private void Compress(int contentTypeId, ContentNestedData nestedData) + /// + private void Compress(int contentTypeId, ContentCacheDataModel model) { - foreach(var propertyAliasToData in nestedData.PropertyData) + foreach(var propertyAliasToData in model.PropertyData) { if (_propertyOptions.IsCompressed(contentTypeId, propertyAliasToData.Key)) { @@ -100,7 +97,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// Used during deserialization to map the property data as lazy or expand the value /// /// - private void Expand(int contentTypeId, ContentNestedData nestedData) + private void Expand(int contentTypeId, ContentCacheDataModel nestedData) { foreach (var propertyAliasToData in nestedData.PropertyData) { @@ -117,6 +114,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } } + + //private class ContentNestedDataResolver : IFormatterResolver //{ // // GetFormatter's get cost should be minimized so use type cache. diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs new file mode 100644 index 0000000000..b509334604 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs @@ -0,0 +1,62 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + internal class MsgPackContentNestedDataSerializerFactory : IContentCacheDataSerializerFactory + { + private readonly IContentTypeService _contentTypeService; + private readonly IMediaTypeService _mediaTypeService; + private readonly IMemberTypeService _memberTypeService; + private readonly PropertyEditorCollection _propertyEditors; + private readonly ConcurrentDictionary<(int, string), CompressedStorageAttribute> _compressedStoragePropertyEditorCache = new ConcurrentDictionary<(int, string), CompressedStorageAttribute>(); + + public MsgPackContentNestedDataSerializerFactory(IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService, PropertyEditorCollection propertyEditors) + { + _contentTypeService = contentTypeService; + _mediaTypeService = mediaTypeService; + _memberTypeService = memberTypeService; + _propertyEditors = propertyEditors; + } + + public IContentCacheDataSerializer Create(ContentCacheDataSerializerEntityType types) + { + // Depending on which entity types are being requested, we need to look up those content types + // to initialize the compression options. + // We need to initialize these options now so that any data lookups required are completed and are not done while the content cache + // is performing DB queries which will result in errors since we'll be trying to query with open readers. + // NOTE: The calls to GetAll() below should be cached if the data has not been changed. + + var contentTypes = new Dictionary(); + if ((types & ContentCacheDataSerializerEntityType.Document) == ContentCacheDataSerializerEntityType.Document) + { + foreach(var ct in _contentTypeService.GetAll()) + { + contentTypes[ct.Id] = ct; + } + } + if ((types & ContentCacheDataSerializerEntityType.Media) == ContentCacheDataSerializerEntityType.Media) + { + foreach (var ct in _mediaTypeService.GetAll()) + { + contentTypes[ct.Id] = ct; + } + } + if ((types & ContentCacheDataSerializerEntityType.Member) == ContentCacheDataSerializerEntityType.Member) + { + foreach (var ct in _memberTypeService.GetAll()) + { + contentTypes[ct.Id] = ct; + } + } + + var options = new CompressedStoragePropertyEditorCompressionOptions(contentTypes, _propertyEditors, _compressedStoragePropertyEditorCache); + var serializer = new MsgPackContentNestedDataSerializer(options); + + return serializer; + } + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs index baa96a4a2e..faeb4f90b4 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; -using System.Configuration; -using System.Linq; +using System.Configuration; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.PropertyEditors; -using Umbraco.Web.PropertyEditors; using Umbraco.Web.PublishedCache.NuCache.DataSource; namespace Umbraco.Web.PublishedCache.NuCache @@ -19,13 +16,11 @@ namespace Umbraco.Web.PublishedCache.NuCache if (serializer != "MsgPack") { // TODO: This allows people to revert to the legacy serializer, by default it will be MessagePack - composition.RegisterUnique(); - composition.RegisterUnique(); + composition.RegisterUnique(); } else { - composition.RegisterUnique(); - composition.RegisterUnique(); + composition.RegisterUnique(); } composition.RegisterUnique(factory => new ContentDataSerializer(new DictionaryOfPropertyDataSerializer())); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 41dfdd7a64..ad8705ef47 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1,14 +1,10 @@ using System; using System.Collections.Generic; using System.Configuration; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using CSharpTest.Net.Collections; -using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; @@ -49,7 +45,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly IPublishedModelFactory _publishedModelFactory; private readonly IDefaultCultureAccessor _defaultCultureAccessor; private readonly UrlSegmentProviderCollection _urlSegmentProviders; - private readonly IContentNestedDataSerializer _contentNestedDataSerializer; + private readonly IContentCacheDataSerializerFactory _contentCacheDataSerializerFactory; private readonly ContentDataSerializer _contentDataSerializer; // volatile because we read it with no lock @@ -84,7 +80,7 @@ namespace Umbraco.Web.PublishedCache.NuCache IDataSource dataSource, IGlobalSettings globalSettings, IEntityXmlSerializer entitySerializer, IPublishedModelFactory publishedModelFactory, - UrlSegmentProviderCollection urlSegmentProviders, IContentNestedDataSerializer contentNestedDataSerializer, ContentDataSerializer contentDataSerializer = null) + UrlSegmentProviderCollection urlSegmentProviders, IContentCacheDataSerializerFactory contentCacheDataSerializerFactory, ContentDataSerializer contentDataSerializer = null) : base(publishedSnapshotAccessor, variationContextAccessor) { //if (Interlocked.Increment(ref _singletonCheck) > 1) @@ -101,7 +97,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _defaultCultureAccessor = defaultCultureAccessor; _globalSettings = globalSettings; _urlSegmentProviders = urlSegmentProviders; - _contentNestedDataSerializer = contentNestedDataSerializer; + _contentCacheDataSerializerFactory = contentCacheDataSerializerFactory; _contentDataSerializer = contentDataSerializer; // we need an Xml serializer here so that the member cache can support XPath, @@ -1286,8 +1282,10 @@ namespace Umbraco.Web.PublishedCache.NuCache var db = args.Scope.Database; var content = (Content)args.Entity; + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); + // always refresh the edited data - OnRepositoryRefreshed(db, content, false); + OnRepositoryRefreshed(serializer, db, content, false); // if unpublishing, remove published data from table if (content.PublishedState == PublishedState.Unpublishing) @@ -1295,33 +1293,37 @@ namespace Umbraco.Web.PublishedCache.NuCache // if publishing, refresh the published data else if (content.PublishedState == PublishedState.Publishing) - OnRepositoryRefreshed(db, content, true); + OnRepositoryRefreshed(serializer, db, content, true); } private void OnMediaRefreshedEntity(MediaRepository sender, MediaRepository.ScopedEntityEventArgs args) { + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); + var db = args.Scope.Database; var media = args.Entity; // refresh the edited data - OnRepositoryRefreshed(db, media, false); + OnRepositoryRefreshed(serializer, db, media, false); } private void OnMemberRefreshedEntity(MemberRepository sender, MemberRepository.ScopedEntityEventArgs args) { + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Member); + var db = args.Scope.Database; var member = args.Entity; // refresh the edited data - OnRepositoryRefreshed(db, member, false); + OnRepositoryRefreshed(serializer, db, member, false); } - private void OnRepositoryRefreshed(IUmbracoDatabase db, IContentBase content, bool published) + private void OnRepositoryRefreshed(IContentCacheDataSerializer serializer, IUmbracoDatabase db, IContentBase content, bool published) { // use a custom SQL to update row version on each update //db.InsertOrUpdate(dto); - var dto = GetDto(content, published); + var dto = GetDto(content, published, serializer); db.InsertOrUpdate(dto, "SET data=@data, dataRaw=@dataRaw, rv=rv+1 WHERE nodeId=@id AND published=@published", new @@ -1375,7 +1377,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - private ContentNuDto GetDto(IContentBase content, bool published) + private ContentNuDto GetDto(IContentBase content, bool published, IContentCacheDataSerializer serializer) { // should inject these in ctor // BUT for the time being we decide not to support ConvertDbToXml/String @@ -1447,19 +1449,21 @@ namespace Umbraco.Web.PublishedCache.NuCache } //the dictionary that will be serialized - var nestedData = new ContentNestedData + var contentCacheData = new ContentCacheDataModel { PropertyData = propertyData, CultureData = cultureData, UrlSegment = content.GetUrlSegment(_urlSegmentProviders) }; + var serialized = serializer.Serialize(content.ContentTypeId, contentCacheData); + var dto = new ContentNuDto { NodeId = content.Id, Published = published, - Data = !(_contentNestedDataSerializer is IContentNestedDataByteSerializer) ? _contentNestedDataSerializer.Serialize(content.ContentTypeId, nestedData) : null, - RawData = (_contentNestedDataSerializer is IContentNestedDataByteSerializer byteSerializer) ? byteSerializer.SerializeBytes(content.ContentTypeId, nestedData) : null + Data = serialized.StringData, + RawData = serialized.ByteData }; //Core.Composing.Current.Logger.Debug(dto.Data); @@ -1482,30 +1486,32 @@ namespace Umbraco.Web.PublishedCache.NuCache public override void Rebuild() { _logger.Debug("Rebuilding..."); + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document | ContentCacheDataSerializerEntityType.Media | ContentCacheDataSerializerEntityType.Member); using (var scope = _scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) { scope.ReadLock(Constants.Locks.ContentTree); scope.ReadLock(Constants.Locks.MediaTree); scope.ReadLock(Constants.Locks.MemberTree); - RebuildContentDbCacheLocked(scope, GetSqlPagingSize(), null); - RebuildMediaDbCacheLocked(scope, GetSqlPagingSize(), null); - RebuildMemberDbCacheLocked(scope, GetSqlPagingSize(), null); + RebuildContentDbCacheLocked(serializer, scope, GetSqlPagingSize(), null); + RebuildMediaDbCacheLocked(serializer, scope, GetSqlPagingSize(), null); + RebuildMemberDbCacheLocked(serializer, scope, GetSqlPagingSize(), null); scope.Complete(); } } public void RebuildContentDbCache(int groupSize = DefaultSqlPagingSize, IEnumerable contentTypeIds = null) { + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); using (var scope = _scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) { scope.ReadLock(Constants.Locks.ContentTree); - RebuildContentDbCacheLocked(scope, groupSize, contentTypeIds); + RebuildContentDbCacheLocked(serializer, scope, groupSize, contentTypeIds); scope.Complete(); } } // assumes content tree lock - private void RebuildContentDbCacheLocked(IScope scope, int groupSize, IEnumerable contentTypeIds) + private void RebuildContentDbCacheLocked(IContentCacheDataSerializer serializer, IScope scope, int groupSize, IEnumerable contentTypeIds) { var contentTypeIdsA = contentTypeIds?.ToArray(); var contentObjectType = Constants.ObjectTypes.Document; @@ -1552,11 +1558,11 @@ WHERE cmsContentNu.nodeId IN ( foreach (var c in descendants) { // always the edited version - items.Add(GetDto(c, false)); + items.Add(GetDto(c, false, serializer)); // and also the published version if it makes any sense if (c.Published) - items.Add(GetDto(c, true)); + items.Add(GetDto(c, true, serializer)); count++; } @@ -1568,16 +1574,17 @@ WHERE cmsContentNu.nodeId IN ( public void RebuildMediaDbCache(int groupSize = DefaultSqlPagingSize, IEnumerable contentTypeIds = null) { + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); using (var scope = _scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) { scope.ReadLock(Constants.Locks.MediaTree); - RebuildMediaDbCacheLocked(scope, groupSize, contentTypeIds); + RebuildMediaDbCacheLocked(serializer, scope, groupSize, contentTypeIds); scope.Complete(); } } // assumes media tree lock - public void RebuildMediaDbCacheLocked(IScope scope, int groupSize, IEnumerable contentTypeIds) + public void RebuildMediaDbCacheLocked(IContentCacheDataSerializer serializer, IScope scope, int groupSize, IEnumerable contentTypeIds) { var contentTypeIdsA = contentTypeIds?.ToArray(); var mediaObjectType = Constants.ObjectTypes.Media; @@ -1619,7 +1626,7 @@ WHERE cmsContentNu.nodeId IN ( { // the tree is locked, counting and comparing to total is safe var descendants = _mediaRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); - var items = descendants.Select(m => GetDto(m, false)).ToList(); + var items = descendants.Select(m => GetDto(m, false, serializer)).ToList(); db.BulkInsertRecords(items); processed += items.Count; } while (processed < total); @@ -1627,16 +1634,17 @@ WHERE cmsContentNu.nodeId IN ( public void RebuildMemberDbCache(int groupSize = DefaultSqlPagingSize, IEnumerable contentTypeIds = null) { + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Member); using (var scope = _scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) { scope.ReadLock(Constants.Locks.MemberTree); - RebuildMemberDbCacheLocked(scope, groupSize, contentTypeIds); + RebuildMemberDbCacheLocked(serializer, scope, groupSize, contentTypeIds); scope.Complete(); } } // assumes member tree lock - public void RebuildMemberDbCacheLocked(IScope scope, int groupSize, IEnumerable contentTypeIds) + public void RebuildMemberDbCacheLocked(IContentCacheDataSerializer serializer, IScope scope, int groupSize, IEnumerable contentTypeIds) { var contentTypeIdsA = contentTypeIds?.ToArray(); var memberObjectType = Constants.ObjectTypes.Member; @@ -1677,7 +1685,7 @@ WHERE cmsContentNu.nodeId IN ( do { var descendants = _memberRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); - var items = descendants.Select(m => GetDto(m, false)).ToArray(); + var items = descendants.Select(m => GetDto(m, false, serializer)).ToArray(); db.BulkInsertRecords(items); processed += items.Length; } while (processed < total); diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 25478bf626..e93caaac66 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -262,12 +262,15 @@ - - + + + + + @@ -277,6 +280,7 @@ + @@ -588,7 +592,7 @@ - + From e3fd9b72c5c29798f52dbb34880fa59c5602ddb9 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Sat, 24 Oct 2020 17:06:28 +1300 Subject: [PATCH 042/289] Optimize content and media count queries when paging --- .../NuCache/DataSource/DatabaseDataSource.cs | 66 ++++++++++++++++--- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index e60fd6623e..4c7cb57f19 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -80,7 +80,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// /// /// - private Sql ContentSourcesCount(IScope scope) + private Sql ContentSourcesCount(IScope scope, Func, Sql> joins = null) { var sqlTemplate = scope.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.ContentSourcesCount, tsql => tsql.Select(x => Alias(x.NodeId, "Id")) @@ -90,6 +90,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource var sql = sqlTemplate.Sql(); + if (joins != null) + sql = joins(sql); + // TODO: We can't use a template with this one because of the 'right.Current' and 'right.Published' ends up being a parameter so not sure how we can do that sql = sql .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) @@ -147,12 +150,20 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .Where(x => x.NodeId == id, "x") .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + // create a more efficient COUNT query without the join on the cmsContentNu table + var sqlCountQuery = ContentSourcesCount(scope, + s => s.InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) + .Where(x => x.NodeId == id, "x"); + var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); + + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + foreach (var row in scope.Database.QueryPaged(PageSize, sql, sqlCount)) { yield return CreateContentNodeKit(row, serializer); } @@ -167,12 +178,17 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .WhereIn(x => x.ContentTypeId, ids) .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + var sqlCountQuery = ContentSourcesCount(scope) + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) + .WhereIn(x => x.ContentTypeId, ids); + var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + foreach (var row in scope.Database.QueryPaged(PageSize, sql, sqlCount)) { yield return CreateContentNodeKit(row, serializer); } @@ -182,9 +198,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { var sql = scope.SqlContext.Sql() - .Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Uid"), - x => Alias(x.Level, "Level"), x => Alias(x.Path, "Path"), x => Alias(x.SortOrder, "SortOrder"), x => Alias(x.ParentId, "ParentId"), - x => Alias(x.CreateDate, "CreateDate"), x => Alias(x.UserId, "CreatorId")) + .Select(x => Alias(x.NodeId, "Id")) .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) .AndSelect(x => Alias(x.Id, "VersionId"), x => Alias(x.Text, "EditName"), x => Alias(x.VersionDate, "EditVersionDate"), x => Alias(x.UserId, "EditWriterId")) .AndSelect("nuEdit", x => Alias(x.Data, "EditData")) @@ -201,6 +215,22 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return sql; } + private Sql MediaSourcesCount(IScope scope, Func, Sql> joins = null) + { + var sql = scope.SqlContext.Sql() + + .Select(x => Alias(x.NodeId, "Id")) + .From(); + + if (joins != null) + sql = joins(sql); + + sql = sql + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current); + + return sql; + } public ContentNodeKit GetMediaSource(IScope scope, int id) { @@ -222,12 +252,17 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + var sqlCountQuery = MediaSourcesCount(scope) + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed); + + var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + foreach (var row in scope.Database.QueryPaged(PageSize, sql, sqlCount)) { yield return CreateMediaNodeKit(row, serializer); } @@ -242,12 +277,19 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .Where(x => x.NodeId == id, "x") .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + var sqlCountQuery = MediaSourcesCount(scope, + s => s.InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) + .Where(x => x.NodeId == id, "x"); + + var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + foreach (var row in scope.Database.QueryPaged(PageSize, sql, sqlCount)) { yield return CreateMediaNodeKit(row, serializer); } @@ -262,12 +304,18 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .WhereIn(x => x.ContentTypeId, ids) .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + var sqlCountQuery = MediaSourcesCount(scope) + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) + .WhereIn(x => x.ContentTypeId, ids); + + var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + foreach (var row in scope.Database.QueryPaged(PageSize, sql, sqlCount)) { yield return CreateMediaNodeKit(row, serializer); } From da3d5093337068a5406115c3818c3de937167f17 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Wed, 18 Nov 2020 14:41:03 +1300 Subject: [PATCH 043/289] bug fix. --- .../PublishedCache/NuCache/DataSource/DatabaseDataSource.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index 4c7cb57f19..98dbf1f85c 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -198,7 +198,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { var sql = scope.SqlContext.Sql() - .Select(x => Alias(x.NodeId, "Id")) + .Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Uid"), + x => Alias(x.Level, "Level"), x => Alias(x.Path, "Path"), x => Alias(x.SortOrder, "SortOrder"), x => Alias(x.ParentId, "ParentId"), + x => Alias(x.CreateDate, "CreateDate"), x => Alias(x.UserId, "CreatorId")) .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) .AndSelect(x => Alias(x.Id, "VersionId"), x => Alias(x.Text, "EditName"), x => Alias(x.VersionDate, "EditVersionDate"), x => Alias(x.UserId, "EditWriterId")) .AndSelect("nuEdit", x => Alias(x.Data, "EditData")) From 95fe8bea23b9a1f4979623788b078b0f3f42aa7e Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Tue, 24 Nov 2020 10:09:27 +1300 Subject: [PATCH 044/289] Serialize more primitives --- .../NuCache/DataSource/SerializerBase.cs | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs index 8b02946fc2..e6ae789702 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs @@ -4,7 +4,7 @@ using CSharpTest.Net.Serialization; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { - internal abstract class SerializerBase + public abstract class SerializerBase { private const char PrefixNull = 'N'; private const char PrefixString = 'S'; @@ -18,6 +18,12 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource private const char PrefixByte = 'O'; private const char PrefixByteArray = 'A'; private const char PrefixCompressedStringByteArray = 'C'; + private const char PrefixSignedByte = 'E'; + private const char PrefixBool = 'M'; + private const char PrefixGuid = 'G'; + private const char PrefixTimeSpan = 'T'; + private const char PrefixInt16 = 'Q'; + private const char PrefixChar = 'R'; protected string ReadString(Stream stream) => PrimitiveSerializer.String.ReadFrom(stream); protected int ReadInt(Stream stream) => PrimitiveSerializer.Int32.ReadFrom(stream); @@ -86,6 +92,18 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return PrimitiveSerializer.DateTime.ReadFrom(stream); case PrefixByteArray: return PrimitiveSerializer.Bytes.ReadFrom(stream); + case PrefixSignedByte: + return PrimitiveSerializer.SByte.ReadFrom(stream); + case PrefixBool: + return PrimitiveSerializer.Boolean.ReadFrom(stream); + case PrefixGuid: + return PrimitiveSerializer.Guid.ReadFrom(stream); + case PrefixTimeSpan: + return PrimitiveSerializer.TimeSpan.ReadFrom(stream); + case PrefixInt16: + return PrimitiveSerializer.Int16.ReadFrom(stream); + case PrefixChar: + return PrimitiveSerializer.Char.ReadFrom(stream); case PrefixCompressedStringByteArray: return new LazyCompressedString(PrimitiveSerializer.Bytes.ReadFrom(stream)); default: @@ -157,6 +175,36 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource PrimitiveSerializer.Char.WriteTo(PrefixCompressedStringByteArray, stream); PrimitiveSerializer.Bytes.WriteTo(lazyCompressedString.GetBytes(), stream); } + else if (value is sbyte signedByteValue) + { + PrimitiveSerializer.Char.WriteTo(PrefixSignedByte, stream); + PrimitiveSerializer.SByte.WriteTo(signedByteValue, stream); + } + else if (value is bool boolValue) + { + PrimitiveSerializer.Char.WriteTo(PrefixBool, stream); + PrimitiveSerializer.Boolean.WriteTo(boolValue, stream); + } + else if (value is Guid guidValue) + { + PrimitiveSerializer.Char.WriteTo(PrefixGuid, stream); + PrimitiveSerializer.Guid.WriteTo(guidValue, stream); + } + else if (value is TimeSpan timespanValue) + { + PrimitiveSerializer.Char.WriteTo(PrefixTimeSpan, stream); + PrimitiveSerializer.TimeSpan.WriteTo(timespanValue, stream); + } + else if (value is short int16Value) + { + PrimitiveSerializer.Char.WriteTo(PrefixInt16, stream); + PrimitiveSerializer.Int16.WriteTo(int16Value, stream); + } + else if (value is char charValue) + { + PrimitiveSerializer.Char.WriteTo(PrefixChar, stream); + PrimitiveSerializer.Char.WriteTo(charValue, stream); + } else throw new NotSupportedException("Value type " + value.GetType().FullName + " cannot be serialized."); } From e8195bfff05af636d15e350ec39a9b49091d5b6f Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Tue, 24 Nov 2020 10:10:38 +1300 Subject: [PATCH 045/289] fix deserialization order --- .../MsgPackContentNestedDataSerializer.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index aad337b236..42468ad930 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -48,7 +48,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public ContentCacheDataModel Deserialize(int contentTypeId, string stringData, byte[] byteData) { - if (stringData != null) + if (byteData != null) + { + var content = MessagePackSerializer.Deserialize(byteData, _options); + Expand(contentTypeId, content); + return content; + } + else if (stringData != null) { // NOTE: We don't really support strings but it's possible if manually used (i.e. tests) var bin = Convert.FromBase64String(stringData); @@ -56,12 +62,6 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource Expand(contentTypeId, content); return content; } - else if (byteData != null) - { - var content = MessagePackSerializer.Deserialize(byteData, _options); - Expand(contentTypeId, content); - return content; - } else { return null; From f09fcfbdd4ed4ef9584ab7f4982a09c3fa15da74 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Fri, 27 Nov 2020 14:45:44 +1300 Subject: [PATCH 046/289] Fix serialization of decompressed lazy strings --- .../NuCache/DataSource/LazyCompressedString.cs | 9 ++++++++- .../NuCache/DataSource/SerializerBase.cs | 12 ++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs index 3e0e796d36..99e6f44af7 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs @@ -13,6 +13,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource private byte[] _bytes; private string _str; private readonly object _locker; + private bool _isDecompressed; /// /// Constructor @@ -22,7 +23,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { _locker = new object(); _bytes = bytes; - _str = null; + _str = null; + _isDecompressed = false; } public byte[] GetBytes() @@ -31,6 +33,10 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource throw new InvalidOperationException("The bytes have already been expanded"); return _bytes; } + public bool IsDecompressed() + { + return _isDecompressed; + } public override string ToString() { @@ -41,6 +47,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource if (_bytes == null) throw new PanicException("Bytes have already been cleared"); _str = Encoding.UTF8.GetString(LZ4Pickler.Unpickle(_bytes)); _bytes = null; + _isDecompressed = true; } return _str; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs index e6ae789702..f7e2f32f5d 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs @@ -172,8 +172,16 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } else if (value is LazyCompressedString lazyCompressedString) { - PrimitiveSerializer.Char.WriteTo(PrefixCompressedStringByteArray, stream); - PrimitiveSerializer.Bytes.WriteTo(lazyCompressedString.GetBytes(), stream); + if (lazyCompressedString.IsDecompressed()) + { + PrimitiveSerializer.Char.WriteTo(PrefixString, stream); + PrimitiveSerializer.String.WriteTo(lazyCompressedString, stream); + } + else + { + PrimitiveSerializer.Char.WriteTo(PrefixCompressedStringByteArray, stream); + PrimitiveSerializer.Bytes.WriteTo(lazyCompressedString.GetBytes(), stream); + } } else if (value is sbyte signedByteValue) { From c62b3f4497b579d27f3c41db67b51c88647f941b Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 16 Dec 2020 15:44:00 +1100 Subject: [PATCH 047/289] re-updates versions for db changes/migrations --- src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs | 7 ++++--- .../{V_8_9_0 => V_8_11_0}/AddCmsContentNuByteColumn.cs | 2 +- .../{V_8_9_0 => V_8_11_0}/UpgradedIncludeIndexes.cs | 8 ++------ src/Umbraco.Core/Umbraco.Core.csproj | 4 ++-- 4 files changed, 9 insertions(+), 12 deletions(-) rename src/Umbraco.Core/Migrations/Upgrade/{V_8_9_0 => V_8_11_0}/AddCmsContentNuByteColumn.cs (90%) rename src/Umbraco.Core/Migrations/Upgrade/{V_8_9_0 => V_8_11_0}/UpgradedIncludeIndexes.cs (95%) diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 911cb612d4..a60b046212 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Migrations.Upgrade.V_8_1_0; using Umbraco.Core.Migrations.Upgrade.V_8_6_0; using Umbraco.Core.Migrations.Upgrade.V_8_9_0; using Umbraco.Core.Migrations.Upgrade.V_8_10_0; +using Umbraco.Core.Migrations.Upgrade.V_8_11_0; namespace Umbraco.Core.Migrations.Upgrade { @@ -199,12 +200,12 @@ namespace Umbraco.Core.Migrations.Upgrade // to 8.9.0 To("{B5838FF5-1D22-4F6C-BCEB-F83ACB14B575}"); - // to 8.9.0... - To("{8DDDCD0B-D7D5-4C97-BD6A-6B38CA65752F}"); - To("{4695D0C9-0729-4976-985B-048D503665D8}"); // to 8.10.0 To("{D6A8D863-38EC-44FB-91EC-ACD6A668BD18}"); + // to 8.11.0... + To("{8DDDCD0B-D7D5-4C97-BD6A-6B38CA65752F}"); + To("{4695D0C9-0729-4976-985B-048D503665D8}"); //FINAL } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_9_0/AddCmsContentNuByteColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_11_0/AddCmsContentNuByteColumn.cs similarity index 90% rename from src/Umbraco.Core/Migrations/Upgrade/V_8_9_0/AddCmsContentNuByteColumn.cs rename to src/Umbraco.Core/Migrations/Upgrade/V_8_11_0/AddCmsContentNuByteColumn.cs index c295e13051..dfde1f0577 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_9_0/AddCmsContentNuByteColumn.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_11_0/AddCmsContentNuByteColumn.cs @@ -1,7 +1,7 @@ using System.Linq; using Umbraco.Core.Persistence.Dtos; -namespace Umbraco.Core.Migrations.Upgrade.V_8_9_0 +namespace Umbraco.Core.Migrations.Upgrade.V_8_11_0 { public class AddCmsContentNuByteColumn : MigrationBase { diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_9_0/UpgradedIncludeIndexes.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_11_0/UpgradedIncludeIndexes.cs similarity index 95% rename from src/Umbraco.Core/Migrations/Upgrade/V_8_9_0/UpgradedIncludeIndexes.cs rename to src/Umbraco.Core/Migrations/Upgrade/V_8_11_0/UpgradedIncludeIndexes.cs index 083294d1a5..6919558fc3 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_9_0/UpgradedIncludeIndexes.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_11_0/UpgradedIncludeIndexes.cs @@ -3,7 +3,7 @@ using Umbraco.Core.Migrations.Expressions.Execute.Expressions; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; -namespace Umbraco.Core.Migrations.Upgrade.V_8_9_0 +namespace Umbraco.Core.Migrations.Upgrade.V_8_11_0 { public class UpgradedIncludeIndexes : MigrationBase { @@ -44,13 +44,9 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_9_0 var tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax); foreach (var i in toDelete) - { if (IndexExists(i)) - { Delete.Index(i).OnTable(tableDef.Name).Do(); - } - } - + } private void CreateIndexes(params string[] toCreate) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index c2cc3a0783..1958ea63b5 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -132,8 +132,8 @@ - - + + From 6b19dfe191e0da43ad189c7dc7878d9235b8eb27 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 16 Dec 2020 15:45:10 +1100 Subject: [PATCH 048/289] Fixing build --- src/Umbraco.Core/Umbraco.Core.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 1958ea63b5..a15fc69bc3 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -134,7 +134,6 @@ - From 78b563c79b47e6b9d5a54980f50881f209ff65b1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 16 Dec 2020 16:12:02 +1100 Subject: [PATCH 049/289] Adds notes --- .../PublishedCache/IPublishedContentCache.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs index 8175285c3a..a31bdf34c5 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs @@ -43,16 +43,26 @@ namespace Umbraco.Web.PublishedCache /// /// A value indicating whether to consider unpublished content. /// The content unique identifier. - /// The route. - /// The value of overrides defaults. + /// A special string formatted route path. + /// + /// + /// The resulting string is a special encoded route string that may contain the domain ID + /// for the current route. If a domain is present the string will be prefixed with the domain ID integer, example: {domainId}/route-path-of-item + /// + /// The value of overrides defaults. + /// string GetRouteById(bool preview, int contentId, string culture = null); /// /// Gets the route for a content identified by its unique identifier. /// /// The content unique identifier. - /// The route. + /// A special string formatted route path. /// Considers published or unpublished content depending on defaults. + /// + /// The resulting string is a special encoded route string that may contain the domain ID + /// for the current route. If a domain is present the string will be prefixed with the domain ID integer, example: {domainId}/route-path-of-item + /// string GetRouteById(int contentId, string culture = null); } } From c3656d3e58d16a41d0405ed8b99828d1e687fc03 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 16 Dec 2020 16:52:43 +1100 Subject: [PATCH 050/289] Adds notes fixes tests --- .../ContentCacheDataSerializationResult.cs | 4 ++++ .../DataSource/IContentCacheDataSerializer.cs | 14 ++++++++++++++ .../DataSource/JsonContentNestedDataSerializer.cs | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataSerializationResult.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataSerializationResult.cs index 7cd388d712..cde39eaa3c 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataSerializationResult.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataSerializationResult.cs @@ -3,6 +3,10 @@ using System.Collections.Generic; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { + /// + /// The serialization result from for which the serialized value + /// will be either a string or a byte[] + /// public struct ContentCacheDataSerializationResult : IEquatable { public ContentCacheDataSerializationResult(string stringData, byte[] byteData) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs index 87ac5af91e..f628c8981b 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs @@ -11,7 +11,21 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// public interface IContentCacheDataSerializer { + /// + /// Deserialize the data into a + /// + /// + /// + /// + /// ContentCacheDataModel Deserialize(int contentTypeId, string stringData, byte[] byteData); + + /// + /// Serializes the + /// + /// + /// + /// ContentCacheDataSerializationResult Serialize(int contentTypeId, ContentCacheDataModel model); } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs index 2fa892a5e6..47f07b8b1d 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs @@ -10,7 +10,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { public ContentCacheDataModel Deserialize(int contentTypeId, string stringData, byte[] byteData) { - if (byteData != null) + if (stringData == null && byteData != null) throw new NotSupportedException($"{typeof(JsonContentNestedDataSerializer)} does not support byte[] serialization"); // by default JsonConvert will deserialize our numeric values as Int64 From f8bb53ac0338d41241659a9771d7e076bd2891c3 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Wed, 16 Dec 2020 19:20:24 +1300 Subject: [PATCH 051/289] changed isdecompressed to a property. Changed serializerbase to internal --- .../NuCache/DataSource/LazyCompressedString.cs | 9 +++++---- .../PublishedCache/NuCache/DataSource/SerializerBase.cs | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs index 99e6f44af7..ea904b0cf4 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs @@ -33,10 +33,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource throw new InvalidOperationException("The bytes have already been expanded"); return _bytes; } - public bool IsDecompressed() - { - return _isDecompressed; - } + /// + /// Whether the bytes have been decompressed to a string. If true calling GetBytes() will throw InvalidOperationException. + /// + public bool IsDecompressed => _isDecompressed; + public override string ToString() { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs index f7e2f32f5d..9e6baed4fe 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs @@ -4,7 +4,7 @@ using CSharpTest.Net.Serialization; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { - public abstract class SerializerBase + internal abstract class SerializerBase { private const char PrefixNull = 'N'; private const char PrefixString = 'S'; From 697206188f4933c64a4a66a11c85b5eecad54b43 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Wed, 16 Dec 2020 19:23:14 +1300 Subject: [PATCH 052/289] use property --- .../PublishedCache/NuCache/DataSource/SerializerBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs index 9e6baed4fe..cafb40657d 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs @@ -172,7 +172,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } else if (value is LazyCompressedString lazyCompressedString) { - if (lazyCompressedString.IsDecompressed()) + if (lazyCompressedString.IsDecompressed) { PrimitiveSerializer.Char.WriteTo(PrefixString, stream); PrimitiveSerializer.String.WriteTo(lazyCompressedString, stream); From 025bcf2f4cc00681cad4d80392ea1f33199e2bd7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 16 Dec 2020 17:50:34 +1100 Subject: [PATCH 053/289] notes --- .../NuCache/DataSource/BTree.cs | 1 + .../NuCache/DataSource/SerializerBase.cs | 25 ++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs index 80d8488495..7ddfc8e6ea 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs @@ -30,6 +30,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource //btree. return tree; + } private static int GetBlockSize() diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs index 8b02946fc2..0cd095d77f 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs @@ -57,11 +57,18 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource protected object ReadObject(Stream stream) => ReadObject(PrimitiveSerializer.Char.ReadFrom(stream), stream); + /// + /// Reads in a value based on its char type + /// + /// + /// + /// + /// + /// This will incur boxing because the result is an object but in most cases the value will be a struct. + /// When the type is known use the specific methods like instead + /// protected object ReadObject(char type, Stream stream) { - // NOTE: This method is only called when reading property data, some boxing may occur but all other reads for structs are - // done with ReadStruct to reduce all boxing. - switch (type) { case PrefixNull: @@ -93,11 +100,17 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } } + /// + /// Writes a value to the stream ensuring it's char type is prefixed to the value for reading later + /// + /// + /// + /// + /// This method will incur boxing if the value is a struct. When the type is known use the + /// to write the value directly. + /// protected void WriteObject(object value, Stream stream) { - // NOTE: This method is only currently used to write 'string' information, all other writes are done directly with the PrimitiveSerializer - // so no boxing occurs. Though potentially we should write everything via this class just like we do for reads. - if (value == null) { PrimitiveSerializer.Char.WriteTo(PrefixNull, stream); From dd111226090642cd6777630ff45e432ce8349656 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Sun, 17 Jan 2021 21:33:35 +1300 Subject: [PATCH 054/289] Huge improvement in speed and memory allocations for localized text service. --- .../Compose/RelateOnTrashComponent.cs | 5 +- .../Services/ILocalizedTextService.cs | 14 + .../Implement/LocalizedTextService.cs | 241 ++++---- .../LocalizedTextServiceExtensions.cs | 67 ++- ...TextServiceGetAllStoredValuesBenchmarks.cs | 533 ++++++++++++++++++ .../Umbraco.Tests.Benchmarks.csproj | 1 + .../Mapping/ContentWebModelMappingTests.cs | 4 +- .../Compose/NotificationsComponent.cs | 8 +- .../Editors/AuthenticationController.cs | 2 +- .../Editors/BackOfficeController.cs | 2 +- src/Umbraco.Web/Editors/CodeFileController.cs | 10 +- src/Umbraco.Web/Editors/ContentController.cs | 134 ++--- .../Editors/ContentTypeController.cs | 6 +- .../Editors/ContentTypeControllerBase.cs | 6 +- .../Editors/CurrentUserController.cs | 2 +- src/Umbraco.Web/Editors/DataTypeController.cs | 4 +- src/Umbraco.Web/Editors/MediaController.cs | 26 +- .../Editors/MediaTypeController.cs | 2 +- src/Umbraco.Web/Editors/MemberController.cs | 2 +- .../Editors/MemberGroupController.cs | 2 +- .../Editors/MemberTypeController.cs | 2 +- .../Editors/PackageInstallController.cs | 10 +- .../Editors/TemplateQueryController.cs | 40 +- .../Editors/UserGroupsController.cs | 6 +- src/Umbraco.Web/Editors/UsersController.cs | 24 +- .../Checks/Config/AbstractConfigCheck.cs | 16 +- .../Checks/Config/CompilationDebugCheck.cs | 6 +- .../Checks/Config/ConfigurationService.cs | 12 +- .../Checks/Config/CustomErrorsCheck.cs | 6 +- .../Checks/Config/MacroErrorsCheck.cs | 6 +- .../Checks/Config/NotificationEmailCheck.cs | 4 +- .../HealthCheck/Checks/Config/TraceCheck.cs | 6 +- .../Config/TrySkipIisCustomErrorsCheck.cs | 6 +- .../FolderAndFilePermissionsCheck.cs | 20 +- .../Checks/Security/BaseHttpHeaderCheck.cs | 14 +- .../Checks/Security/ExcessiveHeadersCheck.cs | 6 +- .../HealthCheck/Checks/Security/HttpsCheck.cs | 28 +- .../HealthCheck/Checks/Services/SmtpCheck.cs | 8 +- .../EmailNotificationMethod.cs | 4 +- src/Umbraco.Web/Macros/MacroRenderer.cs | 4 +- .../Models/Mapping/CommonMapper.cs | 2 +- .../Models/Mapping/ContentVariantMapper.cs | 2 +- .../Mapping/MemberTabsAndPropertiesMapper.cs | 16 +- .../Models/Mapping/SectionMapDefinition.cs | 2 +- .../Models/Mapping/TabsAndPropertiesMapper.cs | 2 +- .../Models/Mapping/UserMapDefinition.cs | 26 +- src/Umbraco.Web/Models/Trees/MenuItem.cs | 7 +- src/Umbraco.Web/Models/Trees/MenuItemList.cs | 6 +- .../BlockEditorPropertyEditor.cs | 4 +- .../DropDownFlexibleConfigurationEditor.cs | 2 +- .../UploadFileTypeValidator.cs | 2 +- .../ValueListConfigurationEditor.cs | 2 +- .../Routing/UrlProviderExtensions.cs | 14 +- .../Trees/ApplicationTreeController.cs | 6 +- .../Trees/ContentTreeControllerBase.cs | 2 +- .../Trees/DataTypeTreeController.cs | 2 +- .../Trees/MediaTypeTreeController.cs | 2 +- src/Umbraco.Web/Trees/MemberTreeController.cs | 2 +- src/Umbraco.Web/Trees/Tree.cs | 2 +- 59 files changed, 1002 insertions(+), 400 deletions(-) create mode 100644 src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs diff --git a/src/Umbraco.Core/Compose/RelateOnTrashComponent.cs b/src/Umbraco.Core/Compose/RelateOnTrashComponent.cs index a216a584ba..27eb559462 100644 --- a/src/Umbraco.Core/Compose/RelateOnTrashComponent.cs +++ b/src/Umbraco.Core/Compose/RelateOnTrashComponent.cs @@ -93,7 +93,8 @@ namespace Umbraco.Core.Compose item.Entity.Id, ObjectTypes.GetName(UmbracoObjectTypes.Document), string.Format(textService.Localize( - "recycleBin/contentTrashed"), + "recycleBin","contentTrashed"), + item.Entity.Id, originalParentId)); } } @@ -132,7 +133,7 @@ namespace Umbraco.Core.Compose item.Entity.Id, ObjectTypes.GetName(UmbracoObjectTypes.Media), string.Format(textService.Localize( - "recycleBin/mediaTrashed"), + "recycleBin", "mediaTrashed"), item.Entity.Id, originalParentId)); } } diff --git a/src/Umbraco.Core/Services/ILocalizedTextService.cs b/src/Umbraco.Core/Services/ILocalizedTextService.cs index f167c64e0f..a4e5141a42 100644 --- a/src/Umbraco.Core/Services/ILocalizedTextService.cs +++ b/src/Umbraco.Core/Services/ILocalizedTextService.cs @@ -4,6 +4,20 @@ using System.Globalization; namespace Umbraco.Core.Services { + + public interface ILocalizedTextService2 : ILocalizedTextService + { + /// + /// Localize a key with variables + /// + /// + /// + /// + /// This can be null + /// + string Localize(string area, string alias, CultureInfo culture, IDictionary tokens = null); + } + /// /// The entry point to localize any key in the text storage source for a given culture /// diff --git a/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs b/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs index 4f5121def7..c099960397 100644 --- a/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs +++ b/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs @@ -10,12 +10,13 @@ namespace Umbraco.Core.Services.Implement { // TODO: Convert all of this over to Niels K's localization framework one day - public class LocalizedTextService : ILocalizedTextService + public class LocalizedTextService : ILocalizedTextService2 { private readonly ILogger _logger; private readonly Lazy _fileSources; private readonly IDictionary>> _dictionarySource; - private readonly IDictionary> _xmlSource; + private readonly IDictionary> _noAreaDictionarySource; + private readonly char[] _splitter = new[] { '/' }; /// /// Initializes with a file sources instance @@ -27,9 +28,40 @@ namespace Umbraco.Core.Services.Implement if (logger == null) throw new ArgumentNullException("logger"); _logger = logger; if (fileSources == null) throw new ArgumentNullException("fileSources"); + var dictionaries = FileSourcesToDictionarySources(fileSources.Value); + _dictionarySource = dictionaries.WithArea; + _noAreaDictionarySource = dictionaries.WithoutArea; _fileSources = fileSources; } + private (IDictionary>> WithArea, IDictionary> WithoutArea) FileSourcesToDictionarySources(LocalizedTextServiceFileSources fileSources) + { + var xmlSources = fileSources.GetXmlSources(); + return XmlSourcesToDictionarySources(xmlSources); + } + private (IDictionary>> WithArea, IDictionary> WithoutArea) XmlSourcesToDictionarySources(IDictionary> source) + { + var cultureDictionary = new Dictionary>>(); + var cultureNoAreaDictionary = new Dictionary>(); + foreach (var xmlSource in source) + { + var areaAliaValue = GetAreaStoredTranslations(source, xmlSource.Key); + cultureDictionary.Add(xmlSource.Key, areaAliaValue); + var aliasValue = new Dictionary(); + foreach (var area in areaAliaValue) + { + foreach (var alias in area.Value) + { + if (!aliasValue.ContainsKey(alias.Key)) + { + aliasValue.Add(alias.Key, alias.Value); + } + } + } + cultureNoAreaDictionary.Add(xmlSource.Key, aliasValue); + } + return (cultureDictionary, cultureNoAreaDictionary); + } /// /// Initializes with an XML source /// @@ -39,10 +71,13 @@ namespace Umbraco.Core.Services.Implement { if (source == null) throw new ArgumentNullException("source"); if (logger == null) throw new ArgumentNullException("logger"); - _xmlSource = source; _logger = logger; + var dictionaries = XmlSourcesToDictionarySources(source); + _dictionarySource = dictionaries.WithArea; + _noAreaDictionarySource = dictionaries.WithoutArea; } + /// /// Initializes with a source of a dictionary of culture -> areas -> sub dictionary of keys/values /// @@ -52,35 +87,50 @@ namespace Umbraco.Core.Services.Implement { _dictionarySource = source ?? throw new ArgumentNullException(nameof(source)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + var cultureNoAreaDictionary = new Dictionary>(); + foreach (var cultureDictionary in _dictionarySource) + { + var areaAliaValue = GetAreaStoredTranslations(source, cultureDictionary.Key); + var aliasValue = new Dictionary(); + foreach (var area in areaAliaValue) + { + foreach (var alias in area.Value) + { + if (!aliasValue.ContainsKey(alias.Key)) + { + aliasValue.Add(alias.Key, alias.Value); + } + } + } + cultureNoAreaDictionary.Add(cultureDictionary.Key, aliasValue); + } + _noAreaDictionarySource = cultureNoAreaDictionary; } public string Localize(string key, CultureInfo culture, IDictionary tokens = null) { if (culture == null) throw new ArgumentNullException(nameof(culture)); - - // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode - culture = ConvertToSupportedCultureWithRegionCode(culture); - - //This is what the legacy ui service did - if (string.IsNullOrEmpty(key)) - return string.Empty; - - var keyParts = key.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries); + var keyParts = key.Split(_splitter, StringSplitOptions.RemoveEmptyEntries); var area = keyParts.Length > 1 ? keyParts[0] : null; var alias = keyParts.Length > 1 ? keyParts[1] : keyParts[0]; - - var xmlSource = _xmlSource ?? (_fileSources != null - ? _fileSources.Value.GetXmlSources() - : null); - - if (xmlSource != null) - { - return GetFromXmlSource(xmlSource, culture, area, alias, tokens); - } - else + return Localize(area, alias, culture, tokens); + } + public string Localize(string area, string alias, CultureInfo culture, IDictionary tokens = null) + { + if (culture == null) throw new ArgumentNullException(nameof(culture)); + var sw = System.Diagnostics.Stopwatch.StartNew(); + try { + // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode + culture = ConvertToSupportedCultureWithRegionCode(culture); + return GetFromDictionarySource(culture, area, alias, tokens); } + finally + { + sw.Stop(); + System.Diagnostics.Debug.WriteLine($"Localize {area}/{alias} ({tokens?.Count}) ({sw.ElapsedTicks})"); + } } /// @@ -96,49 +146,23 @@ namespace Umbraco.Core.Services.Implement var result = new Dictionary(); - var xmlSource = _xmlSource ?? (_fileSources != null - ? _fileSources.Value.GetXmlSources() - : null); - if (xmlSource != null) + if (_dictionarySource.ContainsKey(culture) == false) { - if (xmlSource.ContainsKey(culture) == false) - { - _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture); - return result; - } - - //convert all areas + keys to a single key with a '/' - result = GetStoredTranslations(xmlSource, culture); - - //merge with the English file in case there's keys in there that don't exist in the local file - var englishCulture = new CultureInfo("en-US"); - if (culture.Equals(englishCulture) == false) - { - var englishResults = GetStoredTranslations(xmlSource, englishCulture); - foreach (var englishResult in englishResults.Where(englishResult => result.ContainsKey(englishResult.Key) == false)) - result.Add(englishResult.Key, englishResult.Value); - } + _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture); + return result; } - else - { - if (_dictionarySource.ContainsKey(culture) == false) - { - _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture); - return result; - } - //convert all areas + keys to a single key with a '/' - foreach (var area in _dictionarySource[culture]) + //convert all areas + keys to a single key with a '/' + foreach (var area in _dictionarySource[culture]) + { + foreach (var key in area.Value) { - foreach (var key in area.Value) + var dictionaryKey = string.Format("{0}/{1}", area.Key, key.Key); + //i don't think it's possible to have duplicates because we're dealing with a dictionary in the first place, but we'll double check here just in case. + if (result.ContainsKey(dictionaryKey) == false) { - var dictionaryKey = string.Format("{0}/{1}", area.Key, key.Key); - //i don't think it's possible to have duplicates because we're dealing with a dictionary in the first place, but we'll double check here just in case. - if (result.ContainsKey(dictionaryKey) == false) - { - result.Add(dictionaryKey, key.Value); - } + result.Add(dictionaryKey, key.Value); } } } @@ -146,23 +170,44 @@ namespace Umbraco.Core.Services.Implement return result; } - private Dictionary GetStoredTranslations(IDictionary> xmlSource, CultureInfo cult) + private Dictionary> GetAreaStoredTranslations(IDictionary> xmlSource, CultureInfo cult) { - var result = new Dictionary(); + var overallResult = new Dictionary>(); var areas = xmlSource[cult].Value.XPathSelectElements("//area"); foreach (var area in areas) { + var result = new Dictionary(); var keys = area.XPathSelectElements("./key"); foreach (var key in keys) { - var dictionaryKey = string.Format("{0}/{1}", (string)area.Attribute("alias"), - (string)key.Attribute("alias")); + var dictionaryKey = + (string)key.Attribute("alias"); //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files if (result.ContainsKey(dictionaryKey) == false) result.Add(dictionaryKey, key.Value); } + overallResult.Add(area.Attribute("alias").Value, result); } - return result; + return overallResult; + } + private Dictionary> GetAreaStoredTranslations(IDictionary>> dictionarySource, CultureInfo cult) + { + var overallResult = new Dictionary>(); + var areaDict = dictionarySource[cult]; + + foreach (var area in areaDict) + { + var result = new Dictionary(); + var keys = area.Value.Keys; + foreach (var key in keys) + { + //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files + if (result.ContainsKey(key) == false) + result.Add(key, area.Value[key]); + } + overallResult.Add(area.Key, result); + } + return overallResult; } /// @@ -171,11 +216,7 @@ namespace Umbraco.Core.Services.Implement /// public IEnumerable GetSupportedCultures() { - var xmlSource = _xmlSource ?? (_fileSources != null - ? _fileSources.Value.GetXmlSources() - : null); - - return xmlSource != null ? xmlSource.Keys : _dictionarySource.Keys; + return _dictionarySource.Keys; } /// @@ -211,27 +252,21 @@ namespace Umbraco.Core.Services.Implement return "[" + key + "]"; } - var cultureSource = _dictionarySource[culture]; - string found; - if (area.IsNullOrWhiteSpace()) + string found = null; + if (string.IsNullOrWhiteSpace(area)) { - found = cultureSource - .SelectMany(x => x.Value) - .Where(keyvals => keyvals.Key.InvariantEquals(key)) - .Select(x => x.Value) - .FirstOrDefault(); + _noAreaDictionarySource[culture].TryGetValue(key, out found); } else { - found = cultureSource - .Where(areas => areas.Key.InvariantEquals(area)) - .SelectMany(a => a.Value) - .Where(keyvals => keyvals.Key.InvariantEquals(key)) - .Select(x => x.Value) - .FirstOrDefault(); + if (_dictionarySource[culture].TryGetValue(area, out var areaDictionary)) + { + areaDictionary.TryGetValue(key, out found); + } } + if (found != null) { return ParseTokens(found, tokens); @@ -240,44 +275,6 @@ namespace Umbraco.Core.Services.Implement //NOTE: Based on how legacy works, the default text does not contain the area, just the key return "[" + key + "]"; } - - private string GetFromXmlSource(IDictionary> xmlSource, CultureInfo culture, string area, string key, IDictionary tokens) - { - if (xmlSource.ContainsKey(culture) == false) - { - _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture); - return "[" + key + "]"; - } - - var found = FindTranslation(xmlSource, culture, area, key); - - if (found != null) - { - return ParseTokens(found.Value, tokens); - } - - // Fall back to English by default if we can't find the key - found = FindTranslation(xmlSource, new CultureInfo("en-US"), area, key); - if (found != null) - return ParseTokens(found.Value, tokens); - - // If it can't be found in either file, fall back to the default, showing just the key in square brackets - // NOTE: Based on how legacy works, the default text does not contain the area, just the key - return "[" + key + "]"; - } - - private XElement FindTranslation(IDictionary> xmlSource, CultureInfo culture, string area, string key) - { - var cultureSource = xmlSource[culture].Value; - - var xpath = area.IsNullOrWhiteSpace() - ? string.Format("//key [@alias = '{0}']", key) - : string.Format("//area [@alias = '{0}']/key [@alias = '{1}']", area, key); - - var found = cultureSource.XPathSelectElement(xpath); - return found; - } - /// /// Parses the tokens in the value /// @@ -301,7 +298,7 @@ namespace Umbraco.Core.Services.Implement foreach (var token in tokens) { - value = value.Replace(string.Format("{0}{1}{0}", "%", token.Key), token.Value); + value = value.Replace(string.Concat("%", token.Key, "%"), token.Value); } return value; diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs index ce5b3ef8c4..c6254f7baa 100644 --- a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs +++ b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs @@ -12,12 +12,75 @@ namespace Umbraco.Core.Services /// public static class LocalizedTextServiceExtensions { - public static string Localize(this ILocalizedTextService manager, string area, string key) + public static string Localize(this ILocalizedTextService manager, string area, string alias, CultureInfo culture) { - var fullKey = string.Join("/", area, key); + if(manager is ILocalizedTextService2 manager2) + { + return manager2.Localize(area, alias, culture); + } + var fullKey = string.Concat(area, "/", alias); + return manager.Localize(fullKey, culture); + } + public static string Localize(this ILocalizedTextService manager, string area, string alias) + { + if (manager is ILocalizedTextService2 manager2) + { + return manager2.Localize(area, alias, Thread.CurrentThread.CurrentUICulture); + } + var fullKey = string.Concat(area, "/", alias); return manager.Localize(fullKey, Thread.CurrentThread.CurrentUICulture); } + /// + /// Localize using the current thread culture + /// + /// + /// + /// + /// + /// + public static string Localize(this ILocalizedTextService manager, string area, string alias, string[] tokens) + { + if (manager is ILocalizedTextService2 manager2) + { + return manager2.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, ConvertToDictionaryVars(tokens)); + } + return manager.Localize(string.Join("/",area, alias), Thread.CurrentThread.CurrentUICulture, tokens); + } + /// + /// Localize using the current thread culture + /// + /// + /// + /// + /// + /// + public static string Localize(this ILocalizedTextService manager, string area, string alias, IDictionary tokens = null) + { + if (manager is ILocalizedTextService2 manager2) + { + return manager2.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, tokens); + } + return manager.Localize(string.Join("/", area, alias), Thread.CurrentThread.CurrentUICulture, tokens); + } + + /// + /// Localize a key without any variables + /// + /// + /// + /// + /// + /// + /// + public static string Localize(this ILocalizedTextService manager, string area, string alias, CultureInfo culture, string[] tokens) + { + if (manager is ILocalizedTextService2 manager2) + { + return manager2.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, tokens); + } + return manager.Localize(string.Join("/", area, alias), culture, ConvertToDictionaryVars(tokens)); + } /// /// Localize using the current thread culture /// diff --git a/src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs new file mode 100644 index 0000000000..e084931d9e --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs @@ -0,0 +1,533 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Loggers; +using Moq; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Services.Implement; +using System.Xml.Linq; +using Umbraco.Core.Logging; +using Umbraco.Tests.Benchmarks.Config; +using Umbraco.Core.Services; +using Umbraco.Core; +using System.Xml.XPath; +using ILogger = Umbraco.Core.Logging.ILogger; + +namespace Umbraco.Tests.Benchmarks +{ + [QuickRunWithMemoryDiagnoserConfig] + public class LocalizedTextServiceGetAllStoredValuesBenchmarks + { + private CultureInfo culture; + private OldLocalizedTextService _dictionaryService; + private OldLocalizedTextService _xmlService; + + private LocalizedTextService _optimized; + private LocalizedTextService _optimizedDict; + [GlobalSetup] + public void Setup() + { + culture = CultureInfo.GetCultureInfo("en-US"); + _dictionaryService = GetDictionaryLocalizedTextService(culture); + _xmlService = GetXmlService(culture); + _optimized = GetOptimizedService(culture); + _optimizedDict = GetOptimizedServiceDict(culture); + var result1 = _dictionaryService.Localize("language", culture); + var result2 = _xmlService.Localize("language", culture); + var result3 = _dictionaryService.GetAllStoredValues(culture); + var result4 = _xmlService.GetAllStoredValues(culture); + var result5 = _optimized.GetAllStoredValues(culture); + var result6 = _xmlService.GetAllStoredValues(culture); + } + + [Benchmark] + public void OriginalDictionaryGetAll() + { + for (int i = 0; i < 10000; i++) + { + var result = _dictionaryService.GetAllStoredValues(culture); + } + + } + + [Benchmark] + public void OriginalXmlGetAll() + { + for (int i = 0; i < 10000; i++) + { + var result = _xmlService.GetAllStoredValues(culture); + } + + } + + [Benchmark] + public void OriginalDictionaryLocalize() + { + for (int i = 0; i < 10000; i++) + { + var result = _dictionaryService.Localize("language", culture); + } + + } + + + [Benchmark(Baseline = true)] + public void OriginalXmlLocalize() + { + for (int i = 0; i < 10000; i++) + { + var result = _xmlService.Localize("language", culture); + } + } + [Benchmark()] + public void OptimizedXmlLocalize() + { + for (int i = 0; i < 10000; i++) + { + var result = _optimized.Localize(null, "language", culture); + } + } + [Benchmark()] + public void OptimizedDictLocalize() + { + for (int i = 0; i < 10000; i++) + { + var result = _optimizedDict.Localize(null, "language", culture); + } + } + + private static LocalizedTextService GetOptimizedServiceDict(CultureInfo culture) + { + return new LocalizedTextService( + new Dictionary>> + { + { + culture, new Dictionary> + { + { + "testArea1", new Dictionary + { + {"testKey1", "testValue1"}, + {"testKey2", "testValue2"} + } + }, + { + "testArea2", new Dictionary + { + {"blah1", "blahValue1"}, + {"blah2", "blahValue2"} + } + }, + } + } + }, Mock.Of()); + } + private static LocalizedTextService GetOptimizedService(CultureInfo culture) + { + var txtService = new LocalizedTextService(new Dictionary> + { + { + culture, new Lazy(() => new XDocument( + new XElement("language", + new XElement("area", new XAttribute("alias", "testArea1"), + new XElement("key", new XAttribute("alias", "testKey1"), "testValue1"), + new XElement("key", new XAttribute("alias", "testKey2"), "testValue2")), + new XElement("area", new XAttribute("alias", "testArea2"), + new XElement("key", new XAttribute("alias", "blah1"), "blahValue1"), + new XElement("key", new XAttribute("alias", "blah2"), "blahValue2"))))) + } + }, Mock.Of()); + return txtService; + } + + private static OldLocalizedTextService GetXmlService(CultureInfo culture) + { + var txtService = new OldLocalizedTextService(new Dictionary> + { + { + culture, new Lazy(() => new XDocument( + new XElement("language", + new XElement("area", new XAttribute("alias", "testArea1"), + new XElement("key", new XAttribute("alias", "testKey1"), "testValue1"), + new XElement("key", new XAttribute("alias", "testKey2"), "testValue2")), + new XElement("area", new XAttribute("alias", "testArea2"), + new XElement("key", new XAttribute("alias", "blah1"), "blahValue1"), + new XElement("key", new XAttribute("alias", "blah2"), "blahValue2"))))) + } + }, Mock.Of()); + return txtService; + } + + private static OldLocalizedTextService GetDictionaryLocalizedTextService(CultureInfo culture) + { + return new OldLocalizedTextService( + new Dictionary>> + { + { + culture, new Dictionary> + { + { + "testArea1", new Dictionary + { + {"testKey1", "testValue1"}, + {"testKey2", "testValue2"} + } + }, + { + "testArea2", new Dictionary + { + {"blah1", "blahValue1"}, + {"blah2", "blahValue2"} + } + }, + } + } + }, Mock.Of()); + } + } + + //Original + public class OldLocalizedTextService : ILocalizedTextService + { + private readonly ILogger _logger; + private readonly Lazy _fileSources; + private readonly IDictionary>> _dictionarySource; + private readonly IDictionary> _xmlSource; + + /// + /// Initializes with a file sources instance + /// + /// + /// + public OldLocalizedTextService(Lazy fileSources, ILogger logger) + { + if (logger == null) throw new ArgumentNullException("logger"); + _logger = logger; + if (fileSources == null) throw new ArgumentNullException("fileSources"); + _fileSources = fileSources; + } + + /// + /// Initializes with an XML source + /// + /// + /// + public OldLocalizedTextService(IDictionary> source, ILogger logger) + { + if (source == null) throw new ArgumentNullException("source"); + if (logger == null) throw new ArgumentNullException("logger"); + _xmlSource = source; + _logger = logger; + } + + /// + /// Initializes with a source of a dictionary of culture -> areas -> sub dictionary of keys/values + /// + /// + /// + public OldLocalizedTextService(IDictionary>> source, ILogger logger) + { + _dictionarySource = source ?? throw new ArgumentNullException(nameof(source)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public string Localize(string key, CultureInfo culture, IDictionary tokens = null) + { + if (culture == null) throw new ArgumentNullException(nameof(culture)); + + // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode + culture = ConvertToSupportedCultureWithRegionCode(culture); + + //This is what the legacy ui service did + if (string.IsNullOrEmpty(key)) + return string.Empty; + + var keyParts = key.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + var area = keyParts.Length > 1 ? keyParts[0] : null; + var alias = keyParts.Length > 1 ? keyParts[1] : keyParts[0]; + + var xmlSource = _xmlSource ?? (_fileSources != null + ? _fileSources.Value.GetXmlSources() + : null); + + if (xmlSource != null) + { + return GetFromXmlSource(xmlSource, culture, area, alias, tokens); + } + else + { + return GetFromDictionarySource(culture, area, alias, tokens); + } + } + + /// + /// Returns all key/values in storage for the given culture + /// + /// + public IDictionary GetAllStoredValues(CultureInfo culture) + { + if (culture == null) throw new ArgumentNullException("culture"); + + // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode + culture = ConvertToSupportedCultureWithRegionCode(culture); + + var result = new Dictionary(); + + var xmlSource = _xmlSource ?? (_fileSources != null + ? _fileSources.Value.GetXmlSources() + : null); + + if (xmlSource != null) + { + if (xmlSource.ContainsKey(culture) == false) + { + _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture); + return result; + } + + //convert all areas + keys to a single key with a '/' + result = GetStoredTranslations(xmlSource, culture); + + //merge with the English file in case there's keys in there that don't exist in the local file + var englishCulture = new CultureInfo("en-US"); + if (culture.Equals(englishCulture) == false) + { + var englishResults = GetStoredTranslations(xmlSource, englishCulture); + foreach (var englishResult in englishResults.Where(englishResult => result.ContainsKey(englishResult.Key) == false)) + result.Add(englishResult.Key, englishResult.Value); + } + } + else + { + if (_dictionarySource.ContainsKey(culture) == false) + { + _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture); + return result; + } + + //convert all areas + keys to a single key with a '/' + foreach (var area in _dictionarySource[culture]) + { + foreach (var key in area.Value) + { + var dictionaryKey = string.Format("{0}/{1}", area.Key, key.Key); + //i don't think it's possible to have duplicates because we're dealing with a dictionary in the first place, but we'll double check here just in case. + if (result.ContainsKey(dictionaryKey) == false) + { + result.Add(dictionaryKey, key.Value); + } + } + } + } + + return result; + } + + private Dictionary GetStoredTranslations(IDictionary> xmlSource, CultureInfo cult) + { + var result = new Dictionary(); + var areas = xmlSource[cult].Value.XPathSelectElements("//area"); + foreach (var area in areas) + { + var keys = area.XPathSelectElements("./key"); + foreach (var key in keys) + { + var dictionaryKey = string.Format("{0}/{1}", (string)area.Attribute("alias"), + (string)key.Attribute("alias")); + //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files + if (result.ContainsKey(dictionaryKey) == false) + result.Add(dictionaryKey, key.Value); + } + } + return result; + } + + /// + /// Returns a list of all currently supported cultures + /// + /// + public IEnumerable GetSupportedCultures() + { + var xmlSource = _xmlSource ?? (_fileSources != null + ? _fileSources.Value.GetXmlSources() + : null); + + return xmlSource != null ? xmlSource.Keys : _dictionarySource.Keys; + } + + /// + /// Tries to resolve a full 4 letter culture from a 2 letter culture name + /// + /// + /// The culture to determine if it is only a 2 letter culture, if so we'll try to convert it, otherwise it will just be returned + /// + /// + /// + /// TODO: This is just a hack due to the way we store the language files, they should be stored with 4 letters since that + /// is what they reference but they are stored with 2, further more our user's languages are stored with 2. So this attempts + /// to resolve the full culture if possible. + /// + /// This only works when this service is constructed with the LocalizedTextServiceFileSources + /// + public CultureInfo ConvertToSupportedCultureWithRegionCode(CultureInfo currentCulture) + { + if (currentCulture == null) throw new ArgumentNullException("currentCulture"); + + if (_fileSources == null) return currentCulture; + if (currentCulture.Name.Length > 2) return currentCulture; + + var attempt = _fileSources.Value.TryConvert2LetterCultureTo4Letter(currentCulture.TwoLetterISOLanguageName); + return attempt ? attempt.Result : currentCulture; + } + + private string GetFromDictionarySource(CultureInfo culture, string area, string key, IDictionary tokens) + { + if (_dictionarySource.ContainsKey(culture) == false) + { + _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture); + return "[" + key + "]"; + } + + var cultureSource = _dictionarySource[culture]; + + string found; + if (area.IsNullOrWhiteSpace()) + { + found = cultureSource + .SelectMany(x => x.Value) + .Where(keyvals => keyvals.Key.InvariantEquals(key)) + .Select(x => x.Value) + .FirstOrDefault(); + } + else + { + found = cultureSource + .Where(areas => areas.Key.InvariantEquals(area)) + .SelectMany(a => a.Value) + .Where(keyvals => keyvals.Key.InvariantEquals(key)) + .Select(x => x.Value) + .FirstOrDefault(); + } + + if (found != null) + { + return ParseTokens(found, tokens); + } + + //NOTE: Based on how legacy works, the default text does not contain the area, just the key + return "[" + key + "]"; + } + + private string GetFromXmlSource(IDictionary> xmlSource, CultureInfo culture, string area, string key, IDictionary tokens) + { + if (xmlSource.ContainsKey(culture) == false) + { + _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture); + return "[" + key + "]"; + } + + var found = FindTranslation(xmlSource, culture, area, key); + + if (found != null) + { + return ParseTokens(found.Value, tokens); + } + + // Fall back to English by default if we can't find the key + found = FindTranslation(xmlSource, new CultureInfo("en-US"), area, key); + if (found != null) + return ParseTokens(found.Value, tokens); + + // If it can't be found in either file, fall back to the default, showing just the key in square brackets + // NOTE: Based on how legacy works, the default text does not contain the area, just the key + return "[" + key + "]"; + } + + private XElement FindTranslation(IDictionary> xmlSource, CultureInfo culture, string area, string key) + { + var cultureSource = xmlSource[culture].Value; + + var xpath = area.IsNullOrWhiteSpace() + ? string.Format("//key [@alias = '{0}']", key) + : string.Format("//area [@alias = '{0}']/key [@alias = '{1}']", area, key); + + var found = cultureSource.XPathSelectElement(xpath); + return found; + } + + /// + /// Parses the tokens in the value + /// + /// + /// + /// + /// + /// This is based on how the legacy ui localized text worked, each token was just a sequential value delimited with a % symbol. + /// For example: hello %0%, you are %1% ! + /// + /// Since we're going to continue using the same language files for now, the token system needs to remain the same. With our new service + /// we support a dictionary which means in the future we can really have any sort of token system. + /// Currently though, the token key's will need to be an integer and sequential - though we aren't going to throw exceptions if that is not the case. + /// + internal static string ParseTokens(string value, IDictionary tokens) + { + if (tokens == null || tokens.Any() == false) + { + return value; + } + + foreach (var token in tokens) + { + value = value.Replace(string.Format("{0}{1}{0}", "%", token.Key), token.Value); + } + + return value; + } + + } + +// // * Summary * + +// BenchmarkDotNet=v0.11.3, OS=Windows 10.0.18362 +//Intel Core i5-8265U CPU 1.60GHz(Kaby Lake R), 1 CPU, 8 logical and 4 physical cores +// [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.4250.0 +// Job-JIATTD : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.4250.0 + +//IterationCount=3 IterationTime=100.0000 ms LaunchCount = 1 +//WarmupCount=3 + +// Method | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | +//---------------------- |----------:|-----------:|----------:|------:|------------:|------------:|------------:|--------------------:| +// DictionaryGetAll | 11.199 ms | 1.8170 ms | 0.0996 ms | 0.14 | 1888.8889 | - | - | 5868.59 KB | +// XmlGetAll | 62.963 ms | 24.0615 ms | 1.3189 ms | 0.81 | 13500.0000 | - | - | 42448.71 KB | +// DictionaryLocalize | 9.757 ms | 1.6966 ms | 0.0930 ms | 0.13 | 1100.0000 | - | - | 3677.65 KB | +// XmlLocalize | 77.725 ms | 14.6069 ms | 0.8007 ms | 1.00 | 14000.0000 | - | - | 43032.8 KB | +// OptimizedXmlLocalize | 2.402 ms | 0.4256 ms | 0.0233 ms | 0.03 | 187.5000 | - | - | 626.01 KB | +// OptimizedDictLocalize | 2.345 ms | 0.2411 ms | 0.0132 ms | 0.03 | 187.5000 | - | - | 626.01 KB | + +//// * Warnings * +//MinIterationTime +// LocalizedTextServiceGetAllStoredValuesBenchmarks.DictionaryGetAll: IterationCount= 3, IterationTime= 100.0000 ms, LaunchCount= 1, WarmupCount= 3->MinIterationTime = 99.7816 ms which is very small. It's recommended to increase it. +// LocalizedTextServiceGetAllStoredValuesBenchmarks.DictionaryLocalize: IterationCount= 3, IterationTime= 100.0000 ms, LaunchCount= 1, WarmupCount= 3->MinIterationTime = 96.7415 ms which is very small. It's recommended to increase it. +// LocalizedTextServiceGetAllStoredValuesBenchmarks.XmlLocalize: IterationCount= 3, IterationTime= 100.0000 ms, LaunchCount= 1, WarmupCount= 3->MinIterationTime = 76.8151 ms which is very small. It's recommended to increase it. + +//// * Legends * +// Mean : Arithmetic mean of all measurements +// Error : Half of 99.9% confidence interval +// StdDev : Standard deviation of all measurements +// Ratio : Mean of the ratio distribution ([Current]/[Baseline]) +// Gen 0/1k Op : GC Generation 0 collects per 1k Operations +// Gen 1/1k Op : GC Generation 1 collects per 1k Operations +// Gen 2/1k Op : GC Generation 2 collects per 1k Operations +// Allocated Memory/Op : Allocated memory per single operation(managed only, inclusive, 1KB = 1024B) +// 1 ms : 1 Millisecond(0.001 sec) + +//// * Diagnostic Output - MemoryDiagnoser * + + +// // ***** BenchmarkRunner: End ***** +// Run time: 00:00:09 (9.15 sec), executed benchmarks: 6 +} diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index 48d69cf757..4bb7faa22d 100644 --- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -54,6 +54,7 @@ + diff --git a/src/Umbraco.Tests/Models/Mapping/ContentWebModelMappingTests.cs b/src/Umbraco.Tests/Models/Mapping/ContentWebModelMappingTests.cs index 6a4054d5ae..eebc3a0e8b 100644 --- a/src/Umbraco.Tests/Models/Mapping/ContentWebModelMappingTests.cs +++ b/src/Umbraco.Tests/Models/Mapping/ContentWebModelMappingTests.cs @@ -255,8 +255,8 @@ namespace Umbraco.Tests.Models.Mapping } Assert.AreEqual(contentType.CompositionPropertyGroups.Count(), invariantContent.Tabs.Count() - 1); - Assert.IsTrue(invariantContent.Tabs.Any(x => x.Label == Current.Services.TextService.Localize("general/properties"))); - Assert.AreEqual(2, invariantContent.Tabs.Where(x => x.Label == Current.Services.TextService.Localize("general/properties")).SelectMany(x => x.Properties.Where(p => p.Alias.StartsWith("_umb_") == false)).Count()); + Assert.IsTrue(invariantContent.Tabs.Any(x => x.Label == Current.Services.TextService.Localize("general", "properties"))); + Assert.AreEqual(2, invariantContent.Tabs.Where(x => x.Label == Current.Services.TextService.Localize("general", "properties")).SelectMany(x => x.Properties.Where(p => p.Alias.StartsWith("_umb_") == false)).Count()); } #region Assertions diff --git a/src/Umbraco.Web/Compose/NotificationsComponent.cs b/src/Umbraco.Web/Compose/NotificationsComponent.cs index ea5df65f74..42984cef16 100644 --- a/src/Umbraco.Web/Compose/NotificationsComponent.cs +++ b/src/Umbraco.Web/Compose/NotificationsComponent.cs @@ -257,12 +257,12 @@ namespace Umbraco.Web.Compose siteUri, ((IUser user, NotificationEmailSubjectParams subject) x) => _textService.Localize( - "notifications/mailSubject", + "notifications", "mailSubject", x.user.GetUserCulture(_textService, _globalSettings), new[] { x.subject.SiteUrl, x.subject.Action, x.subject.ItemName }), ((IUser user, NotificationEmailBodyParams body, bool isHtml) x) - => _textService.Localize( - x.isHtml ? "notifications/mailBodyHtml" : "notifications/mailBody", + => _textService.Localize("notifications", + x.isHtml ? "mailBodyHtml" : "mailBody", x.user.GetUserCulture(_textService, _globalSettings), new[] { @@ -274,7 +274,7 @@ namespace Umbraco.Web.Compose x.body.ItemId, //format the summary depending on if it's variant or not contentVariantGroup.Key == ContentVariation.Culture - ? (x.isHtml ? _textService.Localize("notifications/mailBodyVariantHtmlSummary", new[]{ x.body.Summary }) : _textService.Localize("notifications/mailBodyVariantSummary", new []{ x.body.Summary })) + ? (x.isHtml ? _textService.Localize("notifications","mailBodyVariantHtmlSummary", new[]{ x.body.Summary }) : _textService.Localize("notifications","mailBodyVariantSummary", new []{ x.body.Summary })) : x.body.Summary, x.body.ItemUrl })); diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index e2ab829427..d5f4ef9fe8 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -337,7 +337,7 @@ namespace Umbraco.Web.Editors new[] { identityUser.UserName, callbackUrl }); await UserManager.SendEmailAsync(identityUser.Id, - Services.TextService.Localize("login/resetPasswordEmailCopySubject", + Services.TextService.Localize("login", "resetPasswordEmailCopySubject", // Ensure the culture of the found user is used for the email! UserExtensions.GetUserCulture(identityUser.Culture, Services.TextService, GlobalSettings)), message); diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 18740d41fc..2959a983bd 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -347,7 +347,7 @@ namespace Umbraco.Web.Editors } //Add error and redirect for it to be displayed - TempData[ViewDataExtensions.TokenPasswordResetCode] = new[] { Services.TextService.Localize("login/resetCodeExpired") }; + TempData[ViewDataExtensions.TokenPasswordResetCode] = new[] { Services.TextService.Localize("login", "resetCodeExpired") }; return RedirectToLocal(Url.Action("Default", "BackOffice")); } diff --git a/src/Umbraco.Web/Editors/CodeFileController.cs b/src/Umbraco.Web/Editors/CodeFileController.cs index 0e1c4b3e60..967d0d14c7 100644 --- a/src/Umbraco.Web/Editors/CodeFileController.cs +++ b/src/Umbraco.Web/Editors/CodeFileController.cs @@ -88,7 +88,7 @@ namespace Umbraco.Web.Editors if (string.IsNullOrWhiteSpace(parentId)) throw new ArgumentException("Value cannot be null or whitespace.", "parentId"); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name"); if (name.ContainsAny(Path.GetInvalidPathChars())) { - return Request.CreateNotificationValidationErrorResponse(Services.TextService.Localize("codefile/createFolderIllegalChars")); + return Request.CreateNotificationValidationErrorResponse(Services.TextService.Localize("codefile", "createFolderIllegalChars")); } // if the parentId is root (-1) then we just need an empty string as we are @@ -388,8 +388,8 @@ namespace Umbraco.Web.Editors } display.AddErrorNotification( - Services.TextService.Localize("speechBubbles/partialViewErrorHeader"), - Services.TextService.Localize("speechBubbles/partialViewErrorText")); + Services.TextService.Localize("speechBubbles", "partialViewErrorHeader"), + Services.TextService.Localize("speechBubbles", "partialViewErrorText")); break; case Core.Constants.Trees.PartialViewMacros: @@ -403,8 +403,8 @@ namespace Umbraco.Web.Editors } display.AddErrorNotification( - Services.TextService.Localize("speechBubbles/partialViewErrorHeader"), - Services.TextService.Localize("speechBubbles/partialViewErrorText")); + Services.TextService.Localize("speechBubbles", "partialViewErrorHeader"), + Services.TextService.Localize("speechBubbles", "partialViewErrorText")); break; case Core.Constants.Trees.Scripts: diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 6f85a08751..ed629d746a 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -242,7 +242,7 @@ namespace Umbraco.Web.Editors new ContentVariantDisplay { CreateDate = DateTime.Now, - Name = Services.TextService.Localize("general/recycleBin") + Name = Services.TextService.Localize("general","recycleBin") } }, ContentApps = apps @@ -567,8 +567,8 @@ namespace Umbraco.Web.Editors var notificationModel = new SimpleNotificationModel(); notificationModel.AddSuccessNotification( - Services.TextService.Localize("blueprints/createdBlueprintHeading"), - Services.TextService.Localize("blueprints/createdBlueprintMessage", new[] { content.Name }) + Services.TextService.Localize("blueprints", "createdBlueprintHeading"), + Services.TextService.Localize("blueprints", "createdBlueprintMessage", new[] { content.Name }) ); return notificationModel; @@ -579,7 +579,7 @@ namespace Umbraco.Web.Editors var existing = Services.ContentService.GetBlueprintsForContentTypes(content.ContentTypeId); if (existing.Any(x => x.Name == name && x.Id != content.Id)) { - ModelState.AddModelError(modelName, Services.TextService.Localize("blueprints/duplicateBlueprintMessage")); + ModelState.AddModelError(modelName, Services.TextService.Localize("blueprints", "duplicateBlueprintMessage")); throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); } } @@ -741,15 +741,15 @@ namespace Umbraco.Web.Editors var variantName = GetVariantName(culture, segment); AddSuccessNotification(notifications, culture, segment, - Services.TextService.Localize("speechBubbles/editContentSendToPublish"), - Services.TextService.Localize("speechBubbles/editVariantSendToPublishText", new[] { variantName })); + Services.TextService.Localize("speechBubbles", "editContentSendToPublish"), + Services.TextService.Localize("speechBubbles", "editVariantSendToPublishText", new[] { variantName })); } } else if (ModelState.IsValid) { globalNotifications.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSendToPublish"), - Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); + Services.TextService.Localize("speechBubbles", "editContentSendToPublish"), + Services.TextService.Localize("speechBubbles", "editContentSendToPublishText")); } } break; @@ -766,8 +766,8 @@ namespace Umbraco.Web.Editors if (!ValidatePublishBranchPermissions(contentItem, out var noAccess)) { globalNotifications.AddErrorNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/invalidPublishBranchPermissions")); + Services.TextService.Localize(null,"publish"), + Services.TextService.Localize("publish", "invalidPublishBranchPermissions")); wasCancelled = false; break; } @@ -782,8 +782,8 @@ namespace Umbraco.Web.Editors if (!ValidatePublishBranchPermissions(contentItem, out var noAccess)) { globalNotifications.AddErrorNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/invalidPublishBranchPermissions")); + Services.TextService.Localize(null,"publish"), + Services.TextService.Localize("publish", "invalidPublishBranchPermissions")); wasCancelled = false; break; } @@ -873,7 +873,7 @@ namespace Umbraco.Web.Editors //if there's more than 1 variant, then we need to add the culture specific error //messages based on the variants in error so that the messages show in the publish/save dialog if (variants.Count > 1) - AddVariantValidationError(variant.Culture, variant.Segment, "publish/contentPublishedFailedByMissingName"); + AddVariantValidationError(variant.Culture, variant.Segment, "publish","contentPublishedFailedByMissingName"); else return false; //It's invariant and is missing critical data, it cannot be saved } @@ -911,14 +911,14 @@ namespace Umbraco.Web.Editors /// /// /// - /// + /// /// /// /// Method is used for normal Saving and Scheduled Publishing /// private void SaveAndNotify(ContentItemSave contentItem, Func saveMethod, int variantCount, Dictionary notifications, SimpleNotificationModel globalNotifications, - string invariantSavedLocalizationKey, string variantSavedLocalizationKey, string cultureForInvariantErrors, + string invariantSavedLocalizationKey, string variantSavedLocalizationAlias, string cultureForInvariantErrors, out bool wasCancelled) { var saveResult = saveMethod(contentItem.PersistedContent); @@ -938,14 +938,14 @@ namespace Umbraco.Web.Editors var variantName = GetVariantName(culture, segment); AddSuccessNotification(notifications, culture, segment, - Services.TextService.Localize("speechBubbles/editContentSavedHeader"), - Services.TextService.Localize(variantSavedLocalizationKey, new[] { variantName })); + Services.TextService.Localize("speechBubbles", "editContentSavedHeader"), + Services.TextService.Localize(null,variantSavedLocalizationAlias, new[] { variantName })); } } else if (ModelState.IsValid) { globalNotifications.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSavedHeader"), + Services.TextService.Localize("speechBubbles", "editContentSavedHeader"), Services.TextService.Localize(invariantSavedLocalizationKey)); } } @@ -1073,7 +1073,7 @@ namespace Umbraco.Web.Editors { //can't continue, a mandatory variant is not published and not scheduled for publishing // TODO: Add segment - AddVariantValidationError(culture, null, "speechBubbles/scheduleErrReleaseDate2"); + AddVariantValidationError(culture, null, "speechBubbles", "scheduleErrReleaseDate2"); isValid = false; continue; } @@ -1081,7 +1081,7 @@ namespace Umbraco.Web.Editors { //can't continue, a mandatory variant is not published and it's scheduled for publishing after a non-mandatory // TODO: Add segment - AddVariantValidationError(culture, null, "speechBubbles/scheduleErrReleaseDate3"); + AddVariantValidationError(culture, null, "speechBubbles", "scheduleErrReleaseDate3"); isValid = false; continue; } @@ -1095,7 +1095,7 @@ namespace Umbraco.Web.Editors //1) release date cannot be less than now if (variant.ReleaseDate.HasValue && variant.ReleaseDate < DateTime.Now) { - AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles/scheduleErrReleaseDate1"); + AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles", "scheduleErrReleaseDate1"); isValid = false; continue; } @@ -1103,7 +1103,7 @@ namespace Umbraco.Web.Editors //2) expire date cannot be less than now if (variant.ExpireDate.HasValue && variant.ExpireDate < DateTime.Now) { - AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles/scheduleErrExpireDate1"); + AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles", "scheduleErrExpireDate1"); isValid = false; continue; } @@ -1111,7 +1111,7 @@ namespace Umbraco.Web.Editors //3) expire date cannot be less than release date if (variant.ExpireDate.HasValue && variant.ReleaseDate.HasValue && variant.ExpireDate <= variant.ReleaseDate) { - AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles/scheduleErrExpireDate2"); + AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles", "scheduleErrExpireDate2"); isValid = false; continue; } @@ -1360,19 +1360,19 @@ namespace Umbraco.Web.Editors if (r.publishing && !r.isValid) { //flagged for publishing but the mandatory culture is invalid - AddVariantValidationError(r.model.Culture, r.model.Segment, "publish/contentPublishedFailedReqCultureValidationError"); + AddVariantValidationError(r.model.Culture, r.model.Segment, "publish", "contentPublishedFailedReqCultureValidationError"); canPublish = false; } else if (r.publishing && r.isValid && firstInvalidMandatoryCulture != null) { //in this case this culture also cannot be published because another mandatory culture is invalid - AddVariantValidationError(r.model.Culture, r.model.Segment, "publish/contentPublishedFailedReqCultureValidationError", firstInvalidMandatoryCulture); + AddVariantValidationError(r.model.Culture, r.model.Segment, "publish", "contentPublishedFailedReqCultureValidationError", firstInvalidMandatoryCulture); canPublish = false; } else if (!r.publishing) { //cannot continue publishing since a required culture that is not currently being published isn't published - AddVariantValidationError(r.model.Culture, r.model.Segment, "speechBubbles/contentReqCulturePublishError"); + AddVariantValidationError(r.model.Culture, r.model.Segment, "speechBubbles", "contentReqCulturePublishError"); canPublish = false; } } @@ -1397,7 +1397,7 @@ namespace Umbraco.Web.Editors var valid = persistentContent.PublishCulture(CultureImpact.Explicit(variant.Culture, defaultCulture.InvariantEquals(variant.Culture))); if (!valid) { - AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles/contentCultureValidationError"); + AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles", "contentCultureValidationError"); return false; } } @@ -1414,12 +1414,12 @@ namespace Umbraco.Web.Editors /// /// The culture used in the localization message, null by default which means will be used. /// - private void AddVariantValidationError(string culture, string segment, string localizationKey, string cultureToken = null) + private void AddVariantValidationError(string culture, string segment, string localizationArea,string localizationAlias, string cultureToken = null) { var cultureToUse = cultureToken ?? culture; var variantName = GetVariantName(cultureToUse, segment); - var errMsg = Services.TextService.Localize(localizationKey, new[] { variantName }); + var errMsg = Services.TextService.Localize(localizationArea, localizationAlias, new[] { variantName }); ModelState.AddVariantValidationError(culture, segment, errMsg); } @@ -1554,7 +1554,7 @@ namespace Umbraco.Web.Editors { Services.ContentService.EmptyRecycleBin(Security.GetUserId().ResultOr(Constants.Security.SuperUserId)); - return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs/recycleBinIsEmpty")); + return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs", "recycleBinIsEmpty")); } /// @@ -1662,8 +1662,8 @@ namespace Umbraco.Web.Editors else { content.AddSuccessNotification( - Services.TextService.Localize("content/unpublish"), - Services.TextService.Localize("speechBubbles/contentUnpublished")); + Services.TextService.Localize("content", "unpublish"), + Services.TextService.Localize("speechBubbles", "contentUnpublished")); return content; } } @@ -1688,8 +1688,8 @@ namespace Umbraco.Web.Editors if (results.Any(x => x.Value.Result == PublishResultType.SuccessUnpublishMandatoryCulture)) { content.AddSuccessNotification( - Services.TextService.Localize("content/unpublish"), - Services.TextService.Localize("speechBubbles/contentMandatoryCultureUnpublished")); + Services.TextService.Localize("content", "unpublish"), + Services.TextService.Localize("speechBubbles", "contentMandatoryCultureUnpublished")); return content; } @@ -1697,8 +1697,8 @@ namespace Umbraco.Web.Editors foreach (var r in results) { content.AddSuccessNotification( - Services.TextService.Localize("content/unpublish"), - Services.TextService.Localize("speechBubbles/contentCultureUnpublished", new[] { _allLangs.Value[r.Key].CultureName })); + Services.TextService.Localize("conten", "unpublish"), + Services.TextService.Localize("speechBubbles", "contentCultureUnpublished", new[] { _allLangs.Value[r.Key].CultureName })); } return content; @@ -1729,7 +1729,7 @@ namespace Umbraco.Web.Editors } catch (UriFormatException) { - var response = Request.CreateValidationErrorResponse(Services.TextService.Localize("assignDomain/invalidDomain")); + var response = Request.CreateValidationErrorResponse(Services.TextService.Localize("assignDomain", "invalidDomain")); throw new HttpResponseException(response); } } @@ -1887,7 +1887,7 @@ namespace Umbraco.Web.Editors foreach (var (culture, segment) in variantErrors) { - AddVariantValidationError(culture, segment, "speechBubbles/contentCultureValidationError"); + AddVariantValidationError(culture, segment, "speechBubbles", "contentCultureValidationError"); } } @@ -2012,7 +2012,7 @@ namespace Umbraco.Web.Editors { throw new HttpResponseException( Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"))); + Services.TextService.Localize("moveOrCopy", "notAllowedAtRoot"))); } } else @@ -2030,7 +2030,7 @@ namespace Umbraco.Web.Editors { throw new HttpResponseException( Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); + Services.TextService.Localize("moveOrCopy", "notAllowedByContentType"))); } // Check on paths @@ -2038,7 +2038,7 @@ namespace Umbraco.Web.Editors { throw new HttpResponseException( Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedByPath"))); + Services.TextService.Localize("moveOrCopy", "notAllowedByPath"))); } } @@ -2107,16 +2107,16 @@ namespace Umbraco.Web.Editors { //either invariant single publish, or bulk publish where all statuses are already published display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), - Services.TextService.Localize("speechBubbles/editContentPublishedText")); + Services.TextService.Localize("speechBubbles", "editContentPublishedHeader"), + Services.TextService.Localize("speechBubbles", "editContentPublishedText")); } else { foreach (var c in successfulCultures) { display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), - Services.TextService.Localize("speechBubbles/editVariantPublishedText", new[] { _allLangs.Value[c].CultureName })); + Services.TextService.Localize("speechBubbles", "editContentPublishedHeader"), + Services.TextService.Localize("speechBubbles", "editVariantPublishedText", new[] { _allLangs.Value[c].CultureName })); } } } @@ -2132,20 +2132,20 @@ namespace Umbraco.Web.Editors if (successfulCultures == null) { display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), + Services.TextService.Localize("speechBubbles", "editContentPublishedHeader"), totalStatusCount > 1 - ? Services.TextService.Localize("speechBubbles/editMultiContentPublishedText", new[] { itemCount.ToInvariantString() }) - : Services.TextService.Localize("speechBubbles/editContentPublishedText")); + ? Services.TextService.Localize("speechBubbles", "editMultiContentPublishedText", new[] { itemCount.ToInvariantString() }) + : Services.TextService.Localize("speechBubbles", "editContentPublishedText")); } else { foreach (var c in successfulCultures) { display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), + Services.TextService.Localize("speechBubbles", "editContentPublishedHeader"), totalStatusCount > 1 - ? Services.TextService.Localize("speechBubbles/editMultiVariantPublishedText", new[] { itemCount.ToInvariantString(), _allLangs.Value[c].CultureName }) - : Services.TextService.Localize("speechBubbles/editVariantPublishedText", new[] { _allLangs.Value[c].CultureName })); + ? Services.TextService.Localize("speechBubbles", "editMultiVariantPublishedText", new[] { itemCount.ToInvariantString(), _allLangs.Value[c].CultureName }) + : Services.TextService.Localize("speechBubbles", "editVariantPublishedText", new[] { _allLangs.Value[c].CultureName })); } } } @@ -2155,8 +2155,8 @@ namespace Umbraco.Web.Editors //TODO: This doesn't take into account variations with the successfulCultures param var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedByParent", + Services.TextService.Localize(null,"publish"), + Services.TextService.Localize("publish", "contentPublishedFailedByParent", new[] { names }).Trim()); } break; @@ -2172,8 +2172,8 @@ namespace Umbraco.Web.Editors //TODO: This doesn't take into account variations with the successfulCultures param var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", + Services.TextService.Localize(null,"publish"), + Services.TextService.Localize("publish", "contentPublishedFailedAwaitingRelease", new[] { names }).Trim()); } break; @@ -2182,8 +2182,8 @@ namespace Umbraco.Web.Editors //TODO: This doesn't take into account variations with the successfulCultures param var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedExpired", + Services.TextService.Localize(null,"publish"), + Services.TextService.Localize("publish", "contentPublishedFailedExpired", new[] { names }).Trim()); } break; @@ -2192,8 +2192,8 @@ namespace Umbraco.Web.Editors //TODO: This doesn't take into account variations with the successfulCultures param var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedIsTrashed", + Services.TextService.Localize(null,"publish"), + Services.TextService.Localize("publish", "contentPublishedFailedIsTrashed", new[] { names }).Trim()); } break; @@ -2203,8 +2203,8 @@ namespace Umbraco.Web.Editors { var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedInvalid", + Services.TextService.Localize(null,"publish"), + Services.TextService.Localize("publish", "contentPublishedFailedInvalid", new[] { names }).Trim()); } else @@ -2213,8 +2213,8 @@ namespace Umbraco.Web.Editors { var names = string.Join(", ", status.Select(x => $"'{(x.Content.ContentType.VariesByCulture() ? x.Content.GetCultureName(c) : x.Content.Name)}'")); display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedInvalid", + Services.TextService.Localize(null,"publish"), + Services.TextService.Localize("publish", "contentPublishedFailedInvalid", new[] { names }).Trim()); } } @@ -2222,7 +2222,7 @@ namespace Umbraco.Web.Editors break; case PublishResultType.FailedPublishMandatoryCultureMissing: display.AddWarningNotification( - Services.TextService.Localize("publish"), + Services.TextService.Localize(null,"publish"), "publish/contentPublishedFailedByCulture"); break; default: @@ -2355,13 +2355,13 @@ namespace Umbraco.Web.Editors case OperationResultType.NoOperation: default: notificationModel.AddErrorNotification( - Services.TextService.Localize("speechBubbles/operationFailedHeader"), + Services.TextService.Localize("speechBubbles", "operationFailedHeader"), null); // TODO: There is no specific failed to save error message AFAIK break; case OperationResultType.FailedCancelledByEvent: notificationModel.AddErrorNotification( - Services.TextService.Localize("speechBubbles/operationCancelledHeader"), - Services.TextService.Localize("speechBubbles/operationCancelledText")); + Services.TextService.Localize("speechBubbles", "operationCancelledHeader"), + Services.TextService.Localize("speechBubbles", "operationCancelledText")); break; } diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index d426cb1f56..89f0b99d7f 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -384,7 +384,7 @@ namespace Umbraco.Web.Editors var display = Mapper.Map(savedCt); display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/contentTypeSavedHeader"), + Services.TextService.Localize("speechBubbles", "contentTypeSavedHeader"), string.Empty); return display; @@ -666,8 +666,8 @@ namespace Umbraco.Web.Editors else { model.Notifications.Add(new Notification( - Services.TextService.Localize("speechBubbles/operationFailedHeader"), - Services.TextService.Localize("media/disallowedFileType"), + Services.TextService.Localize("speechBubbles", "operationFailedHeader"), + Services.TextService.Localize("media", "disallowedFileType"), NotificationStyle.Warning)); } diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index d56820a549..8eefbb616b 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -256,7 +256,7 @@ namespace Umbraco.Web.Editors var exists = allAliases.InvariantContains(contentTypeSave.Alias); if (exists && (ctId == 0 || !ct.Alias.InvariantEquals(contentTypeSave.Alias))) { - ModelState.AddModelError("Alias", Services.TextService.Localize("editcontenttype/aliasAlreadyExists")); + ModelState.AddModelError("Alias", Services.TextService.Localize("editcontenttype", "aliasAlreadyExists")); } // execute the external validators @@ -388,7 +388,7 @@ namespace Umbraco.Web.Editors return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); case MoveOperationStatusType.FailedNotAllowedByPath: var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy", "notAllowedByPath"), ""); return Request.CreateValidationErrorResponse(notificationModel); default: throw new ArgumentOutOfRangeException(); @@ -432,7 +432,7 @@ namespace Umbraco.Web.Editors return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); case MoveOperationStatusType.FailedNotAllowedByPath: var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy", "notAllowedByPath"), ""); return Request.CreateValidationErrorResponse(notificationModel); default: throw new ArgumentOutOfRangeException(); diff --git a/src/Umbraco.Web/Editors/CurrentUserController.cs b/src/Umbraco.Web/Editors/CurrentUserController.cs index 46151f2a9e..8da2243255 100644 --- a/src/Umbraco.Web/Editors/CurrentUserController.cs +++ b/src/Umbraco.Web/Editors/CurrentUserController.cs @@ -183,7 +183,7 @@ namespace Umbraco.Web.Editors //even if we weren't resetting this, it is the correct value (null), otherwise if we were resetting then it will contain the new pword var result = new ModelWithNotifications(passwordChangeResult.Result.ResetPassword); - result.AddSuccessNotification(Services.TextService.Localize("user/password"), Services.TextService.Localize("user/passwordChanged")); + result.AddSuccessNotification(Services.TextService.Localize("user", "password"), Services.TextService.Localize("user", "passwordChanged")); return result; } diff --git a/src/Umbraco.Web/Editors/DataTypeController.cs b/src/Umbraco.Web/Editors/DataTypeController.cs index ad92d40ecf..76b8d590f2 100644 --- a/src/Umbraco.Web/Editors/DataTypeController.cs +++ b/src/Umbraco.Web/Editors/DataTypeController.cs @@ -281,7 +281,7 @@ namespace Umbraco.Web.Editors // map back to display model, and return var display = Mapper.Map(dataType.PersistedDataType); - display.AddSuccessNotification(Services.TextService.Localize("speechBubbles/dataTypeSaved"), ""); + display.AddSuccessNotification(Services.TextService.Localize("speechBubbles", "dataTypeSaved"), ""); return display; } @@ -316,7 +316,7 @@ namespace Umbraco.Web.Editors return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); case MoveOperationStatusType.FailedNotAllowedByPath: var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy", "notAllowedByPath"), ""); return Request.CreateValidationErrorResponse(notificationModel); default: throw new ArgumentOutOfRangeException(); diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index dfe6939552..758fb85898 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -106,7 +106,7 @@ namespace Umbraco.Web.Editors Id = Constants.System.RecycleBinMedia, Alias = "recycleBin", ParentId = -1, - Name = Services.TextService.Localize("general/recycleBin"), + Name = Services.TextService.Localize("general", "recycleBin"), ContentTypeAlias = "recycleBin", CreateDate = DateTime.Now, IsContainer = true, @@ -431,7 +431,7 @@ namespace Umbraco.Web.Editors if (sourceParentID == destinationParentID) { - return Request.CreateValidationErrorResponse(new SimpleNotificationModel(new Notification("",Services.TextService.Localize("media/moveToSameFolderFailed"),NotificationStyle.Error))); + return Request.CreateValidationErrorResponse(new SimpleNotificationModel(new Notification("",Services.TextService.Localize("media", "moveToSameFolderFailed"),NotificationStyle.Error))); } if (moveResult == false) { @@ -519,8 +519,8 @@ namespace Umbraco.Web.Editors if (saveStatus.Success) { display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editMediaSaved"), - Services.TextService.Localize("speechBubbles/editMediaSavedText")); + Services.TextService.Localize("speechBubbles", "editMediaSaved"), + Services.TextService.Localize("speechBubbles", "editMediaSavedText")); } else { @@ -551,7 +551,7 @@ namespace Umbraco.Web.Editors { Services.MediaService.EmptyRecycleBin(Security.GetUserId().ResultOr(Constants.Security.SuperUserId)); - return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs/recycleBinIsEmpty")); + return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs", "recycleBinIsEmpty")); } /// @@ -730,7 +730,7 @@ namespace Umbraco.Web.Editors if (saveResult == false) { AddCancelMessage(tempFiles, - message: Services.TextService.Localize("speechBubbles/operationCancelledText") + " -- " + mediaItemName); + message: Services.TextService.Localize("speechBubbles", "operationCancelledText") + " -- " + mediaItemName); } else { @@ -745,8 +745,8 @@ namespace Umbraco.Web.Editors else { tempFiles.Notifications.Add(new Notification( - Services.TextService.Localize("speechBubbles/operationFailedHeader"), - Services.TextService.Localize("media/disallowedFileType"), + Services.TextService.Localize("speechBubbles", "operationFailedHeader"), + Services.TextService.Localize("media", "disallowedFileType"), NotificationStyle.Warning)); } } @@ -840,8 +840,8 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(Request.CreateResponse( HttpStatusCode.Forbidden, new SimpleNotificationModel(new Notification( - Services.TextService.Localize("speechBubbles/operationFailedHeader"), - Services.TextService.Localize("speechBubbles/invalidUserPermissionsText"), + Services.TextService.Localize("speechBubbles", "operationFailedHeader"), + Services.TextService.Localize("speechBubbles", "invalidUserPermissionsText"), NotificationStyle.Warning)))); } @@ -874,7 +874,7 @@ namespace Umbraco.Web.Editors if (toMove.ContentType.AllowedAsRoot == false && mediaTypeService.GetAll().Any(ct => ct.AllowedAsRoot)) { var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"), ""); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy", "notAllowedAtRoot"), ""); throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); } } @@ -892,7 +892,7 @@ namespace Umbraco.Web.Editors .Any(x => x.Value == toMove.ContentType.Id) == false) { var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByContentType"), ""); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy", "notAllowedByContentType"), ""); throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); } @@ -900,7 +900,7 @@ namespace Umbraco.Web.Editors if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) { var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy", "notAllowedByPath"), ""); throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); } } diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index d816f13c92..3b7c3fb4d9 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -250,7 +250,7 @@ namespace Umbraco.Web.Editors var display = Mapper.Map(savedCt); display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/mediaTypeSavedHeader"), + Services.TextService.Localize("speechBubbles", "mediaTypeSavedHeader"), string.Empty); return display; diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index 477dced7d0..2f6fb621b0 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -377,7 +377,7 @@ namespace Umbraco.Web.Editors { case ContentSaveAction.Save: case ContentSaveAction.SaveNew: - display.AddSuccessNotification(localizedTextService.Localize("speechBubbles/editMemberSaved"), localizedTextService.Localize("speechBubbles/editMemberSaved")); + display.AddSuccessNotification(localizedTextService.Localize("speechBubbles", "editMemberSaved"), localizedTextService.Localize("speechBubbles", "editMemberSaved")); break; } diff --git a/src/Umbraco.Web/Editors/MemberGroupController.cs b/src/Umbraco.Web/Editors/MemberGroupController.cs index ed11139e56..19adf17a2a 100644 --- a/src/Umbraco.Web/Editors/MemberGroupController.cs +++ b/src/Umbraco.Web/Editors/MemberGroupController.cs @@ -152,7 +152,7 @@ namespace Umbraco.Web.Editors var display = Mapper.Map(memberGroup); display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/memberGroupSavedHeader"), + Services.TextService.Localize("speechBubbles", "memberGroupSavedHeader"), string.Empty); return display; diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index 4bfea76eda..d55afd5e95 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -231,7 +231,7 @@ namespace Umbraco.Web.Editors var display = Mapper.Map(savedCt); display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/memberTypeSavedHeader"), + Services.TextService.Localize("speechBubbles", "memberTypeSavedHeader"), string.Empty); return display; diff --git a/src/Umbraco.Web/Editors/PackageInstallController.cs b/src/Umbraco.Web/Editors/PackageInstallController.cs index 1030498734..d12308aeee 100644 --- a/src/Umbraco.Web/Editors/PackageInstallController.cs +++ b/src/Umbraco.Web/Editors/PackageInstallController.cs @@ -180,7 +180,7 @@ namespace Umbraco.Web.Editors { //this package is already installed throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("packager/packageAlreadyInstalled"))); + Services.TextService.Localize("packager", "packageAlreadyInstalled"))); } model.OriginalVersion = installType == PackageInstallType.Upgrade ? alreadyInstalled.Version : null; @@ -189,8 +189,8 @@ namespace Umbraco.Web.Editors else { model.Notifications.Add(new Notification( - Services.TextService.Localize("speechBubbles/operationFailedHeader"), - Services.TextService.Localize("media/disallowedFileType"), + Services.TextService.Localize("speechBubbles", "operationFailedHeader"), + Services.TextService.Localize("media", "disallowedFileType"), NotificationStyle.Warning)); } @@ -234,7 +234,7 @@ namespace Umbraco.Web.Editors if (installType == PackageInstallType.AlreadyInstalled) { throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("packager/packageAlreadyInstalled"))); + Services.TextService.Localize("packager", "packageAlreadyInstalled"))); } model.OriginalVersion = installType == PackageInstallType.Upgrade ? alreadyInstalled.Version : null; @@ -260,7 +260,7 @@ namespace Umbraco.Web.Editors var packageMinVersion = packageInfo.UmbracoVersion; if (UmbracoVersion.Current < packageMinVersion) throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("packager/targetVersionMismatch", new[] {packageMinVersion.ToString()}))); + Services.TextService.Localize("packager", "targetVersionMismatch", new[] {packageMinVersion.ToString()}))); } var installType = Services.PackagingService.GetPackageInstallType(packageInfo.Name, SemVersion.Parse(packageInfo.Version), out var alreadyInstalled); diff --git a/src/Umbraco.Web/Editors/TemplateQueryController.cs b/src/Umbraco.Web/Editors/TemplateQueryController.cs index 1f07267802..a3b19cc9fe 100644 --- a/src/Umbraco.Web/Editors/TemplateQueryController.cs +++ b/src/Umbraco.Web/Editors/TemplateQueryController.cs @@ -21,28 +21,28 @@ namespace Umbraco.Web.Editors { private IEnumerable Terms => new List { - new OperatorTerm(Services.TextService.Localize("template/is"), Operator.Equals, new [] {"string"}), - new OperatorTerm(Services.TextService.Localize("template/isNot"), Operator.NotEquals, new [] {"string"}), - new OperatorTerm(Services.TextService.Localize("template/before"), Operator.LessThan, new [] {"datetime"}), - new OperatorTerm(Services.TextService.Localize("template/beforeIncDate"), Operator.LessThanEqualTo, new [] {"datetime"}), - new OperatorTerm(Services.TextService.Localize("template/after"), Operator.GreaterThan, new [] {"datetime"}), - new OperatorTerm(Services.TextService.Localize("template/afterIncDate"), Operator.GreaterThanEqualTo, new [] {"datetime"}), - new OperatorTerm(Services.TextService.Localize("template/equals"), Operator.Equals, new [] {"int"}), - new OperatorTerm(Services.TextService.Localize("template/doesNotEqual"), Operator.NotEquals, new [] {"int"}), - new OperatorTerm(Services.TextService.Localize("template/contains"), Operator.Contains, new [] {"string"}), - new OperatorTerm(Services.TextService.Localize("template/doesNotContain"), Operator.NotContains, new [] {"string"}), - new OperatorTerm(Services.TextService.Localize("template/greaterThan"), Operator.GreaterThan, new [] {"int"}), - new OperatorTerm(Services.TextService.Localize("template/greaterThanEqual"), Operator.GreaterThanEqualTo, new [] {"int"}), - new OperatorTerm(Services.TextService.Localize("template/lessThan"), Operator.LessThan, new [] {"int"}), - new OperatorTerm(Services.TextService.Localize("template/lessThanEqual"), Operator.LessThanEqualTo, new [] {"int"}) + new OperatorTerm(Services.TextService.Localize("template","is"), Operator.Equals, new [] {"string"}), + new OperatorTerm(Services.TextService.Localize("template","isNot"), Operator.NotEquals, new [] {"string"}), + new OperatorTerm(Services.TextService.Localize("template","before"), Operator.LessThan, new [] {"datetime"}), + new OperatorTerm(Services.TextService.Localize("template","beforeIncDate"), Operator.LessThanEqualTo, new [] {"datetime"}), + new OperatorTerm(Services.TextService.Localize("template","after"), Operator.GreaterThan, new [] {"datetime"}), + new OperatorTerm(Services.TextService.Localize("template","afterIncDate"), Operator.GreaterThanEqualTo, new [] {"datetime"}), + new OperatorTerm(Services.TextService.Localize("template","equals"), Operator.Equals, new [] {"int"}), + new OperatorTerm(Services.TextService.Localize("template","doesNotEqual"), Operator.NotEquals, new [] {"int"}), + new OperatorTerm(Services.TextService.Localize("template","contains"), Operator.Contains, new [] {"string"}), + new OperatorTerm(Services.TextService.Localize("template","doesNotContain"), Operator.NotContains, new [] {"string"}), + new OperatorTerm(Services.TextService.Localize("template","greaterThan"), Operator.GreaterThan, new [] {"int"}), + new OperatorTerm(Services.TextService.Localize("template","greaterThanEqual"), Operator.GreaterThanEqualTo, new [] {"int"}), + new OperatorTerm(Services.TextService.Localize("template","lessThan"), Operator.LessThan, new [] {"int"}), + new OperatorTerm(Services.TextService.Localize("template","lessThanEqual"), Operator.LessThanEqualTo, new [] {"int"}) }; private IEnumerable Properties => new List { - new PropertyModel { Name = Services.TextService.Localize("template/id"), Alias = "Id", Type = "int" }, - new PropertyModel { Name = Services.TextService.Localize("template/name"), Alias = "Name", Type = "string" }, - new PropertyModel { Name = Services.TextService.Localize("template/createdDate"), Alias = "CreateDate", Type = "datetime" }, - new PropertyModel { Name = Services.TextService.Localize("template/lastUpdatedDate"), Alias = "UpdateDate", Type = "datetime" } + new PropertyModel { Name = Services.TextService.Localize("template","id"), Alias = "Id", Type = "int" }, + new PropertyModel { Name = Services.TextService.Localize("template","name"), Alias = "Name", Type = "string" }, + new PropertyModel { Name = Services.TextService.Localize("template","createdDate"), Alias = "CreateDate", Type = "datetime" }, + new PropertyModel { Name = Services.TextService.Localize("template","lastUpdatedDate"), Alias = "UpdateDate", Type = "datetime" } }; public QueryResultModel PostTemplateQuery(QueryModel model) @@ -206,10 +206,10 @@ namespace Umbraco.Web.Editors public IEnumerable GetContentTypes() { var contentTypes = Services.ContentTypeService.GetAll() - .Select(x => new ContentTypeModel { Alias = x.Alias, Name = Services.TextService.Localize("template/contentOfType", tokens: new string[] { x.Name }) }) + .Select(x => new ContentTypeModel { Alias = x.Alias, Name = Services.TextService.Localize("template", "contentOfType", tokens: new string[] { x.Name }) }) .OrderBy(x => x.Name).ToList(); - contentTypes.Insert(0, new ContentTypeModel { Alias = string.Empty, Name = Services.TextService.Localize("template/allContent") }); + contentTypes.Insert(0, new ContentTypeModel { Alias = string.Empty, Name = Services.TextService.Localize("template", "allContent") }); return contentTypes; } diff --git a/src/Umbraco.Web/Editors/UserGroupsController.cs b/src/Umbraco.Web/Editors/UserGroupsController.cs index b081ca6137..03e6c5b8b7 100644 --- a/src/Umbraco.Web/Editors/UserGroupsController.cs +++ b/src/Umbraco.Web/Editors/UserGroupsController.cs @@ -78,7 +78,7 @@ namespace Umbraco.Web.Editors var display = Mapper.Map(userGroupSave.PersistedUserGroup); - display.AddSuccessNotification(Services.TextService.Localize("speechBubbles/operationSavedHeader"), Services.TextService.Localize("speechBubbles/editUserGroupSaved")); + display.AddSuccessNotification(Services.TextService.Localize("speechBubbles", "operationSavedHeader"), Services.TextService.Localize("speechBubbles", "editUserGroupSaved")); return display; } @@ -163,9 +163,9 @@ namespace Umbraco.Web.Editors } if (userGroups.Length > 1) return Request.CreateNotificationSuccessResponse( - Services.TextService.Localize("speechBubbles/deleteUserGroupsSuccess", new[] {userGroups.Length.ToString()})); + Services.TextService.Localize("speechBubbles", "deleteUserGroupsSuccess", new[] {userGroups.Length.ToString()})); return Request.CreateNotificationSuccessResponse( - Services.TextService.Localize("speechBubbles/deleteUserGroupSuccess", new[] {userGroups[0].Name})); + Services.TextService.Localize("speechBubbles", "deleteUserGroupSuccess", new[] {userGroups[0].Name})); } } } diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index b022e6f27a..72162dd179 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -470,7 +470,7 @@ namespace Umbraco.Web.Editors await SendUserInviteEmailAsync(display, Security.CurrentUser.Name, Security.CurrentUser.Email, user, userSave.Message); } - display.AddSuccessNotification(Services.TextService.Localize("speechBubbles/resendInviteHeader"), Services.TextService.Localize("speechBubbles/resendInviteSuccess", new[] { user.Name })); + display.AddSuccessNotification(Services.TextService.Localize("speechBubbles", "resendInviteHeader"), Services.TextService.Localize("speechBubbles", "resendInviteSuccess", new[] { user.Name })); return display; } @@ -529,10 +529,10 @@ namespace Umbraco.Web.Editors var applicationUri = RuntimeState.ApplicationUrl; var inviteUri = new Uri(applicationUri, action); - var emailSubject = Services.TextService.Localize("user/inviteEmailCopySubject", + var emailSubject = Services.TextService.Localize("user", "inviteEmailCopySubject", //Ensure the culture of the found user is used for the email! UserExtensions.GetUserCulture(to.Language, Services.TextService, GlobalSettings)); - var emailBody = Services.TextService.Localize("user/inviteEmailCopyFormat", + var emailBody = Services.TextService.Localize("user", "inviteEmailCopyFormat", //Ensure the culture of the found user is used for the email! UserExtensions.GetUserCulture(to.Language, Services.TextService, GlobalSettings), new[] { userDisplay.Name, from, message, inviteUri.ToString(), fromEmail }); @@ -678,7 +678,7 @@ namespace Umbraco.Web.Editors if (passwordChangeResult.Success) { var result = new ModelWithNotifications(passwordChangeResult.Result.ResetPassword); - result.AddSuccessNotification(Services.TextService.Localize("general/success"), Services.TextService.Localize("user/passwordChangedGeneric")); + result.AddSuccessNotification(Services.TextService.Localize("general", "success"), Services.TextService.Localize("user", "passwordChangedGeneric")); return result; } @@ -716,11 +716,11 @@ namespace Umbraco.Web.Editors if (users.Length > 1) { return Request.CreateNotificationSuccessResponse( - Services.TextService.Localize("speechBubbles/disableUsersSuccess", new[] {userIds.Length.ToString()})); + Services.TextService.Localize("speechBubbles", "disableUsersSuccess", new[] {userIds.Length.ToString()})); } return Request.CreateNotificationSuccessResponse( - Services.TextService.Localize("speechBubbles/disableUserSuccess", new[] { users[0].Name })); + Services.TextService.Localize("speechBubbles", "disableUserSuccess", new[] { users[0].Name })); } /// @@ -740,11 +740,11 @@ namespace Umbraco.Web.Editors if (users.Length > 1) { return Request.CreateNotificationSuccessResponse( - Services.TextService.Localize("speechBubbles/enableUsersSuccess", new[] { userIds.Length.ToString() })); + Services.TextService.Localize("speechBubbles", "enableUsersSuccess", new[] { userIds.Length.ToString() })); } return Request.CreateNotificationSuccessResponse( - Services.TextService.Localize("speechBubbles/enableUserSuccess", new[] { users[0].Name })); + Services.TextService.Localize("speechBubbles", "enableUserSuccess", new[] { users[0].Name })); } /// @@ -767,7 +767,7 @@ namespace Umbraco.Web.Editors } var user = await UserManager.FindByIdAsync(userIds[0]); return Request.CreateNotificationSuccessResponse( - Services.TextService.Localize("speechBubbles/unlockUserSuccess", new[] { user.Name })); + Services.TextService.Localize("speechBubbles", "unlockUserSuccess", new[] { user.Name })); } foreach (var u in userIds) @@ -781,7 +781,7 @@ namespace Umbraco.Web.Editors } return Request.CreateNotificationSuccessResponse( - Services.TextService.Localize("speechBubbles/unlockUsersSuccess", new[] { userIds.Length.ToString() })); + Services.TextService.Localize("speechBubbles", "unlockUsersSuccess", new[] { userIds.Length.ToString() })); } [AdminUsersAuthorize("userIds")] @@ -799,7 +799,7 @@ namespace Umbraco.Web.Editors } Services.UserService.Save(users); return Request.CreateNotificationSuccessResponse( - Services.TextService.Localize("speechBubbles/setUserGroupOnUsersSuccess")); + Services.TextService.Localize("speechBubbles", "setUserGroupOnUsersSuccess")); } /// @@ -830,7 +830,7 @@ namespace Umbraco.Web.Editors Services.UserService.Delete(user, true); return Request.CreateNotificationSuccessResponse( - Services.TextService.Localize("speechBubbles/deleteUserSuccess", new[] { userName })); + Services.TextService.Localize("speechBubbles", "deleteUserSuccess", new[] { userName })); } public class PagedUserResult : PagedResult diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/AbstractConfigCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/AbstractConfigCheck.cs index 01863dac0c..f9e4c85f31 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Config/AbstractConfigCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Config/AbstractConfigCheck.cs @@ -74,7 +74,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config { get { - return TextService.Localize("healthcheck/checkSuccessMessage", + return TextService.Localize("healthcheck", "checkSuccessMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, XPath, AbsoluteFilePath }); } } @@ -87,9 +87,9 @@ namespace Umbraco.Web.HealthCheck.Checks.Config get { return ValueComparisonType == ValueComparisonType.ShouldEqual - ? TextService.Localize("healthcheck/checkErrorMessageDifferentExpectedValue", + ? TextService.Localize("healthcheck", "checkErrorMessageDifferentExpectedValue", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, XPath, AbsoluteFilePath }) - : TextService.Localize("healthcheck/checkErrorMessageUnexpectedValue", + : TextService.Localize("healthcheck", "checkErrorMessageUnexpectedValue", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, XPath, AbsoluteFilePath }); } } @@ -105,7 +105,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config var rectifiedValue = recommendedValue != null ? recommendedValue.Value : ProvidedValue; - return TextService.Localize("healthcheck/rectifySuccessMessage", + return TextService.Localize("healthcheck", "rectifySuccessMessage", new[] { CurrentValue, @@ -156,7 +156,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config // Declare the action for rectifying the config value var rectifyAction = new HealthCheckAction("rectify", Id) { - Name = TextService.Localize("healthcheck/rectifyButton"), + Name = TextService.Localize("healthcheck", "rectifyButton"), ValueRequired = CanRectifyWithValue, }; @@ -178,7 +178,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config public virtual HealthCheckStatus Rectify() { if (ValueComparisonType == ValueComparisonType.ShouldNotEqual) - throw new InvalidOperationException(TextService.Localize("healthcheck/cannotRectifyShouldNotEqual")); + throw new InvalidOperationException(TextService.Localize("healthcheck", "cannotRectifyShouldNotEqual")); var recommendedValue = Values.First(v => v.IsRecommended).Value; return UpdateConfigurationValue(recommendedValue); @@ -192,10 +192,10 @@ namespace Umbraco.Web.HealthCheck.Checks.Config public virtual HealthCheckStatus Rectify(string value) { if (ValueComparisonType == ValueComparisonType.ShouldEqual) - throw new InvalidOperationException(TextService.Localize("healthcheck/cannotRectifyShouldEqualWithValue")); + throw new InvalidOperationException(TextService.Localize("healthcheck", "cannotRectifyShouldEqualWithValue")); if (string.IsNullOrWhiteSpace(value)) - throw new InvalidOperationException(TextService.Localize("healthcheck/valueToRectifyNotProvided")); + throw new InvalidOperationException(TextService.Localize("healthcheck", "valueToRectifyNotProvided")); // Need to track provided value in order to correctly put together the rectify message ProvidedValue = value; diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/CompilationDebugCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/CompilationDebugCheck.cs index 951609f91f..959a0cdbae 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Config/CompilationDebugCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Config/CompilationDebugCheck.cs @@ -25,10 +25,10 @@ namespace Umbraco.Web.HealthCheck.Checks.Config new AcceptableConfiguration { IsRecommended = true, Value = bool.FalseString.ToLower() } }; - public override string CheckSuccessMessage => TextService.Localize("healthcheck/compilationDebugCheckSuccessMessage"); + public override string CheckSuccessMessage => TextService.Localize("healthcheck", "compilationDebugCheckSuccessMessage"); - public override string CheckErrorMessage => TextService.Localize("healthcheck/compilationDebugCheckErrorMessage"); + public override string CheckErrorMessage => TextService.Localize("healthcheck", "compilationDebugCheckErrorMessage"); - public override string RectifySuccessMessage => TextService.Localize("healthcheck/compilationDebugCheckRectifySuccessMessage"); + public override string RectifySuccessMessage => TextService.Localize("healthcheck", "compilationDebugCheckRectifySuccessMessage"); } } diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/ConfigurationService.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/ConfigurationService.cs index 95b458b142..8801ab0030 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Config/ConfigurationService.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Config/ConfigurationService.cs @@ -36,7 +36,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config return new ConfigurationServiceResult { Success = false, - Result = _textService.Localize("healthcheck/configurationServiceFileNotFound", new[] { _configFilePath }) + Result = _textService.Localize("healthcheck", "configurationServiceFileNotFound", new[] { _configFilePath }) }; var xmlDocument = new XmlDocument(); @@ -47,7 +47,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config return new ConfigurationServiceResult { Success = false, - Result = _textService.Localize("healthcheck/configurationServiceNodeNotFound", new[] { _xPath, _configFilePath }) + Result = _textService.Localize("healthcheck", "configurationServiceNodeNotFound", new[] { _xPath, _configFilePath }) }; return new ConfigurationServiceResult @@ -62,7 +62,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config return new ConfigurationServiceResult { Success = false, - Result = _textService.Localize("healthcheck/configurationServiceError", new[] { ex.Message }) + Result = _textService.Localize("healthcheck", "configurationServiceError", new[] { ex.Message }) }; } } @@ -80,7 +80,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config return new ConfigurationServiceResult { Success = false, - Result = _textService.Localize("healthcheck/configurationServiceFileNotFound", new[] { _configFilePath }) + Result = _textService.Localize("healthcheck", "configurationServiceFileNotFound", new[] { _configFilePath }) }; var xmlDocument = new XmlDocument { PreserveWhitespace = true }; @@ -91,7 +91,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config return new ConfigurationServiceResult { Success = false, - Result = _textService.Localize("healthcheck/configurationServiceNodeNotFound", new[] { _xPath, _configFilePath }) + Result = _textService.Localize("healthcheck", "configurationServiceNodeNotFound", new[] { _xPath, _configFilePath }) }; if (node.NodeType == XmlNodeType.Element) @@ -108,7 +108,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config return new ConfigurationServiceResult { Success = false, - Result = _textService.Localize("healthcheck/configurationServiceError", new[] { ex.Message }) + Result = _textService.Localize("healthcheck", "configurationServiceError", new[] { ex.Message }) }; } } diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/CustomErrorsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/CustomErrorsCheck.cs index 63986b6c62..9b52e9b8e9 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Config/CustomErrorsCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Config/CustomErrorsCheck.cs @@ -30,7 +30,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config { get { - return TextService.Localize("healthcheck/customErrorsCheckSuccessMessage", new[] { CurrentValue }); + return TextService.Localize("healthcheck", "customErrorsCheckSuccessMessage", new[] { CurrentValue }); } } @@ -38,7 +38,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config { get { - return TextService.Localize("healthcheck/customErrorsCheckErrorMessage", + return TextService.Localize("healthcheck", "customErrorsCheckErrorMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value }); } } @@ -47,7 +47,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config { get { - return TextService.Localize("healthcheck/customErrorsCheckRectifySuccessMessage", + return TextService.Localize("healthcheck", "customErrorsCheckRectifySuccessMessage", new[] { Values.First(v => v.IsRecommended).Value }); } } diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/MacroErrorsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/MacroErrorsCheck.cs index 09c041998e..1e0283321f 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Config/MacroErrorsCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Config/MacroErrorsCheck.cs @@ -45,7 +45,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config { get { - return TextService.Localize("healthcheck/macroErrorModeCheckSuccessMessage", + return TextService.Localize("healthcheck", "macroErrorModeCheckSuccessMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value }); } } @@ -54,7 +54,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config { get { - return TextService.Localize("healthcheck/macroErrorModeCheckErrorMessage", + return TextService.Localize("healthcheck", "macroErrorModeCheckErrorMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value }); } } @@ -63,7 +63,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config { get { - return TextService.Localize("healthcheck/macroErrorModeCheckRectifySuccessMessage", + return TextService.Localize("healthcheck", "macroErrorModeCheckRectifySuccessMessage", new[] { Values.First(v => v.IsRecommended).Value }); } } diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/NotificationEmailCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/NotificationEmailCheck.cs index 8c52a5686b..af9494ced7 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Config/NotificationEmailCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Config/NotificationEmailCheck.cs @@ -25,8 +25,8 @@ namespace Umbraco.Web.HealthCheck.Checks.Config new AcceptableConfiguration { IsRecommended = false, Value = DefaultFromEmail } }; - public override string CheckSuccessMessage => TextService.Localize("healthcheck/notificationEmailsCheckSuccessMessage", new [] { CurrentValue } ); + public override string CheckSuccessMessage => TextService.Localize("healthcheck", "notificationEmailsCheckSuccessMessage", new [] { CurrentValue } ); - public override string CheckErrorMessage => TextService.Localize("healthcheck/notificationEmailsCheckErrorMessage", new[] { DefaultFromEmail }); + public override string CheckErrorMessage => TextService.Localize("healthcheck", "notificationEmailsCheckErrorMessage", new[] { DefaultFromEmail }); } } diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/TraceCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/TraceCheck.cs index b8e4e51eb9..47c88bf250 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Config/TraceCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Config/TraceCheck.cs @@ -24,10 +24,10 @@ namespace Umbraco.Web.HealthCheck.Checks.Config new AcceptableConfiguration { IsRecommended = true, Value = bool.FalseString.ToLower() } }; - public override string CheckSuccessMessage => TextService.Localize("healthcheck/traceModeCheckSuccessMessage"); + public override string CheckSuccessMessage => TextService.Localize("healthcheck", "traceModeCheckSuccessMessage"); - public override string CheckErrorMessage => TextService.Localize("healthcheck/traceModeCheckErrorMessage"); + public override string CheckErrorMessage => TextService.Localize("healthcheck", "traceModeCheckErrorMessage"); - public override string RectifySuccessMessage => TextService.Localize("healthcheck/traceModeCheckRectifySuccessMessage"); + public override string RectifySuccessMessage => TextService.Localize("healthcheck", "traceModeCheckRectifySuccessMessage"); } } diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/TrySkipIisCustomErrorsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/TrySkipIisCustomErrorsCheck.cs index 4a467d7120..2c4e59091e 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Config/TrySkipIisCustomErrorsCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Config/TrySkipIisCustomErrorsCheck.cs @@ -39,7 +39,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config { get { - return TextService.Localize("healthcheck/trySkipIisCustomErrorsCheckSuccessMessage", + return TextService.Localize("healthcheck", "trySkipIisCustomErrorsCheckSuccessMessage", new[] { Values.First(v => v.IsRecommended).Value, _serverVersion.ToString() }); } } @@ -48,7 +48,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config { get { - return TextService.Localize("healthcheck/trySkipIisCustomErrorsCheckErrorMessage", + return TextService.Localize("healthcheck", "trySkipIisCustomErrorsCheckErrorMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, _serverVersion.ToString() }); } } @@ -57,7 +57,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Config { get { - return TextService.Localize("healthcheck/trySkipIisCustomErrorsCheckRectifySuccessMessage", + return TextService.Localize("healthcheck", "trySkipIisCustomErrorsCheckRectifySuccessMessage", new[] { Values.First(v => v.IsRecommended).Value, _serverVersion.ToString() }); } } diff --git a/src/Umbraco.Web/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs index ee6f254235..218cb2cc9c 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs @@ -135,22 +135,20 @@ namespace Umbraco.Web.HealthCheck.Checks.Permissions { // Return error if any required paths fail the check, or warning if any optional ones do var resultType = StatusResultType.Success; - var messageKey = string.Format("healthcheck/{0}PermissionsCheckMessage", - checkingFor == PermissionCheckFor.Folder ? "folder" : "file"); - var message = _textService.Localize(messageKey); + var messageArea = "healthcheck"; + var messageAlias = string.Concat(checkingFor == PermissionCheckFor.Folder ? "folder" : "file", "PermissionsCheckMessage"); + var message = _textService.Localize(messageArea, messageAlias); if (requiredPathCheckResult == false) { resultType = StatusResultType.Error; - messageKey = string.Format("healthcheck/required{0}PermissionFailed", - checkingFor == PermissionCheckFor.Folder ? "Folder" : "File"); - message = GetMessageForPathCheckFailure(messageKey, requiredFailedPaths); + messageAlias = string.Concat("required", checkingFor == PermissionCheckFor.Folder ? "Folder" : "File", "PermissionFailed"); + message = GetMessageForPathCheckFailure(messageArea, messageAlias, requiredFailedPaths); } else if (optionalPathCheckResult == false) { resultType = StatusResultType.Warning; - messageKey = string.Format("healthcheck/optional{0}PermissionFailed", - checkingFor == PermissionCheckFor.Folder ? "Folder" : "File"); - message = GetMessageForPathCheckFailure(messageKey, optionalFailedPaths); + messageAlias = string.Concat("optional", checkingFor == PermissionCheckFor.Folder ? "Folder" : "File", "PermissionFailed"); + message = GetMessageForPathCheckFailure(messageArea, messageAlias, optionalFailedPaths); } var actions = new List(); @@ -162,12 +160,12 @@ namespace Umbraco.Web.HealthCheck.Checks.Permissions }; } - private string GetMessageForPathCheckFailure(string messageKey, IEnumerable failedPaths) + private string GetMessageForPathCheckFailure(string messageArea,string messageAlias, IEnumerable failedPaths) { var rootFolder = IOHelper.MapPath("/"); var failedFolders = failedPaths .Select(x => ParseFolderFromFullPath(rootFolder, x)); - return _textService.Localize(messageKey, + return _textService.Localize(messageArea, messageAlias, new[] { string.Join(", ", failedFolders) }); } diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs index 5e66bc47b1..6bb32d9d74 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs @@ -87,12 +87,12 @@ namespace Umbraco.Web.HealthCheck.Checks.Security } message = success - ? TextService.Localize($"healthcheck/{_localizedTextPrefix}CheckHeaderFound") - : TextService.Localize($"healthcheck/{_localizedTextPrefix}CheckHeaderNotFound"); + ? TextService.Localize($"healthcheck", "{_localizedTextPrefix}CheckHeaderFound") + : TextService.Localize($"healthcheck", "{_localizedTextPrefix}CheckHeaderNotFound"); } catch (Exception ex) { - message = TextService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url.ToString(), ex.Message }); + message = TextService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.ToString(), ex.Message }); } var actions = new List(); @@ -100,8 +100,8 @@ namespace Umbraco.Web.HealthCheck.Checks.Security { actions.Add(new HealthCheckAction(SetHeaderInConfigAction, Id) { - Name = TextService.Localize("healthcheck/setHeaderInConfig"), - Description = TextService.Localize($"healthcheck/{_localizedTextPrefix}SetHeaderInConfigDescription") + Name = TextService.Localize("healthcheck", "setHeaderInConfig"), + Description = TextService.Localize($"healthcheck", "{_localizedTextPrefix}SetHeaderInConfigDescription") }); } @@ -149,14 +149,14 @@ namespace Umbraco.Web.HealthCheck.Checks.Security if (success) { return - new HealthCheckStatus(TextService.Localize(string.Format("healthcheck/{0}SetHeaderInConfigSuccess", _localizedTextPrefix))) + new HealthCheckStatus(TextService.Localize("healthcheck", _localizedTextPrefix + "SetHeaderInConfigSuccess")) { ResultType = StatusResultType.Success }; } return - new HealthCheckStatus(TextService.Localize("healthcheck/setHeaderInConfigError", new [] { errorMessage })) + new HealthCheckStatus(TextService.Localize("healthcheck", "setHeaderInConfigError", new [] { errorMessage })) { ResultType = StatusResultType.Error }; diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs index fd76b9d486..28904cc6bd 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs @@ -64,12 +64,12 @@ namespace Umbraco.Web.HealthCheck.Checks.Security .ToArray(); success = headersFound.Any() == false; message = success - ? _textService.Localize("healthcheck/excessiveHeadersNotFound") - : _textService.Localize("healthcheck/excessiveHeadersFound", new [] { string.Join(", ", headersFound) }); + ? _textService.Localize("healthcheck", "excessiveHeadersNotFound") + : _textService.Localize("healthcheck", "excessiveHeadersFound", new [] { string.Join(", ", headersFound) }); } catch (Exception ex) { - message = _textService.Localize("healthcheck/httpsCheckInvalidUrl", new[] { url.ToString(), ex.Message }); + message = _textService.Localize("healthcheck", "httpsCheckInvalidUrl", new[] { url.ToString(), ex.Message }); } var actions = new List(); diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs index 98f8a83c1d..51b253fe94 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs @@ -84,23 +84,23 @@ namespace Umbraco.Web.HealthCheck.Checks.Security if (daysToExpiry <= 0) { result = StatusResultType.Error; - message = _textService.Localize("healthcheck/httpsCheckExpiredCertificate"); + message = _textService.Localize("healthcheck", "httpsCheckExpiredCertificate"); } else if (daysToExpiry < NumberOfDaysForExpiryWarning) { result = StatusResultType.Warning; - message = _textService.Localize("healthcheck/httpsCheckExpiringCertificate", new[] { daysToExpiry.ToString() }); + message = _textService.Localize("healthcheck", "httpsCheckExpiringCertificate", new[] { daysToExpiry.ToString() }); } else { result = StatusResultType.Success; - message = _textService.Localize("healthcheck/httpsCheckValidCertificate"); + message = _textService.Localize("healthcheck", "httpsCheckValidCertificate"); } } else { result = StatusResultType.Error; - message = _textService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url, response.StatusDescription }); + message = _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url, response.StatusDescription }); } } catch (Exception ex) @@ -109,12 +109,12 @@ namespace Umbraco.Web.HealthCheck.Checks.Security if (exception != null) { message = exception.Status == WebExceptionStatus.TrustFailure - ? _textService.Localize("healthcheck/httpsCheckInvalidCertificate", new [] { exception.Message }) - : _textService.Localize("healthcheck/healthCheckInvalidUrl", new [] { url, exception.Message }); + ? _textService.Localize("healthcheck", "httpsCheckInvalidCertificate", new [] { exception.Message }) + : _textService.Localize("healthcheck", "healthCheckInvalidUrl", new [] { url, exception.Message }); } else { - message = _textService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url, ex.Message }); + message = _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url, ex.Message }); } result = StatusResultType.Error; @@ -138,7 +138,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Security var actions = new List(); return - new HealthCheckStatus(_textService.Localize("healthcheck/httpsCheckIsCurrentSchemeHttps", new[] { success ? string.Empty : "not" })) + new HealthCheckStatus(_textService.Localize("healthcheck", "httpsCheckIsCurrentSchemeHttps", new[] { success ? string.Empty : "not" })) { ResultType = success ? StatusResultType.Success : StatusResultType.Error, Actions = actions @@ -155,7 +155,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Security StatusResultType resultType; if (uri.Scheme != "https") { - resultMessage = _textService.Localize("healthcheck/httpsCheckConfigurationRectifyNotPossible"); + resultMessage = _textService.Localize("healthcheck", "httpsCheckConfigurationRectifyNotPossible"); resultType = StatusResultType.Info; } else @@ -163,11 +163,11 @@ namespace Umbraco.Web.HealthCheck.Checks.Security if (httpsSettingEnabled == false) actions.Add(new HealthCheckAction(FixHttpsSettingAction, Id) { - Name = _textService.Localize("healthcheck/httpsCheckEnableHttpsButton"), - Description = _textService.Localize("healthcheck/httpsCheckEnableHttpsDescription") + Name = _textService.Localize("healthcheck", "httpsCheckEnableHttpsButton"), + Description = _textService.Localize("healthcheck", "httpsCheckEnableHttpsDescription") }); - resultMessage = _textService.Localize("healthcheck/httpsCheckConfigurationCheckResult", + resultMessage = _textService.Localize("healthcheck", "httpsCheckConfigurationCheckResult", new[] {httpsSettingEnabled.ToString(), httpsSettingEnabled ? string.Empty : "not"}); resultType = httpsSettingEnabled ? StatusResultType.Success: StatusResultType.Error; } @@ -190,14 +190,14 @@ namespace Umbraco.Web.HealthCheck.Checks.Security if (updateConfigFile.Success) { return - new HealthCheckStatus(_textService.Localize("healthcheck/httpsCheckEnableHttpsSuccess")) + new HealthCheckStatus(_textService.Localize("healthcheck", "httpsCheckEnableHttpsSuccess")) { ResultType = StatusResultType.Success }; } return - new HealthCheckStatus(_textService.Localize("healthcheck/httpsCheckEnableHttpsError", new [] { updateConfigFile.Result })) + new HealthCheckStatus(_textService.Localize("healthcheck", "httpsCheckEnableHttpsError", new [] { updateConfigFile.Result })) { ResultType = StatusResultType.Error }; diff --git a/src/Umbraco.Web/HealthCheck/Checks/Services/SmtpCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Services/SmtpCheck.cs index d6f7cef497..2627e24fb8 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Services/SmtpCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Services/SmtpCheck.cs @@ -56,7 +56,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Services var settings = (MailSettingsSectionGroup)config.GetSectionGroup("system.net/mailSettings"); if (settings == null) { - message = _textService.Localize("healthcheck/smtpMailSettingsNotFound"); + message = _textService.Localize("healthcheck", "smtpMailSettingsNotFound"); } else { @@ -64,14 +64,14 @@ namespace Umbraco.Web.HealthCheck.Checks.Services var port = settings.Smtp.Network.Port == 0 ? DefaultSmtpPort : settings.Smtp.Network.Port; if (string.IsNullOrEmpty(host)) { - message = _textService.Localize("healthcheck/smtpMailSettingsHostNotConfigured"); + message = _textService.Localize("healthcheck", "smtpMailSettingsHostNotConfigured"); } else { success = CanMakeSmtpConnection(host, port); message = success - ? _textService.Localize("healthcheck/smtpMailSettingsConnectionSuccess") - : _textService.Localize("healthcheck/smtpMailSettingsConnectionFail", new [] { host, port.ToString() }); + ? _textService.Localize("healthcheck", "smtpMailSettingsConnectionSuccess") + : _textService.Localize("healthcheck", "smtpMailSettingsConnectionFail", new [] { host, port.ToString() }); } } diff --git a/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs b/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs index 873b356214..54e4f2c52e 100644 --- a/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs +++ b/src/Umbraco.Web/HealthCheck/NotificationMethods/EmailNotificationMethod.cs @@ -48,7 +48,7 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods return; } - var message = _textService.Localize("healthcheck/scheduledHealthCheckEmailBody", new[] + var message = _textService.Localize("healthcheck", "scheduledHealthCheckEmailBody", new[] { DateTime.Now.ToShortDateString(), DateTime.Now.ToShortTimeString(), @@ -59,7 +59,7 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods // you can identify the site that these results are for. var host = _runtimeState.ApplicationUrl; - var subject = _textService.Localize("healthcheck/scheduledHealthCheckEmailSubject", new[] { host.ToString() }); + var subject = _textService.Localize("healthcheck", "scheduledHealthCheckEmailSubject", new[] { host.ToString() }); var mailSender = new EmailSender(); using (var mailMessage = CreateMailMessage(subject, message)) diff --git a/src/Umbraco.Web/Macros/MacroRenderer.cs b/src/Umbraco.Web/Macros/MacroRenderer.cs index b4fd8c0d86..4538dc03c6 100755 --- a/src/Umbraco.Web/Macros/MacroRenderer.cs +++ b/src/Umbraco.Web/Macros/MacroRenderer.cs @@ -340,14 +340,14 @@ namespace Umbraco.Web.Macros $"Executing PartialView: MacroSource=\"{model.MacroSource}\".", "Executed PartialView.", () => ExecutePartialView(model, content), - () => textService.Localize("errors/macroErrorLoadingPartialView", new[] { model.MacroSource })); + () => textService.Localize("errors", "macroErrorLoadingPartialView", new[] { model.MacroSource })); default: return ExecuteMacroWithErrorWrapper(model, $"Execute macro with unsupported type \"{model.MacroType}\".", "Executed.", () => { throw new Exception("Unsupported macro type."); }, - () => textService.Localize("errors/macroErrorUnsupportedType")); + () => textService.Localize("errors", "macroErrorUnsupportedType")); } } diff --git a/src/Umbraco.Web/Models/Mapping/CommonMapper.cs b/src/Umbraco.Web/Models/Mapping/CommonMapper.cs index f7db17ff74..d04579d5ee 100644 --- a/src/Umbraco.Web/Models/Mapping/CommonMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/CommonMapper.cs @@ -80,7 +80,7 @@ namespace Umbraco.Web.Models.Mapping // localize content app names foreach (var app in apps) { - var localizedAppName = _localizedTextService.Localize($"apps/{app.Alias}"); + var localizedAppName = _localizedTextService.Localize($"apps", "{app.Alias}"); if (localizedAppName.Equals($"[{app.Alias}]", StringComparison.OrdinalIgnoreCase) == false) { app.Name = localizedAppName; diff --git a/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs index 0b6be53045..7b240d2981 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs @@ -153,7 +153,7 @@ namespace Umbraco.Web.Models.Mapping if(!isCultureVariant && !isSegmentVariant) { - return _localizedTextService.Localize("general/default"); + return _localizedTextService.Localize("general", "default"); } var parts = new List(); diff --git a/src/Umbraco.Web/Models/Mapping/MemberTabsAndPropertiesMapper.cs b/src/Umbraco.Web/Models/Mapping/MemberTabsAndPropertiesMapper.cs index 8744b068a7..61c6684093 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberTabsAndPropertiesMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberTabsAndPropertiesMapper.cs @@ -61,7 +61,7 @@ namespace Umbraco.Web.Models.Mapping if (isLockedOutProperty?.Value != null && isLockedOutProperty.Value.ToString() != "1") { isLockedOutProperty.View = "readonlyvalue"; - isLockedOutProperty.Value = _localizedTextService.Localize("general/no"); + isLockedOutProperty.Value = _localizedTextService.Localize("general", "no"); } } else @@ -75,7 +75,7 @@ namespace Umbraco.Web.Models.Mapping if (isLockedOutProperty?.Value != null && isLockedOutProperty.Value.ToString() != "1") { isLockedOutProperty.View = "readonlyvalue"; - isLockedOutProperty.Value = _localizedTextService.Localize("general/no"); + isLockedOutProperty.Value = _localizedTextService.Localize("general", "no"); } } @@ -115,14 +115,14 @@ namespace Umbraco.Web.Models.Mapping new ContentPropertyDisplay { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}id", - Label = _localizedTextService.Localize("general/id"), + Label = _localizedTextService.Localize("general","id"), Value = new List {member.Id.ToString(), member.Key.ToString()}, View = "idwithguid" }, new ContentPropertyDisplay { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}doctype", - Label = _localizedTextService.Localize("content/membertype"), + Label = _localizedTextService.Localize("content","membertype"), Value = _localizedTextService.UmbracoDictionaryTranslate(member.ContentType.Name), View = Current.PropertyEditors[Constants.PropertyEditors.Aliases.Label].GetValueEditor().View }, @@ -130,7 +130,7 @@ namespace Umbraco.Web.Models.Mapping new ContentPropertyDisplay { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email", - Label = _localizedTextService.Localize("general/email"), + Label = _localizedTextService.Localize("general","email"), Value = member.Email, View = "email", Validation = {Mandatory = true} @@ -138,7 +138,7 @@ namespace Umbraco.Web.Models.Mapping new ContentPropertyDisplay { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password", - Label = _localizedTextService.Localize("password"), + Label = _localizedTextService.Localize(null,"password"), // NOTE: The value here is a json value - but the only property we care about is the generatedPassword one if it exists, the newPassword exists // only when creating a new member and we want to have a generated password pre-filled. Value = new Dictionary @@ -159,7 +159,7 @@ namespace Umbraco.Web.Models.Mapping new ContentPropertyDisplay { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}membergroup", - Label = _localizedTextService.Localize("content/membergroup"), + Label = _localizedTextService.Localize("content","membergroup"), Value = GetMemberGroupValue(member.Username), View = "membergroups", Config = new Dictionary {{"IsRequired", true}} @@ -223,7 +223,7 @@ namespace Umbraco.Web.Models.Mapping var prop = new ContentPropertyDisplay { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login", - Label = localizedText.Localize("login"), + Label = localizedText.Localize(null,"login"), Value = member.Username }; diff --git a/src/Umbraco.Web/Models/Mapping/SectionMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/SectionMapDefinition.cs index e05e6e5c84..de6a92549a 100644 --- a/src/Umbraco.Web/Models/Mapping/SectionMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/SectionMapDefinition.cs @@ -35,7 +35,7 @@ namespace Umbraco.Web.Models.Mapping private void Map(ISection source, Section target, MapperContext context) { target.Alias = source.Alias; - target.Name = _textService.Localize("sections/" + source.Alias); + target.Name = _textService.Localize("sections", source.Alias); } // Umbraco.Code.MapAll diff --git a/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesMapper.cs b/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesMapper.cs index b8d76572fb..85af1dbe9e 100644 --- a/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesMapper.cs @@ -58,7 +58,7 @@ namespace Umbraco.Web.Models.Mapping tabs.Add(new Tab { Id = 0, - Label = LocalizedTextService.Localize("general/properties"), + Label = LocalizedTextService.Localize("general", "properties"), Alias = "Generic properties", Properties = genericproperties }); diff --git a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs index aa158799cb..924ffda554 100644 --- a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs @@ -273,8 +273,8 @@ namespace Umbraco.Web.Models.Mapping { target.AvailableCultures = _textService.GetSupportedCultures().ToDictionary(x => x.Name, x => x.DisplayName); target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache); - target.CalculatedStartContentIds = GetStartNodes(source.CalculateContentStartNodeIds(_entityService), UmbracoObjectTypes.Document, "content/contentRoot", context); - target.CalculatedStartMediaIds = GetStartNodes(source.CalculateMediaStartNodeIds(_entityService), UmbracoObjectTypes.Media, "media/mediaRoot", context); + target.CalculatedStartContentIds = GetStartNodes(source.CalculateContentStartNodeIds(_entityService), UmbracoObjectTypes.Document, "content","contentRoot", context); + target.CalculatedStartMediaIds = GetStartNodes(source.CalculateMediaStartNodeIds(_entityService), UmbracoObjectTypes.Media, "media","mediaRoot", context); target.CreateDate = source.CreateDate; target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); target.Email = source.Email; @@ -289,8 +289,8 @@ namespace Umbraco.Web.Models.Mapping target.Navigation = CreateUserEditorNavigation(); target.ParentId = -1; target.Path = "-1," + source.Id; - target.StartContentIds = GetStartNodes(source.StartContentIds.ToArray(), UmbracoObjectTypes.Document, "content/contentRoot", context); - target.StartMediaIds = GetStartNodes(source.StartMediaIds.ToArray(), UmbracoObjectTypes.Media, "media/mediaRoot", context); + target.StartContentIds = GetStartNodes(source.StartContentIds.ToArray(), UmbracoObjectTypes.Document, "content","contentRoot", context); + target.StartMediaIds = GetStartNodes(source.StartMediaIds.ToArray(), UmbracoObjectTypes.Media, "media","mediaRoot", context); target.UpdateDate = source.UpdateDate; target.UserGroups = context.MapEnumerable(source.Groups); target.Username = source.Username; @@ -347,12 +347,12 @@ namespace Umbraco.Web.Models.Mapping if (sourceStartMediaId > 0) target.MediaStartNode = context.Map(_entityService.Get(sourceStartMediaId.Value, UmbracoObjectTypes.Media)); else if (sourceStartMediaId == -1) - target.MediaStartNode = CreateRootNode(_textService.Localize("media/mediaRoot")); + target.MediaStartNode = CreateRootNode(_textService.Localize("media", "mediaRoot")); if (sourceStartContentId > 0) target.ContentStartNode = context.Map(_entityService.Get(sourceStartContentId.Value, UmbracoObjectTypes.Document)); else if (sourceStartContentId == -1) - target.ContentStartNode = CreateRootNode(_textService.Localize("content/contentRoot")); + target.ContentStartNode = CreateRootNode(_textService.Localize("content", "contentRoot")); if (target.Icon.IsNullOrWhiteSpace()) target.Icon = Constants.Icons.UserGroup; @@ -364,10 +364,10 @@ namespace Umbraco.Web.Models.Mapping => new Permission { Category = action.Category.IsNullOrWhiteSpace() - ? _textService.Localize($"actionCategories/{Constants.Conventions.PermissionCategories.OtherCategory}") - : _textService.Localize($"actionCategories/{action.Category}"), - Name = _textService.Localize($"actions/{action.Alias}"), - Description = _textService.Localize($"actionDescriptions/{action.Alias}"), + ? _textService.Localize("actionCategories",Constants.Conventions.PermissionCategories.OtherCategory) + : _textService.Localize("actionCategories", action.Category), + Name = _textService.Localize("actions", action.Alias), + Description = _textService.Localize("actionDescriptions", action.Alias), Icon = action.Icon, Checked = source.Permissions != null && source.Permissions.Contains(action.Letter.ToString(CultureInfo.InvariantCulture)), PermissionCode = action.Letter.ToString(CultureInfo.InvariantCulture) @@ -383,14 +383,14 @@ namespace Umbraco.Web.Models.Mapping private static string MapContentTypeIcon(IEntitySlim entity) => entity is IContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; - private IEnumerable GetStartNodes(int[] startNodeIds, UmbracoObjectTypes objectType, string localizedKey, MapperContext context) + private IEnumerable GetStartNodes(int[] startNodeIds, UmbracoObjectTypes objectType, string localizedArea,string localizedAlias, MapperContext context) { if (startNodeIds.Length <= 0) return Enumerable.Empty(); var startNodes = new List(); if (startNodeIds.Contains(-1)) - startNodes.Add(CreateRootNode(_textService.Localize(localizedKey))); + startNodes.Add(CreateRootNode(_textService.Localize(localizedArea, localizedAlias))); var mediaItems = _entityService.GetAll(objectType, startNodeIds); startNodes.AddRange(context.MapEnumerable(mediaItems)); @@ -406,7 +406,7 @@ namespace Umbraco.Web.Models.Mapping Active = true, Alias = "details", Icon = "icon-umb-users", - Name = _textService.Localize("general/user"), + Name = _textService.Localize("general","user"), View = "views/users/views/user/details.html" } }; diff --git a/src/Umbraco.Web/Models/Trees/MenuItem.cs b/src/Umbraco.Web/Models/Trees/MenuItem.cs index fb4dfb836c..9d4d146596 100644 --- a/src/Umbraco.Web/Models/Trees/MenuItem.cs +++ b/src/Umbraco.Web/Models/Trees/MenuItem.cs @@ -32,12 +32,9 @@ namespace Umbraco.Web.Models.Trees public MenuItem(string alias, ILocalizedTextService textService) : this() { - var values = textService.GetAllStoredValues(Thread.CurrentThread.CurrentUICulture); - values.TryGetValue($"visuallyHiddenTexts/{alias}_description", out var textDescription); - Alias = alias; - Name = textService.Localize($"actions/{Alias}"); - TextDescription = textDescription; + Name = textService.Localize("actions", Alias); + TextDescription = textService.Localize("visuallyHiddenTexts", alias + "_description", Thread.CurrentThread.CurrentUICulture); } /// diff --git a/src/Umbraco.Web/Models/Trees/MenuItemList.cs b/src/Umbraco.Web/Models/Trees/MenuItemList.cs index 4aaf0632ab..edcfd24584 100644 --- a/src/Umbraco.Web/Models/Trees/MenuItemList.cs +++ b/src/Umbraco.Web/Models/Trees/MenuItemList.cs @@ -97,14 +97,12 @@ namespace Umbraco.Web.Models.Trees var item = Current.Actions.GetAction(); if (item == null) return null; - var values = textService.GetAllStoredValues(Thread.CurrentThread.CurrentUICulture); - values.TryGetValue($"visuallyHiddenTexts/{item.Alias}Description", out var textDescription); - var menuItem = new MenuItem(item, textService.Localize($"actions/{item.Alias}")) + var menuItem = new MenuItem(item, textService.Localize("actions",item.Alias)) { SeparatorBefore = hasSeparator, OpensDialog = opensDialog, - TextDescription = textDescription, + TextDescription = textService.Localize("visuallyHiddenTexts", item.Alias+"Description", Thread.CurrentThread.CurrentUICulture), }; return menuItem; diff --git a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs index 8c4ebf49c3..705d4a706d 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs @@ -249,7 +249,7 @@ namespace Umbraco.Web.PropertyEditors || (blockEditorData != null && validationLimit.Min.HasValue && blockEditorData.Layout.Count() < validationLimit.Min)) { yield return new ValidationResult( - _textService.Localize("validation/entriesShort", new[] + _textService.Localize("validation", "entriesShort", new[] { validationLimit.Min.ToString(), (validationLimit.Min - blockEditorData.Layout.Count()).ToString() @@ -260,7 +260,7 @@ namespace Umbraco.Web.PropertyEditors if (blockEditorData != null && validationLimit.Max.HasValue && blockEditorData.Layout.Count() > validationLimit.Max) { yield return new ValidationResult( - _textService.Localize("validation/entriesExceed", new[] + _textService.Localize("validation", "entriesExceed", new[] { validationLimit.Max.ToString(), (blockEditorData.Layout.Count() - validationLimit.Max).ToString() diff --git a/src/Umbraco.Web/PropertyEditors/DropDownFlexibleConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/DropDownFlexibleConfigurationEditor.cs index dbdedebd0a..cf0aca7ba0 100644 --- a/src/Umbraco.Web/PropertyEditors/DropDownFlexibleConfigurationEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DropDownFlexibleConfigurationEditor.cs @@ -14,7 +14,7 @@ namespace Umbraco.Web.PropertyEditors var items = Fields.First(x => x.Key == "items"); // customize the items field - items.Name = textService.Localize("editdatatype/addPrevalue"); + items.Name = textService.Localize("editdatatype", "addPrevalue"); items.Validators.Add(new ValueListUniqueValueValidator()); } diff --git a/src/Umbraco.Web/PropertyEditors/UploadFileTypeValidator.cs b/src/Umbraco.Web/PropertyEditors/UploadFileTypeValidator.cs index 8d7bd29889..f691feba52 100644 --- a/src/Umbraco.Web/PropertyEditors/UploadFileTypeValidator.cs +++ b/src/Umbraco.Web/PropertyEditors/UploadFileTypeValidator.cs @@ -40,7 +40,7 @@ namespace Umbraco.Web.PropertyEditors { //we only store a single value for this editor so the 'member' or 'field' // we'll associate this error with will simply be called 'value' - yield return new ValidationResult(Current.Services.TextService.Localize("errors/dissallowedMediaType"), new[] { "value" }); + yield return new ValidationResult(Current.Services.TextService.Localize("errors", "dissallowedMediaType"), new[] { "value" }); } } } diff --git a/src/Umbraco.Web/PropertyEditors/ValueListConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/ValueListConfigurationEditor.cs index abfde3646a..b4f600dbf3 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueListConfigurationEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueListConfigurationEditor.cs @@ -19,7 +19,7 @@ namespace Umbraco.Web.PropertyEditors var items = Fields.First(x => x.Key == "items"); // customize the items field - items.Name = textService.Localize("editdatatype/addPrevalue"); + items.Name = textService.Localize("editdatatype", "addPrevalue"); items.Validators.Add(new ValueListUniqueValueValidator()); } diff --git a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs index afa0bc96cc..7d5fba7df5 100644 --- a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs @@ -35,7 +35,7 @@ namespace Umbraco.Web.Routing if (content.Published == false) { - yield return UrlInfo.Message(textService.Localize("content/itemNotPublished")); + yield return UrlInfo.Message(textService.Localize("content", "itemNotPublished")); yield break; } @@ -126,7 +126,7 @@ namespace Umbraco.Web.Routing // deal with exceptions case "#ex": - yield return UrlInfo.Message(textService.Localize("content/getUrlException"), culture); + yield return UrlInfo.Message(textService.Localize("content", "getUrlException"), culture); break; // got a URL, deal with collisions, add URL @@ -152,13 +152,13 @@ namespace Umbraco.Web.Routing while (parent != null && parent.Published && (!parent.ContentType.VariesByCulture() || parent.IsCulturePublished(culture))); if (parent == null) // oops, internal error - return UrlInfo.Message(textService.Localize("content/parentNotPublishedAnomaly"), culture); + return UrlInfo.Message(textService.Localize("content", "parentNotPublishedAnomaly"), culture); else if (!parent.Published) // totally not published - return UrlInfo.Message(textService.Localize("content/parentNotPublished", new[] {parent.Name}), culture); + return UrlInfo.Message(textService.Localize("content", "parentNotPublished", new[] {parent.Name}), culture); else // culture not published - return UrlInfo.Message(textService.Localize("content/parentCultureNotPublished", new[] {parent.Name}), culture); + return UrlInfo.Message(textService.Localize("content", "parentCultureNotPublished", new[] {parent.Name}), culture); } private static bool DetectCollision(IContent content, string url, string culture, UmbracoContext umbracoContext, IPublishedRouter publishedRouter, ILocalizedTextService textService, out UrlInfo urlInfo) @@ -174,7 +174,7 @@ namespace Umbraco.Web.Routing if (pcr.HasPublishedContent == false) { - urlInfo = UrlInfo.Message(textService.Localize("content/routeErrorCannotRoute"), culture); + urlInfo = UrlInfo.Message(textService.Localize("content", "routeErrorCannotRoute"), culture); return true; } @@ -193,7 +193,7 @@ namespace Umbraco.Web.Routing l.Reverse(); var s = "/" + string.Join("/", l) + " (id=" + pcr.PublishedContent.Id + ")"; - urlInfo = UrlInfo.Message(textService.Localize("content/routeError", new[] { s }), culture); + urlInfo = UrlInfo.Message(textService.Localize("content", "routeError", new[] { s }), culture); return true; } diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs index ad9e3ca172..82853bace3 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeController.cs @@ -70,7 +70,7 @@ namespace Umbraco.Web.Trees { //if there are no trees defined for this section but the section is defined then we can have a simple //full screen section without trees - var name = Services.TextService.Localize("sections/" + application); + var name = Services.TextService.Localize("sections", application); return TreeRootNode.CreateSingleTreeRoot(Constants.System.RootString, null, null, name, TreeNodeCollection.Empty, true); } @@ -103,7 +103,7 @@ namespace Umbraco.Web.Trees nodes.Add(node); } - var name = Services.TextService.Localize("sections/" + application); + var name = Services.TextService.Localize("sections", application); if (nodes.Count > 0) { @@ -138,7 +138,7 @@ namespace Umbraco.Web.Trees var name = groupName.IsNullOrWhiteSpace() ? "thirdPartyGroup" : groupName; var groupRootNode = TreeRootNode.CreateGroupNode(nodes, application); - groupRootNode.Name = Services.TextService.Localize("treeHeaders/" + name); + groupRootNode.Name = Services.TextService.Localize("treeHeaders", name); treeRootNodes.Add(groupRootNode); } diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index 95de72b7bf..66e9fbf50c 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -331,7 +331,7 @@ namespace Umbraco.Web.Trees RecycleBinId.ToInvariantString(), id, queryStrings, - Services.TextService.Localize("general/recycleBin"), + Services.TextService.Localize("general", "recycleBin"), "icon-trash", RecycleBinSmells, queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/recyclebin")); diff --git a/src/Umbraco.Web/Trees/DataTypeTreeController.cs b/src/Umbraco.Web/Trees/DataTypeTreeController.cs index 6a7fb7f5ad..c0727d6e78 100644 --- a/src/Umbraco.Web/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/DataTypeTreeController.cs @@ -135,7 +135,7 @@ namespace Umbraco.Web.Trees menu.Items.Add(Services.TextService, opensDialog: true); - menu.Items.Add(new MenuItem("rename", Services.TextService.Localize("actions/rename")) + menu.Items.Add(new MenuItem("rename", Services.TextService.Localize("actions", "rename")) { Icon = "icon icon-edit" }); diff --git a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs index 7a9a80c8fc..d3989e54bb 100644 --- a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs @@ -99,7 +99,7 @@ namespace Umbraco.Web.Trees menu.Items.Add(Services.TextService, opensDialog: true); - menu.Items.Add(new MenuItem("rename", Services.TextService.Localize("actions/rename")) + menu.Items.Add(new MenuItem("rename", Services.TextService.Localize("actions", "rename")) { Icon = "icon icon-edit" }); diff --git a/src/Umbraco.Web/Trees/MemberTreeController.cs b/src/Umbraco.Web/Trees/MemberTreeController.cs index c0a9d15cfa..0bf1787402 100644 --- a/src/Umbraco.Web/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTreeController.cs @@ -124,7 +124,7 @@ namespace Umbraco.Web.Trees if (id == Constants.System.RootString) { nodes.Add( - CreateTreeNode(Constants.Conventions.MemberTypes.AllMembersListId, id, queryStrings, Services.TextService.Localize("member/allMembers"), Constants.Icons.MemberType, true, + CreateTreeNode(Constants.Conventions.MemberTypes.AllMembersListId, id, queryStrings, Services.TextService.Localize("member", "allMembers"), Constants.Icons.MemberType, true, queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/list/" + Constants.Conventions.MemberTypes.AllMembersListId)); if (_isUmbracoProvider) diff --git a/src/Umbraco.Web/Trees/Tree.cs b/src/Umbraco.Web/Trees/Tree.cs index 4747d2495b..bc0511fe1e 100644 --- a/src/Umbraco.Web/Trees/Tree.cs +++ b/src/Umbraco.Web/Trees/Tree.cs @@ -50,7 +50,7 @@ namespace Umbraco.Web.Trees var label = $"[{tree.TreeAlias}]"; // try to look up a the localized tree header matching the tree alias - var localizedLabel = textService.Localize("treeHeaders/" + tree.TreeAlias); + var localizedLabel = textService.Localize("treeHeader", tree.TreeAlias); // if the localizedLabel returns [alias] then return the title if it's defined if (localizedLabel != null && localizedLabel.Equals(label, StringComparison.InvariantCultureIgnoreCase)) From 69e8ee087e22150ca2ffc56ed8a242bce91216b4 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Sun, 17 Jan 2021 21:38:52 +1300 Subject: [PATCH 055/289] Remove debug code, set comparer StringComparer.InvariantCultureIgnoreCase --- .../Implement/LocalizedTextService.cs | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs b/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs index c099960397..ad75e8a2d1 100644 --- a/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs +++ b/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -17,7 +18,6 @@ namespace Umbraco.Core.Services.Implement private readonly IDictionary>> _dictionarySource; private readonly IDictionary> _noAreaDictionarySource; private readonly char[] _splitter = new[] { '/' }; - /// /// Initializes with a file sources instance /// @@ -47,7 +47,7 @@ namespace Umbraco.Core.Services.Implement { var areaAliaValue = GetAreaStoredTranslations(source, xmlSource.Key); cultureDictionary.Add(xmlSource.Key, areaAliaValue); - var aliasValue = new Dictionary(); + var aliasValue = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (var area in areaAliaValue) { foreach (var alias in area.Value) @@ -118,19 +118,10 @@ namespace Umbraco.Core.Services.Implement public string Localize(string area, string alias, CultureInfo culture, IDictionary tokens = null) { if (culture == null) throw new ArgumentNullException(nameof(culture)); - var sw = System.Diagnostics.Stopwatch.StartNew(); - try - { // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode culture = ConvertToSupportedCultureWithRegionCode(culture); return GetFromDictionarySource(culture, area, alias, tokens); - } - finally - { - sw.Stop(); - System.Diagnostics.Debug.WriteLine($"Localize {area}/{alias} ({tokens?.Count}) ({sw.ElapsedTicks})"); - } } /// @@ -172,11 +163,11 @@ namespace Umbraco.Core.Services.Implement private Dictionary> GetAreaStoredTranslations(IDictionary> xmlSource, CultureInfo cult) { - var overallResult = new Dictionary>(); + var overallResult = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); var areas = xmlSource[cult].Value.XPathSelectElements("//area"); foreach (var area in areas) { - var result = new Dictionary(); + var result = new Dictionary(StringComparer.InvariantCultureIgnoreCase); var keys = area.XPathSelectElements("./key"); foreach (var key in keys) { @@ -192,12 +183,12 @@ namespace Umbraco.Core.Services.Implement } private Dictionary> GetAreaStoredTranslations(IDictionary>> dictionarySource, CultureInfo cult) { - var overallResult = new Dictionary>(); + var overallResult = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); var areaDict = dictionarySource[cult]; foreach (var area in areaDict) { - var result = new Dictionary(); + var result = new Dictionary(StringComparer.InvariantCultureIgnoreCase); var keys = area.Value.Keys; foreach (var key in keys) { From befa789d2361ba97733f49279dc3025493ae90fe Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Sun, 17 Jan 2021 21:56:40 +1300 Subject: [PATCH 056/289] Additional benchmarks --- ...dTextServiceGetAllStoredValuesBenchmarks.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs index e084931d9e..be45ffbd8c 100644 --- a/src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs +++ b/src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs @@ -82,6 +82,24 @@ namespace Umbraco.Tests.Benchmarks var result = _xmlService.Localize("language", culture); } } + [Benchmark] + public void OptimizedXmlGetAll() + { + for (int i = 0; i < 10000; i++) + { + var result = _optimized.GetAllStoredValues(culture); + } + + } + [Benchmark] + public void OptimizedDictGetAll() + { + for (int i = 0; i < 10000; i++) + { + var result = _optimizedDict.GetAllStoredValues(culture); + } + } + [Benchmark()] public void OptimizedXmlLocalize() { From 2c2ffc671cd0112b618347690f50a53c33e73bb8 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Sun, 17 Jan 2021 22:02:01 +1300 Subject: [PATCH 057/289] Add back in check for empty alias. --- .../Services/Implement/LocalizedTextService.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs b/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs index ad75e8a2d1..ff53dd2ec5 100644 --- a/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs +++ b/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs @@ -110,6 +110,11 @@ namespace Umbraco.Core.Services.Implement public string Localize(string key, CultureInfo culture, IDictionary tokens = null) { if (culture == null) throw new ArgumentNullException(nameof(culture)); + + //This is what the legacy ui service did + if (string.IsNullOrEmpty(key)) + return string.Empty; + var keyParts = key.Split(_splitter, StringSplitOptions.RemoveEmptyEntries); var area = keyParts.Length > 1 ? keyParts[0] : null; var alias = keyParts.Length > 1 ? keyParts[1] : keyParts[0]; @@ -118,8 +123,13 @@ namespace Umbraco.Core.Services.Implement public string Localize(string area, string alias, CultureInfo culture, IDictionary tokens = null) { if (culture == null) throw new ArgumentNullException(nameof(culture)); - // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode - culture = ConvertToSupportedCultureWithRegionCode(culture); + + //This is what the legacy ui service did + if (string.IsNullOrEmpty(alias)) + return string.Empty; + + // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode + culture = ConvertToSupportedCultureWithRegionCode(culture); return GetFromDictionarySource(culture, area, alias, tokens); } From 264259164e634de3cfe9b4bb26e80a4e150bc5da Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Sun, 17 Jan 2021 22:07:35 +1300 Subject: [PATCH 058/289] corrected comparer. --- .../Services/Implement/LocalizedTextService.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs b/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs index ff53dd2ec5..246481a48b 100644 --- a/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs +++ b/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs @@ -47,7 +47,7 @@ namespace Umbraco.Core.Services.Implement { var areaAliaValue = GetAreaStoredTranslations(source, xmlSource.Key); cultureDictionary.Add(xmlSource.Key, areaAliaValue); - var aliasValue = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + var aliasValue = new Dictionary(StringComparer.InvariantCulture); foreach (var area in areaAliaValue) { foreach (var alias in area.Value) @@ -173,11 +173,11 @@ namespace Umbraco.Core.Services.Implement private Dictionary> GetAreaStoredTranslations(IDictionary> xmlSource, CultureInfo cult) { - var overallResult = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); + var overallResult = new Dictionary>(StringComparer.InvariantCulture); var areas = xmlSource[cult].Value.XPathSelectElements("//area"); foreach (var area in areas) { - var result = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + var result = new Dictionary(StringComparer.InvariantCulture); var keys = area.XPathSelectElements("./key"); foreach (var key in keys) { @@ -193,12 +193,12 @@ namespace Umbraco.Core.Services.Implement } private Dictionary> GetAreaStoredTranslations(IDictionary>> dictionarySource, CultureInfo cult) { - var overallResult = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); + var overallResult = new Dictionary>(StringComparer.InvariantCulture); var areaDict = dictionarySource[cult]; foreach (var area in areaDict) { - var result = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + var result = new Dictionary(StringComparer.InvariantCulture); var keys = area.Value.Keys; foreach (var key in keys) { From 97d20baecb07dbac6436b915333540dbc44d3995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 20 Jan 2021 15:30:41 +0100 Subject: [PATCH 059/289] skipValidation for content save --- .../components/content/edit.controller.js | 22 ++++++++++++------- .../services/contenteditinghelper.service.js | 19 ++++++++++------ 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index f2dc0622c7..ac470642e7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -271,7 +271,7 @@ * @param {any} app the active content app */ function createButtons(content) { - + var isBlueprint = content.isBlueprint; if ($scope.page.isNew && $location.path().search(/contentBlueprints/i) !== -1) { @@ -456,7 +456,8 @@ create: $scope.page.isNew, action: args.action, showNotifications: args.showNotifications, - softRedirect: true + softRedirect: true, + skipValidation: args.skipValidation }).then(function (data) { //success init(); @@ -468,7 +469,9 @@ eventsService.emit("content.saved", { content: $scope.content, action: args.action }); - resetNestedFieldValiation(fieldsToRollback); + if($scope.contentForm.$invalid !== true) { + resetNestedFieldValiation(fieldsToRollback); + } ensureDirtyIsSetIfAnyVariantIsDirty(); return $q.when(data); @@ -476,7 +479,9 @@ function (err) { syncTreeNode($scope.content, $scope.content.path); - resetNestedFieldValiation(fieldsToRollback); + if($scope.contentForm.$invalid !== true) { + resetNestedFieldValiation(fieldsToRollback); + } return $q.reject(err); }); @@ -729,9 +734,9 @@ clearNotifications($scope.content); // TODO: Add "..." to save button label if there are more than one variant to publish - currently it just adds the elipses if there's more than 1 variant if (hasVariants($scope.content)) { - //before we launch the dialog we want to execute all client side validations first - if (formHelper.submitForm({ scope: $scope, action: "openSaveDialog" })) { + //before we launch the dialog we want to execute all client side validations first + if (formHelper.submitForm({ scope: $scope, action: "openSaveDialog", skipValidation:true, keepServerValidation:true })) { var dialog = { parentScope: $scope, view: "views/content/overlays/save.html", @@ -778,7 +783,8 @@ $scope.page.saveButtonState = "busy"; return performSave({ saveMethod: $scope.saveMethod(), - action: "save" + action: "save", + skipValidation: true }).then(function () { $scope.page.saveButtonState = "success"; }, function (err) { @@ -981,7 +987,7 @@ $scope.appChanged = function (activeApp) { $scope.activeApp = activeApp; - + _.forEach($scope.content.apps, function (app) { app.active = false; if (app.alias === $scope.activeApp.alias) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index 6d41ea087d..fd0bd3efd9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -84,7 +84,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt //when true, the url will change but it won't actually re-route //this is merely here for compatibility, if only the content/media/members used this service we'd prob be ok but tons of editors //use this service unfortunately and probably packages too. - args.softRedirect = false; + args.softRedirect = false; } @@ -93,7 +93,12 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt //we will use the default one for content if not specified var rebindCallback = args.rebindCallback === undefined ? self.reBindChangedProperties : args.rebindCallback; - if (formHelper.submitForm({ scope: args.scope, action: args.action })) { + var formSubmitOptions = { scope: args.scope, action: args.action }; + if(args.skipValidation === true) { + formSubmitOptions.skipValidation = true; + formSubmitOptions.keepServerValidation = true; + } + if (formHelper.submitForm(formSubmitOptions)) { return args.saveMethod(args.content, args.create, fileManager.getFiles(), args.showNotifications) .then(function (data) { @@ -298,7 +303,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt } // if publishing is allowed also allow schedule publish - // we add this manually becuase it doesn't have a permission so it wont + // we add this manually becuase it doesn't have a permission so it wont // get picked up by the loop through permissions if (_.contains(args.content.allowedActions, "U")) { buttons.subButtons.push(createButtonDefinition("SCHEDULE")); @@ -622,7 +627,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt if (!args.err) { throw "args.err cannot be null"; } - + //When the status is a 400 status with a custom header: X-Status-Reason: Validation failed, we have validation errors. //Otherwise the error is probably due to invalid data (i.e. someone mucking around with the ids or something). //Or, some strange server error @@ -640,7 +645,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt if (!this.redirectToCreatedContent(args.err.data.id, args.softRedirect) || args.softRedirect) { // If we are not redirecting it's because this is not newly created content, else in some cases we are - // soft-redirecting which means the URL will change but the route wont (i.e. creating content). + // soft-redirecting which means the URL will change but the route wont (i.e. creating content). // In this case we need to detect what properties have changed and re-bind them with the server data. if (args.rebindCallback && angular.isFunction(args.rebindCallback)) { @@ -687,7 +692,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt if (!this.redirectToCreatedContent(args.redirectId ? args.redirectId : args.savedContent.id, args.softRedirect) || args.softRedirect) { // If we are not redirecting it's because this is not newly created content, else in some cases we are - // soft-redirecting which means the URL will change but the route wont (i.e. creating content). + // soft-redirecting which means the URL will change but the route wont (i.e. creating content). // In this case we need to detect what properties have changed and re-bind them with the server data. if (args.rebindCallback && angular.isFunction(args.rebindCallback)) { @@ -723,7 +728,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt navigationService.setSoftRedirect(); } //change to new path - $location.path("/" + $routeParams.section + "/" + $routeParams.tree + "/" + $routeParams.method + "/" + id); + $location.path("/" + $routeParams.section + "/" + $routeParams.tree + "/" + $routeParams.method + "/" + id); //don't add a browser history for this $location.replace(); return true; From 4a400b125b0b0a8b694f9ff91fc23c8e6cccad8f Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 22 Jan 2021 00:14:37 +1100 Subject: [PATCH 060/289] Fixes merge issues and updates npoco to latest 4.x --- src/Umbraco.Core/Umbraco.Core.csproj | 2 +- src/Umbraco.Examine/Umbraco.Examine.csproj | 2 +- .../Umbraco.Tests.Benchmarks.csproj | 2 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 6 ++---- src/Umbraco.Web/Umbraco.Web.csproj | 2 +- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index f890faecb3..eaf13eb955 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -91,7 +91,7 @@ - + 2.8.0 diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index 55ed24b3e8..91a2fdda99 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -56,7 +56,7 @@ all - + 3.3.0 runtime; build; native; contentfiles; analyzers diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index 48d69cf757..df2af67573 100644 --- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -93,4 +93,4 @@ - \ No newline at end of file + diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index c7b9954ca4..a475a6a355 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -103,9 +103,7 @@ 4.14.5 - - - + @@ -648,4 +646,4 @@ - \ No newline at end of file + diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 86c21b7182..35218e5787 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -97,7 +97,7 @@ - + 3.3.0 runtime; build; native; contentfiles; analyzers From eec81a3890bcc974e9a92bb5127631bf216ac44b Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 22 Jan 2021 00:15:07 +1100 Subject: [PATCH 061/289] updates sql usage in DatabaseDataSource --- src/Umbraco.Core/Constants-SqlTemplates.cs | 8 +- .../NuCache/DataSource/DatabaseDataSource.cs | 241 ++++++++++-------- 2 files changed, 145 insertions(+), 104 deletions(-) diff --git a/src/Umbraco.Core/Constants-SqlTemplates.cs b/src/Umbraco.Core/Constants-SqlTemplates.cs index 3def094702..940524cd1a 100644 --- a/src/Umbraco.Core/Constants-SqlTemplates.cs +++ b/src/Umbraco.Core/Constants-SqlTemplates.cs @@ -27,8 +27,14 @@ internal static class NuCacheDatabaseDataSource { - public const string ContentSourcesSelect1 = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesSelect1"; + public const string ContentSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesSelect"; + public const string ContentSourcesSelectUmbracoNodeJoin = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesSelectUmbracoNodeJoin"; public const string ContentSourcesCount = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesCount"; + public const string MediaSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.MediaSourcesSelect"; + public const string MediaSourcesCount = "Umbraco.Web.PublishedCache.NuCache.DataSource.MediaSourcesCount"; + public const string ObjectTypeNotTrashedFilter = "Umbraco.Web.PublishedCache.NuCache.DataSource.ObjectTypeNotTrashedFilter"; + public const string OrderByLevelIdSortOrder = "Umbraco.Web.PublishedCache.NuCache.DataSource.OrderByLevelIdSortOrder"; + } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index 98dbf1f85c..a49a97568b 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -28,9 +28,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // we want arrays, we want them all loaded, not an enumerable - private Sql ContentSourcesSelect(IScope scope, Func, Sql> joins = null) + private Sql SqlContentSourcesSelect(IScope scope, Func, Sql> joins = null) { - var sqlTemplate = scope.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.ContentSourcesSelect1, tsql => + var sqlTemplate = scope.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.ContentSourcesSelect, tsql => tsql.Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Uid"), x => Alias(x.Level, "Level"), x => Alias(x.Path, "Path"), x => Alias(x.SortOrder, "SortOrder"), x => Alias(x.ParentId, "ParentId"), x => Alias(x.CreateDate, "CreateDate"), x => Alias(x.UserId, "CreatorId")) @@ -75,12 +75,48 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return sql; } + private Sql SqlContentSourcesSelectUmbracoNodeJoin(Sql s) + { + var syntax = s.SqlContext.SqlSyntax; + var sqlTemplate = s.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.ContentSourcesSelectUmbracoNodeJoin, s => + s.InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")); + var sql = sqlTemplate.Sql(); + return sql; + } + + private Sql SqlWhereNodeId(Sql s, int id) + { + var syntax = s.SqlContext.SqlSyntax; + var sqlTemplate = s.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.ContentSourcesSelectUmbracoNodeJoin, s => + s.Where(x => x.NodeId == SqlTemplate.Arg("id"), "x")); + var sql = sqlTemplate.Sql(id); + return sql; + } + + private Sql SqlOrderByLevelIdSortOrder(Sql s) + { + var syntax = s.SqlContext.SqlSyntax; + var sqlTemplate = s.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.OrderByLevelIdSortOrder, s => + s.OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder)); + var sql = sqlTemplate.Sql(); + return sql; + } + + private Sql SqlObjectTypeNotTrashed(Sql s, Guid nodeObjectType) + { + var syntax = s.SqlContext.SqlSyntax; + var sqlTemplate = s.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.ObjectTypeNotTrashedFilter, s => + s.Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && !x.Trashed)); + var sql = sqlTemplate.Sql(nodeObjectType); + return sql; + } + /// /// Returns a slightly more optimized query to use for the document counting when paging over the content sources /// /// /// - private Sql ContentSourcesCount(IScope scope, Func, Sql> joins = null) + private Sql SqlContentSourcesCount(IScope scope, Func, Sql> joins = null) { var sqlTemplate = scope.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.ContentSourcesCount, tsql => tsql.Select(x => Alias(x.NodeId, "Id")) @@ -104,11 +140,55 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return sql; } + private Sql SqlMediaSourcesSelect(IScope scope, Func, Sql> joins = null) + { + var sqlTemplate = scope.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.MediaSourcesSelect, tsql => + tsql.Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Uid"), + x => Alias(x.Level, "Level"), x => Alias(x.Path, "Path"), x => Alias(x.SortOrder, "SortOrder"), x => Alias(x.ParentId, "ParentId"), + x => Alias(x.CreateDate, "CreateDate"), x => Alias(x.UserId, "CreatorId")) + .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) + .AndSelect(x => Alias(x.Id, "VersionId"), x => Alias(x.Text, "EditName"), x => Alias(x.VersionDate, "EditVersionDate"), x => Alias(x.UserId, "EditWriterId")) + .AndSelect("nuEdit", x => Alias(x.Data, "EditData")) + .AndSelect("nuEdit", x => Alias(x.RawData, "EditDataRaw")) + .From()); + + var sql = sqlTemplate.Sql(); + + if (joins != null) + sql = joins(sql); + + // TODO: We can't use a template with this one because of the 'right.Published' ends up being a parameter so not sure how we can do that + sql = sql + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) + .LeftJoin("nuEdit").On((left, right) => left.NodeId == right.NodeId && !right.Published, aliasRight: "nuEdit"); + + return sql; + } + private Sql SqlMediaSourcesCount(IScope scope, Func, Sql> joins = null) + { + var sqlTemplate = scope.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.MediaSourcesCount, tsql => + tsql.Select(x => Alias(x.NodeId, "Id")).From()); + + var sql = sqlTemplate.Sql(); + + if (joins != null) + sql = joins(sql); + + // TODO: We can't use a template with this one because of the 'right.Current' ends up being a parameter so not sure how we can do that + sql = sql + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current); + + return sql; + } + public ContentNodeKit GetContentSource(IScope scope, int id) { - var sql = ContentSourcesSelect(scope) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && x.NodeId == id && !x.Trashed) - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + var sql = SqlContentSourcesSelect(scope) + .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Document)) + .Append(SqlWhereNodeId(scope.SqlContext.Sql(), id)) + .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext.Sql())); var dto = scope.Database.Fetch(sql).FirstOrDefault(); @@ -120,14 +200,14 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public IEnumerable GetAllContentSources(IScope scope) { - // Create a different query for the SQL vs the COUNT Sql since the auto-generated COUNT Sql will be inneficient - var sql = ContentSourcesSelect(scope) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + var sql = SqlContentSourcesSelect(scope) + .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Document)) + .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext.Sql())); + + // Use a more efficient COUNT query + var sqlCountQuery = SqlContentSourcesCount(scope) + .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Document)); - // create a more efficient COUNT query without the join on the cmsContentNu table - var sqlCountQuery = ContentSourcesCount(scope) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed); var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); @@ -138,26 +218,22 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource foreach (var row in scope.Database.QueryPaged(PageSize, sql, sqlCount)) { yield return CreateContentNodeKit(row, serializer); - } + } } public IEnumerable GetBranchContentSources(IScope scope, int id) { - var syntax = scope.SqlContext.SqlSyntax; - var sql = ContentSourcesSelect(scope, - s => s.InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) - .Where(x => x.NodeId == id, "x") - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + var sql = SqlContentSourcesSelect(scope, SqlContentSourcesSelectUmbracoNodeJoin) + .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Document)) + .Append(SqlWhereNodeId(scope.SqlContext.Sql(), id)) + .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext.Sql())); - // create a more efficient COUNT query without the join on the cmsContentNu table - var sqlCountQuery = ContentSourcesCount(scope, - s => s.InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) - .Where(x => x.NodeId == id, "x"); + // Use a more efficient COUNT query + var sqlCountQuery = SqlContentSourcesCount(scope, SqlContentSourcesSelectUmbracoNodeJoin) + .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Document)) + .Append(SqlWhereNodeId(scope.SqlContext.Sql(), id)); var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); - var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. @@ -166,20 +242,21 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource foreach (var row in scope.Database.QueryPaged(PageSize, sql, sqlCount)) { yield return CreateContentNodeKit(row, serializer); - } + } } public IEnumerable GetTypeContentSources(IScope scope, IEnumerable ids) { if (!ids.Any()) yield break; - var sql = ContentSourcesSelect(scope) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) + var sql = SqlContentSourcesSelect(scope) + .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Document)) .WhereIn(x => x.ContentTypeId, ids) - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext.Sql())); - var sqlCountQuery = ContentSourcesCount(scope) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) + // Use a more efficient COUNT query + var sqlCountQuery = SqlContentSourcesCount(scope, SqlContentSourcesSelectUmbracoNodeJoin) + .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Document)) .WhereIn(x => x.ContentTypeId, ids); var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); @@ -191,54 +268,15 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource foreach (var row in scope.Database.QueryPaged(PageSize, sql, sqlCount)) { yield return CreateContentNodeKit(row, serializer); - } - } - - private Sql MediaSourcesSelect(IScope scope, Func, Sql> joins = null) - { - var sql = scope.SqlContext.Sql() - - .Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Uid"), - x => Alias(x.Level, "Level"), x => Alias(x.Path, "Path"), x => Alias(x.SortOrder, "SortOrder"), x => Alias(x.ParentId, "ParentId"), - x => Alias(x.CreateDate, "CreateDate"), x => Alias(x.UserId, "CreatorId")) - .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) - .AndSelect(x => Alias(x.Id, "VersionId"), x => Alias(x.Text, "EditName"), x => Alias(x.VersionDate, "EditVersionDate"), x => Alias(x.UserId, "EditWriterId")) - .AndSelect("nuEdit", x => Alias(x.Data, "EditData")) - .AndSelect("nuEdit", x => Alias(x.RawData, "EditDataRaw")) - .From(); - - if (joins != null) - sql = joins(sql); - - sql = sql - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) - .LeftJoin("nuEdit").On((left, right) => left.NodeId == right.NodeId && !right.Published, aliasRight: "nuEdit"); - - return sql; - } - private Sql MediaSourcesCount(IScope scope, Func, Sql> joins = null) - { - var sql = scope.SqlContext.Sql() - - .Select(x => Alias(x.NodeId, "Id")) - .From(); - - if (joins != null) - sql = joins(sql); - - sql = sql - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current); - - return sql; + } } public ContentNodeKit GetMediaSource(IScope scope, int id) { - var sql = MediaSourcesSelect(scope) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && x.NodeId == id && !x.Trashed) - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + var sql = SqlMediaSourcesSelect(scope) + .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Media)) + .Append(SqlWhereNodeId(scope.SqlContext.Sql(), id)) + .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext.Sql())); var dto = scope.Database.Fetch(sql).FirstOrDefault(); @@ -250,13 +288,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public IEnumerable GetAllMediaSources(IScope scope) { - var sql = MediaSourcesSelect(scope) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - - var sqlCountQuery = MediaSourcesCount(scope) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed); + var sql = SqlMediaSourcesSelect(scope) + .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Media)) + .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext.Sql())); + // Use a more efficient COUNT query + var sqlCountQuery = SqlMediaSourcesCount(scope) + .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Media)); var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); @@ -267,23 +305,20 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource foreach (var row in scope.Database.QueryPaged(PageSize, sql, sqlCount)) { yield return CreateMediaNodeKit(row, serializer); - } + } } public IEnumerable GetBranchMediaSources(IScope scope, int id) { - var syntax = scope.SqlContext.SqlSyntax; - var sql = MediaSourcesSelect(scope, - s => s.InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) - .Where(x => x.NodeId == id, "x") - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - - var sqlCountQuery = MediaSourcesCount(scope, - s => s.InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) - .Where(x => x.NodeId == id, "x"); + var sql = SqlMediaSourcesSelect(scope, SqlContentSourcesSelectUmbracoNodeJoin) + .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Media)) + .Append(SqlWhereNodeId(scope.SqlContext.Sql(), id)) + .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext.Sql())); + // Use a more efficient COUNT query + var sqlCountQuery = SqlMediaSourcesCount(scope) + .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Media)) + .Append(SqlWhereNodeId(scope.SqlContext.Sql(), id)); var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); @@ -294,22 +329,22 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource foreach (var row in scope.Database.QueryPaged(PageSize, sql, sqlCount)) { yield return CreateMediaNodeKit(row, serializer); - } + } } public IEnumerable GetTypeMediaSources(IScope scope, IEnumerable ids) { if (!ids.Any()) yield break; - var sql = MediaSourcesSelect(scope) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) + var sql = SqlMediaSourcesSelect(scope) + .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Media)) .WhereIn(x => x.ContentTypeId, ids) - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - - var sqlCountQuery = MediaSourcesCount(scope) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) - .WhereIn(x => x.ContentTypeId, ids); + .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext.Sql())); + // Use a more efficient COUNT query + var sqlCountQuery = SqlMediaSourcesCount(scope) + .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Media)) + .WhereIn(x => x.ContentTypeId, ids); var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); @@ -320,7 +355,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource foreach (var row in scope.Database.QueryPaged(PageSize, sql, sqlCount)) { yield return CreateMediaNodeKit(row, serializer); - } + } } private ContentNodeKit CreateContentNodeKit(ContentSourceDto dto, IContentCacheDataSerializer serializer) From e01f8319f6b78feb0a64dd9d96a3c1f232f3bb33 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 22 Jan 2021 01:12:26 +1100 Subject: [PATCH 062/289] Updates the new nodedto index to include 2 cols as the key and update the includes, this is required for more optimal querying for nucache and others. --- src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs | 4 ++-- .../{V_8_11_0 => V_8_12_0}/AddCmsContentNuByteColumn.cs | 2 +- .../Upgrade/{V_8_11_0 => V_8_12_0}/UpgradedIncludeIndexes.cs | 2 +- src/Umbraco.Core/Persistence/Dtos/NodeDto.cs | 2 +- src/Umbraco.Core/Umbraco.Core.csproj | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) rename src/Umbraco.Core/Migrations/Upgrade/{V_8_11_0 => V_8_12_0}/AddCmsContentNuByteColumn.cs (90%) rename src/Umbraco.Core/Migrations/Upgrade/{V_8_11_0 => V_8_12_0}/UpgradedIncludeIndexes.cs (98%) diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index a60b046212..2a24c800b5 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Migrations.Upgrade.V_8_1_0; using Umbraco.Core.Migrations.Upgrade.V_8_6_0; using Umbraco.Core.Migrations.Upgrade.V_8_9_0; using Umbraco.Core.Migrations.Upgrade.V_8_10_0; -using Umbraco.Core.Migrations.Upgrade.V_8_11_0; +using Umbraco.Core.Migrations.Upgrade.V_8_12_0; namespace Umbraco.Core.Migrations.Upgrade { @@ -203,7 +203,7 @@ namespace Umbraco.Core.Migrations.Upgrade // to 8.10.0 To("{D6A8D863-38EC-44FB-91EC-ACD6A668BD18}"); - // to 8.11.0... + // to 8.12.0... To("{8DDDCD0B-D7D5-4C97-BD6A-6B38CA65752F}"); To("{4695D0C9-0729-4976-985B-048D503665D8}"); diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_11_0/AddCmsContentNuByteColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_12_0/AddCmsContentNuByteColumn.cs similarity index 90% rename from src/Umbraco.Core/Migrations/Upgrade/V_8_11_0/AddCmsContentNuByteColumn.cs rename to src/Umbraco.Core/Migrations/Upgrade/V_8_12_0/AddCmsContentNuByteColumn.cs index dfde1f0577..7c793688ec 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_11_0/AddCmsContentNuByteColumn.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_12_0/AddCmsContentNuByteColumn.cs @@ -1,7 +1,7 @@ using System.Linq; using Umbraco.Core.Persistence.Dtos; -namespace Umbraco.Core.Migrations.Upgrade.V_8_11_0 +namespace Umbraco.Core.Migrations.Upgrade.V_8_12_0 { public class AddCmsContentNuByteColumn : MigrationBase { diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_11_0/UpgradedIncludeIndexes.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_12_0/UpgradedIncludeIndexes.cs similarity index 98% rename from src/Umbraco.Core/Migrations/Upgrade/V_8_11_0/UpgradedIncludeIndexes.cs rename to src/Umbraco.Core/Migrations/Upgrade/V_8_12_0/UpgradedIncludeIndexes.cs index 6919558fc3..d88abdef75 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_11_0/UpgradedIncludeIndexes.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_12_0/UpgradedIncludeIndexes.cs @@ -3,7 +3,7 @@ using Umbraco.Core.Migrations.Expressions.Execute.Expressions; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; -namespace Umbraco.Core.Migrations.Upgrade.V_8_11_0 +namespace Umbraco.Core.Migrations.Upgrade.V_8_12_0 { public class UpgradedIncludeIndexes : MigrationBase { diff --git a/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs b/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs index 62475af833..207195e594 100644 --- a/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs @@ -58,7 +58,7 @@ namespace Umbraco.Core.Persistence.Dtos [Column("nodeObjectType")] // TODO: db rename to 'objectType' [NullSetting(NullSetting = NullSettings.Null)] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType", IncludeColumns = "parentId,level,path,sortOrder,trashed,nodeUser,text,createDate")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType", ForColumns = "nodeObjectType,trashed", IncludeColumns = "uniqueId,parentId,level,path,sortOrder,nodeUser,text,createDate")] public Guid? NodeObjectType { get; set; } [Column("createDate")] diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index eaf13eb955..6c473235a7 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -133,8 +133,8 @@ - - + + From c5bd53770a8289a8d0c54953572d64d701012dbe Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 22 Jan 2021 01:13:03 +1100 Subject: [PATCH 063/289] fixes sql template --- .../PublishedCache/NuCache/DataSource/DatabaseDataSource.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index a49a97568b..88e364634e 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -106,8 +106,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { var syntax = s.SqlContext.SqlSyntax; var sqlTemplate = s.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.ObjectTypeNotTrashedFilter, s => - s.Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && !x.Trashed)); - var sql = sqlTemplate.Sql(nodeObjectType); + s.Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.Trashed == SqlTemplate.Arg("trashed"))); + var sql = sqlTemplate.Sql(nodeObjectType, false); return sql; } From 808e58b60f97e7b2a7015656bfffd293bed22a6e Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 22 Jan 2021 16:14:25 +1100 Subject: [PATCH 064/289] fixes up inconsistencies --- src/Umbraco.Core/Constants-SqlTemplates.cs | 6 ++-- .../NuCache/DataSource/DatabaseDataSource.cs | 28 +++++++++++++------ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Core/Constants-SqlTemplates.cs b/src/Umbraco.Core/Constants-SqlTemplates.cs index 940524cd1a..8529e6bfbc 100644 --- a/src/Umbraco.Core/Constants-SqlTemplates.cs +++ b/src/Umbraco.Core/Constants-SqlTemplates.cs @@ -27,8 +27,10 @@ internal static class NuCacheDatabaseDataSource { - public const string ContentSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesSelect"; - public const string ContentSourcesSelectUmbracoNodeJoin = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesSelectUmbracoNodeJoin"; + public const string WhereNodeId = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeId"; + public const string WhereNodeIdX = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeIdX"; + public const string SourcesSelectUmbracoNodeJoin = "Umbraco.Web.PublishedCache.NuCache.DataSource.SourcesSelectUmbracoNodeJoin"; + public const string ContentSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesSelect"; public const string ContentSourcesCount = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesCount"; public const string MediaSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.MediaSourcesSelect"; public const string MediaSourcesCount = "Umbraco.Web.PublishedCache.NuCache.DataSource.MediaSourcesCount"; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index 88e364634e..3bd30d2d73 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -78,8 +78,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource private Sql SqlContentSourcesSelectUmbracoNodeJoin(Sql s) { var syntax = s.SqlContext.SqlSyntax; - var sqlTemplate = s.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.ContentSourcesSelectUmbracoNodeJoin, s => - s.InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")); + var sqlTemplate = s.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.SourcesSelectUmbracoNodeJoin, s => + s.InnerJoin("x") + .On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")); var sql = sqlTemplate.Sql(); return sql; } @@ -87,7 +88,16 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource private Sql SqlWhereNodeId(Sql s, int id) { var syntax = s.SqlContext.SqlSyntax; - var sqlTemplate = s.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.ContentSourcesSelectUmbracoNodeJoin, s => + var sqlTemplate = s.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.WhereNodeId, s => + s.Where(x => x.NodeId == SqlTemplate.Arg("id"))); + var sql = sqlTemplate.Sql(id); + return sql; + } + + private Sql SqlWhereNodeIdX(Sql s, int id) + { + var syntax = s.SqlContext.SqlSyntax; + var sqlTemplate = s.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.WhereNodeIdX, s => s.Where(x => x.NodeId == SqlTemplate.Arg("id"), "x")); var sql = sqlTemplate.Sql(id); return sql; @@ -225,13 +235,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { var sql = SqlContentSourcesSelect(scope, SqlContentSourcesSelectUmbracoNodeJoin) .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Document)) - .Append(SqlWhereNodeId(scope.SqlContext.Sql(), id)) + .Append(SqlWhereNodeIdX(scope.SqlContext.Sql(), id)) .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext.Sql())); // Use a more efficient COUNT query var sqlCountQuery = SqlContentSourcesCount(scope, SqlContentSourcesSelectUmbracoNodeJoin) .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Document)) - .Append(SqlWhereNodeId(scope.SqlContext.Sql(), id)); + .Append(SqlWhereNodeIdX(scope.SqlContext.Sql(), id)); var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); @@ -255,7 +265,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext.Sql())); // Use a more efficient COUNT query - var sqlCountQuery = SqlContentSourcesCount(scope, SqlContentSourcesSelectUmbracoNodeJoin) + var sqlCountQuery = SqlContentSourcesCount(scope) .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Document)) .WhereIn(x => x.ContentTypeId, ids); var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); @@ -312,13 +322,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { var sql = SqlMediaSourcesSelect(scope, SqlContentSourcesSelectUmbracoNodeJoin) .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Media)) - .Append(SqlWhereNodeId(scope.SqlContext.Sql(), id)) + .Append(SqlWhereNodeIdX(scope.SqlContext.Sql(), id)) .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext.Sql())); // Use a more efficient COUNT query - var sqlCountQuery = SqlMediaSourcesCount(scope) + var sqlCountQuery = SqlMediaSourcesCount(scope, SqlContentSourcesSelectUmbracoNodeJoin) .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Media)) - .Append(SqlWhereNodeId(scope.SqlContext.Sql(), id)); + .Append(SqlWhereNodeIdX(scope.SqlContext.Sql(), id)); var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); From 0cb4952338e0dc1ad82cda3b0a56145626c201c6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 25 Jan 2021 15:56:01 +1100 Subject: [PATCH 065/289] Don't keep re-creating serializer settings on deserialize --- .../JsonContentNestedDataSerializer.cs | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs index 47f07b8b1d..e7bb5656bb 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs @@ -8,26 +8,25 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public class JsonContentNestedDataSerializer : IContentCacheDataSerializer { + // by default JsonConvert will deserialize our numeric values as Int64 + // which is bad, because they were Int32 in the database - take care + private readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings + { + Converters = new List { new ForceInt32Converter() }, + + // Explicitly specify date handling so that it's consistent and follows the same date handling as MessagePack + DateParseHandling = DateParseHandling.DateTime, + DateFormatHandling = DateFormatHandling.IsoDateFormat, + DateTimeZoneHandling = DateTimeZoneHandling.Utc, + DateFormatString = "o" + }; + public ContentCacheDataModel Deserialize(int contentTypeId, string stringData, byte[] byteData) { if (stringData == null && byteData != null) throw new NotSupportedException($"{typeof(JsonContentNestedDataSerializer)} does not support byte[] serialization"); - // by default JsonConvert will deserialize our numeric values as Int64 - // which is bad, because they were Int32 in the database - take care - - var settings = new JsonSerializerSettings - { - Converters = new List { new ForceInt32Converter() }, - - // Explicitly specify date handling so that it's consistent and follows the same date handling as MessagePack - DateParseHandling = DateParseHandling.DateTime, - DateFormatHandling = DateFormatHandling.IsoDateFormat, - DateTimeZoneHandling = DateTimeZoneHandling.Utc, - DateFormatString = "o" - }; - - return JsonConvert.DeserializeObject(stringData, settings); + return JsonConvert.DeserializeObject(stringData, _jsonSerializerSettings); } public ContentCacheDataSerializationResult Serialize(int contentTypeId, ContentCacheDataModel model) From 9200c46cd0b33aed42c4e66ee9e38ce6afc4d145 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 27 Jan 2021 15:11:16 +1100 Subject: [PATCH 066/289] Ensure LazyCompressedString doesn't decompress when debugging --- .../DataSource/LazyCompressedString.cs | 68 +++++++++++++++++-- 1 file changed, 63 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs index 3e0e796d36..2be2568f7e 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs @@ -1,5 +1,6 @@ using K4os.Compression.LZ4; using System; +using System.Diagnostics; using System.Text; using Umbraco.Core.Exceptions; @@ -8,6 +9,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// /// Lazily decompresses a LZ4 Pickler compressed UTF8 string /// + [DebuggerDisplay("{Display}")] internal struct LazyCompressedString { private byte[] _bytes; @@ -22,29 +24,85 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { _locker = new object(); _bytes = bytes; - _str = null; + _str = null; } public byte[] GetBytes() { if (_bytes == null) + { throw new InvalidOperationException("The bytes have already been expanded"); + } + return _bytes; } - public override string ToString() + /// + /// Returns the decompressed string from the bytes. This methods can only be called once. + /// + /// + /// Throws if this is called more than once + public string DecompressString() { - if (_str != null) return _str; + if (_str != null) + { + return _str; + } + lock (_locker) { - if (_str != null) return _str; // double check - if (_bytes == null) throw new PanicException("Bytes have already been cleared"); + if (_str != null) + { + // double check + return _str; + } + + if (_bytes == null) + { + throw new InvalidOperationException("Bytes have already been cleared"); + } + _str = Encoding.UTF8.GetString(LZ4Pickler.Unpickle(_bytes)); _bytes = null; } return _str; } + /// + /// Used to display debugging output since ToString() can only be called once + /// + private string Display + { + get + { + if (_str != null) + { + return $"Decompressed: {_str}"; + } + + lock (_locker) + { + if (_str != null) + { + // double check + return $"Decompressed: {_str}"; + } + + if (_bytes == null) + { + // This shouldn't happen + throw new PanicException("Bytes have already been cleared"); + } + else + { + return $"Compressed Bytes: {_bytes.Length}"; + } + } + } + } + + public override string ToString() => DecompressString(); + public static implicit operator string(LazyCompressedString l) => l.ToString(); } From 57708960f0429d0e116fc06754ad45f7e370ec8d Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 27 Jan 2021 15:16:22 +1100 Subject: [PATCH 067/289] reverts change for IsDecompressed --- .../NuCache/DataSource/SerializerBase.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs index 6fdb4dd188..7a0f6962b6 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/SerializerBase.cs @@ -185,16 +185,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } else if (value is LazyCompressedString lazyCompressedString) { - if (lazyCompressedString.IsDecompressed) - { - PrimitiveSerializer.Char.WriteTo(PrefixString, stream); - PrimitiveSerializer.String.WriteTo(lazyCompressedString, stream); - } - else - { - PrimitiveSerializer.Char.WriteTo(PrefixCompressedStringByteArray, stream); - PrimitiveSerializer.Bytes.WriteTo(lazyCompressedString.GetBytes(), stream); - } + PrimitiveSerializer.Char.WriteTo(PrefixCompressedStringByteArray, stream); + PrimitiveSerializer.Bytes.WriteTo(lazyCompressedString.GetBytes(), stream); } else if (value is sbyte signedByteValue) { From 4bfba2eebab1e7f091c5fb891d006191e2337bbe Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 27 Jan 2021 15:31:40 +1100 Subject: [PATCH 068/289] adds some notes --- .../DataSource/MsgPackContentNestedDataSerializer.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index 42468ad930..6ffa130da0 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -79,6 +79,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// Used during serialization to compress properties /// /// + /// + /// This will essentially 'double compress' property data. The MsgPack data as a whole will already be compressed + /// but this will go a step further and double compress property data so that it is stored in the nucache file + /// as compressed bytes and therefore will exist in memory as compressed bytes. That is, until the bytes are + /// read/decompressed as a string to be displayed on the front-end. This allows for potentially a significant + /// memory savings but could also affect performance of first rendering pages while decompression occurs. + /// private void Compress(int contentTypeId, ContentCacheDataModel model) { foreach(var propertyAliasToData in model.PropertyData) From 32f88dba95f31e6cc87ebaabe0404578490d4976 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 28 Jan 2021 13:50:18 +1100 Subject: [PATCH 069/289] removes CompressedStorageAttribute, makes IPropertyCacheCompressionOptions which can be defined in DI to configure what properties can be compressed in memory and new IReadOnlyContentBase to provide some context to the IPropertyCacheCompressionOptions --- src/Umbraco.Core/Models/IContentBase.cs | 1 + .../Models/IReadOnlyContentBase.cs | 72 +++++++++++++++++++ .../Models/ReadOnlyContentBaseAdapter.cs | 42 +++++++++++ .../CompressedStorageAttribute.cs | 21 ------ ...StoragePropertyEditorCompressionOptions.cs | 47 ------------ ...ptions.cs => IPropertyCacheCompression.cs} | 9 ++- .../IPropertyCacheCompressionOptions.cs | 9 +++ .../NoopPropertyCacheCompressionOptions.cs | 12 ++++ .../PropertyCacheCompression.cs | 49 +++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 9 ++- .../ContentSerializationTests.cs | 17 +++-- .../BlockListPropertyEditor.cs | 1 - .../PropertyEditors/GridPropertyEditor.cs | 1 - .../PropertyEditors/MarkdownPropertyEditor.cs | 1 - .../NestedContentPropertyEditor.cs | 1 - .../PropertyEditors/RichTextPropertyEditor.cs | 1 - .../PropertyEditors/TextAreaPropertyEditor.cs | 1 - .../NuCache/DataSource/ContentSourceDto.cs | 10 ++- .../NuCache/DataSource/DatabaseDataSource.cs | 10 +-- .../DataSource/IContentCacheDataSerializer.cs | 13 +--- .../JsonContentNestedDataSerializer.cs | 5 +- .../MsgPackContentNestedDataSerializer.cs | 31 ++++---- ...gPackContentNestedDataSerializerFactory.cs | 15 ++-- .../PublishedCache/NuCache/NuCacheComposer.cs | 6 +- .../NuCache/PublishedSnapshotService.cs | 2 +- 25 files changed, 258 insertions(+), 128 deletions(-) create mode 100644 src/Umbraco.Core/Models/IReadOnlyContentBase.cs create mode 100644 src/Umbraco.Core/Models/ReadOnlyContentBaseAdapter.cs delete mode 100644 src/Umbraco.Core/PropertyEditors/CompressedStorageAttribute.cs delete mode 100644 src/Umbraco.Core/PropertyEditors/CompressedStoragePropertyEditorCompressionOptions.cs rename src/Umbraco.Core/PropertyEditors/{IPropertyCompressionOptions.cs => IPropertyCacheCompression.cs} (50%) create mode 100644 src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs create mode 100644 src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs create mode 100644 src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index 0f660181fb..f4fae44ffc 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -4,6 +4,7 @@ using Umbraco.Core.Models.Entities; namespace Umbraco.Core.Models { + /// /// Provides a base class for content items. /// diff --git a/src/Umbraco.Core/Models/IReadOnlyContentBase.cs b/src/Umbraco.Core/Models/IReadOnlyContentBase.cs new file mode 100644 index 0000000000..3ed39b63ab --- /dev/null +++ b/src/Umbraco.Core/Models/IReadOnlyContentBase.cs @@ -0,0 +1,72 @@ +using System; + +namespace Umbraco.Core.Models +{ + public interface IReadOnlyContentBase + { + /// + /// Gets the integer identifier of the entity. + /// + int Id { get; } + + /// + /// Gets the Guid unique identifier of the entity. + /// + Guid Key { get; } + + /// + /// Gets the creation date. + /// + DateTime CreateDate { get; } + + /// + /// Gets the last update date. + /// + DateTime UpdateDate { get; } + + /// + /// Gets the name of the entity. + /// + string Name { get; } + + /// + /// Gets the identifier of the user who created this entity. + /// + int CreatorId { get; } + + /// + /// Gets the identifier of the parent entity. + /// + int ParentId { get; } + + /// + /// Gets the level of the entity. + /// + int Level { get; } + + /// + /// Gets the path to the entity. + /// + string Path { get; } + + /// + /// Gets the sort order of the entity. + /// + int SortOrder { get; } + + /// + /// Gets the content type id + /// + int ContentTypeId { get; } + + /// + /// Gets the identifier of the writer. + /// + int WriterId { get; } + + /// + /// Gets the version identifier. + /// + int VersionId { get; } + } +} diff --git a/src/Umbraco.Core/Models/ReadOnlyContentBaseAdapter.cs b/src/Umbraco.Core/Models/ReadOnlyContentBaseAdapter.cs new file mode 100644 index 0000000000..f707d2ab1c --- /dev/null +++ b/src/Umbraco.Core/Models/ReadOnlyContentBaseAdapter.cs @@ -0,0 +1,42 @@ +using System; + +namespace Umbraco.Core.Models +{ + internal struct ReadOnlyContentBaseAdapter : IReadOnlyContentBase + { + private readonly IContentBase _content; + + private ReadOnlyContentBaseAdapter(IContentBase content) + { + _content = content ?? throw new ArgumentNullException(nameof(content)); + } + + public static ReadOnlyContentBaseAdapter Create(IContentBase content) => new ReadOnlyContentBaseAdapter(content); + + public int Id => _content.Id; + + public Guid Key => _content.Key; + + public DateTime CreateDate => _content.CreateDate; + + public DateTime UpdateDate => _content.UpdateDate; + + public string Name => _content.Name; + + public int CreatorId => _content.CreatorId; + + public int ParentId => _content.ParentId; + + public int Level => _content.Level; + + public string Path => _content.Path; + + public int SortOrder => _content.SortOrder; + + public int ContentTypeId => _content.ContentTypeId; + + public int WriterId => _content.WriterId; + + public int VersionId => _content.VersionId; + } +} diff --git a/src/Umbraco.Core/PropertyEditors/CompressedStorageAttribute.cs b/src/Umbraco.Core/PropertyEditors/CompressedStorageAttribute.cs deleted file mode 100644 index 31689c4ee9..0000000000 --- a/src/Umbraco.Core/PropertyEditors/CompressedStorageAttribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -namespace Umbraco.Core.PropertyEditors -{ - /// - /// When assigned to a DataEditor it indicates that the values it generates can be compressed - /// - /// - /// Used in conjunction with - /// - [AttributeUsage(AttributeTargets.Class)] - public sealed class CompressedStorageAttribute : Attribute - { - public CompressedStorageAttribute(bool isCompressed = true) - { - IsCompressed = isCompressed; - } - - public bool IsCompressed { get; } - } -} diff --git a/src/Umbraco.Core/PropertyEditors/CompressedStoragePropertyEditorCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/CompressedStoragePropertyEditorCompressionOptions.cs deleted file mode 100644 index a99452a5b1..0000000000 --- a/src/Umbraco.Core/PropertyEditors/CompressedStoragePropertyEditorCompressionOptions.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core.Models; - -namespace Umbraco.Core.PropertyEditors -{ - - /// - /// Ensures all property types that have a property editor attributed with use data compression - /// - internal class CompressedStoragePropertyEditorCompressionOptions : IPropertyCompressionOptions - { - private readonly IReadOnlyDictionary _contentTypes; - private readonly PropertyEditorCollection _propertyEditors; - private readonly ConcurrentDictionary<(int, string), CompressedStorageAttribute> _compressedStoragePropertyEditorCache; - - public CompressedStoragePropertyEditorCompressionOptions( - IReadOnlyDictionary contentTypes, - PropertyEditorCollection propertyEditors, - ConcurrentDictionary<(int, string), CompressedStorageAttribute> compressedStoragePropertyEditorCache) - { - _contentTypes = contentTypes ?? throw new System.ArgumentNullException(nameof(contentTypes)); - _propertyEditors = propertyEditors ?? throw new System.ArgumentNullException(nameof(propertyEditors)); - _compressedStoragePropertyEditorCache = compressedStoragePropertyEditorCache; - } - - public bool IsCompressed(int contentTypeId, string alias) - { - var compressedStorage = _compressedStoragePropertyEditorCache.GetOrAdd((contentTypeId, alias), x => - { - if (!_contentTypes.TryGetValue(contentTypeId, out var ct)) - return null; - - var propertyType = ct.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == alias); - if (propertyType == null) return null; - - if (!_propertyEditors.TryGet(propertyType.PropertyEditorAlias, out var propertyEditor)) return null; - - var attribute = propertyEditor.GetType().GetCustomAttribute(true); - return attribute; - }); - - return compressedStorage?.IsCompressed ?? false; - } - } -} diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs similarity index 50% rename from src/Umbraco.Core/PropertyEditors/IPropertyCompressionOptions.cs rename to src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs index d1add38f19..96a559630b 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyCompressionOptions.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs @@ -3,10 +3,13 @@ namespace Umbraco.Core.PropertyEditors { /// - /// Determines if a property type's value should be compressed + /// Determines if a property type's value should be compressed in memory /// - public interface IPropertyCompressionOptions + /// + /// + /// + public interface IPropertyCacheCompression { - bool IsCompressed(int contentTypeId, string propertyTypeAlias); + bool IsCompressed(IReadOnlyContentBase content, string propertyTypeAlias); } } diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs new file mode 100644 index 0000000000..2fa0153f9e --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs @@ -0,0 +1,9 @@ +using Umbraco.Core.Models; + +namespace Umbraco.Core.PropertyEditors +{ + public interface IPropertyCacheCompressionOptions + { + bool IsCompressed(IReadOnlyContentBase content, PropertyType propertyType, IDataEditor dataEditor); + } +} diff --git a/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs new file mode 100644 index 0000000000..1f12d45769 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs @@ -0,0 +1,12 @@ +using Umbraco.Core.Models; + +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Default implementation for which does not compress any property data + /// + internal class NoopPropertyCacheCompressionOptions : IPropertyCacheCompressionOptions + { + public bool IsCompressed(IReadOnlyContentBase content, PropertyType propertyType, IDataEditor dataEditor) => false; + } +} diff --git a/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs b/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs new file mode 100644 index 0000000000..6be21fca7f --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs @@ -0,0 +1,49 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Models; + +namespace Umbraco.Core.PropertyEditors +{ + + /// + /// Compresses property data based on config + /// + internal class PropertyCacheCompression : IPropertyCacheCompression + { + private readonly IPropertyCacheCompressionOptions _compressionOptions; + private readonly IReadOnlyDictionary _contentTypes; + private readonly PropertyEditorCollection _propertyEditors; + private readonly ConcurrentDictionary<(int contentTypeId, string propertyAlias), bool> _isCompressedCache; + + public PropertyCacheCompression( + IPropertyCacheCompressionOptions compressionOptions, + IReadOnlyDictionary contentTypes, + PropertyEditorCollection propertyEditors, + ConcurrentDictionary<(int, string), bool> compressedStoragePropertyEditorCache) + { + _compressionOptions = compressionOptions; + _contentTypes = contentTypes ?? throw new System.ArgumentNullException(nameof(contentTypes)); + _propertyEditors = propertyEditors ?? throw new System.ArgumentNullException(nameof(propertyEditors)); + _isCompressedCache = compressedStoragePropertyEditorCache; + } + + public bool IsCompressed(IReadOnlyContentBase content, string alias) + { + var compressedStorage = _isCompressedCache.GetOrAdd((content.ContentTypeId, alias), x => + { + if (!_contentTypes.TryGetValue(x.contentTypeId, out var ct)) + return false; + + var propertyType = ct.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == alias); + if (propertyType == null) return false; + + if (!_propertyEditors.TryGet(propertyType.PropertyEditorAlias, out var propertyEditor)) return false; + + return _compressionOptions.IsCompressed(content, propertyType, propertyEditor); + }); + + return compressedStorage; + } + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 6c473235a7..a146d5cc4a 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -152,14 +152,17 @@ + + - - - + + + + diff --git a/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs b/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs index 4be80083b8..b3543dad1a 100644 --- a/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs +++ b/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs @@ -2,6 +2,7 @@ using NUnit.Framework; using System; using System.Collections.Generic; +using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Web.PublishedCache.NuCache.DataSource; @@ -11,13 +12,13 @@ namespace Umbraco.Tests.PublishedContent public class ContentSerializationTests { [Test] - public void Ensure_Same_Results() + public void GivenACacheModel_WhenItsSerializedAndDeserializedWithAnySerializer_TheResultsAreTheSame() { var jsonSerializer = new JsonContentNestedDataSerializer(); - var msgPackSerializer = new MsgPackContentNestedDataSerializer(Mock.Of()); + var msgPackSerializer = new MsgPackContentNestedDataSerializer(Mock.Of()); var now = DateTime.Now; - var content = new ContentCacheDataModel + var cacheModel = new ContentCacheDataModel { PropertyData = new Dictionary { @@ -53,14 +54,16 @@ namespace Umbraco.Tests.PublishedContent UrlSegment = "home" }; - var json = jsonSerializer.Serialize(1, content).StringData; - var msgPack = msgPackSerializer.Serialize(1, content).ByteData; + var content = Mock.Of(x => x.ContentTypeId == 1); + + var json = jsonSerializer.Serialize(content, cacheModel).StringData; + var msgPack = msgPackSerializer.Serialize(content, cacheModel).ByteData; Console.WriteLine(json); Console.WriteLine(msgPackSerializer.ToJson(msgPack)); - var jsonContent = jsonSerializer.Deserialize(1, json, null); - var msgPackContent = msgPackSerializer.Deserialize(1, null, msgPack); + var jsonContent = jsonSerializer.Deserialize(content, json, null); + var msgPackContent = msgPackSerializer.Deserialize(content, null, msgPack); CollectionAssert.AreEqual(jsonContent.CultureData.Keys, msgPackContent.CultureData.Keys); diff --git a/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs index 2c1221e99e..42023382f1 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs @@ -13,7 +13,6 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a block list property editor. /// - [CompressedStorage] [DataEditor( Constants.PropertyEditors.Aliases.BlockList, "Block List", diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index 7ce312c516..862837381a 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -17,7 +17,6 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a grid property and parameter editor. /// - [CompressedStorage] [DataEditor( Constants.PropertyEditors.Aliases.Grid, "Grid layout", diff --git a/src/Umbraco.Web/PropertyEditors/MarkdownPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MarkdownPropertyEditor.cs index 6ce66aaa00..2d66da5461 100644 --- a/src/Umbraco.Web/PropertyEditors/MarkdownPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MarkdownPropertyEditor.cs @@ -7,7 +7,6 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a markdown editor. /// - [CompressedStorage] [DataEditor( Constants.PropertyEditors.Aliases.MarkdownEditor, "Markdown editor", diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 0e1cea3399..ca3c5a2a04 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -22,7 +22,6 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a nested content property editor. /// - [CompressedStorage] [DataEditor( Constants.PropertyEditors.Aliases.NestedContent, "Nested Content", diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 7c7a358bf3..42777f11ad 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -17,7 +17,6 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a rich text property editor. /// - [CompressedStorage] [DataEditor( Constants.PropertyEditors.Aliases.TinyMce, "Rich Text Editor", diff --git a/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs index 878330820a..c7bc2efbda 100644 --- a/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs @@ -7,7 +7,6 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a textarea property and parameter editor. /// - [CompressedStorage] [DataEditor( Constants.PropertyEditors.Aliases.TextArea, EditorType.PropertyValue | EditorType.MacroParameter, diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentSourceDto.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentSourceDto.cs index be2f9921d1..343885b037 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentSourceDto.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentSourceDto.cs @@ -1,12 +1,13 @@ using System; +using Umbraco.Core.Models; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { // read-only dto - internal class ContentSourceDto + internal class ContentSourceDto : IReadOnlyContentBase { public int Id { get; set; } - public Guid Uid { get; set; } + public Guid Key { get; set; } public int ContentTypeId { get; set; } public int Level { get; set; } @@ -37,5 +38,10 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public int PubTemplateId { get; set; } public string PubData { get; set; } public byte[] PubDataRaw { get; set; } + + // Explicit implementation + DateTime IReadOnlyContentBase.UpdateDate => EditVersionDate; + string IReadOnlyContentBase.Name => EditName; + int IReadOnlyContentBase.WriterId => EditWriterId; } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index 3bd30d2d73..4584344891 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -383,7 +383,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } else { - var deserializedContent = serializer.Deserialize(dto.ContentTypeId, dto.EditData, dto.EditDataRaw); + var deserializedContent = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw); d = new ContentData { @@ -410,7 +410,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } else { - var deserializedContent = serializer.Deserialize(dto.ContentTypeId, dto.PubData, dto.PubDataRaw); + var deserializedContent = serializer.Deserialize(dto, dto.PubData, dto.PubDataRaw); p = new ContentData { @@ -427,7 +427,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } } - var n = new ContentNode(dto.Id, dto.Uid, + var n = new ContentNode(dto.Id, dto.Key, dto.Level, dto.Path, dto.SortOrder, dto.ParentId, dto.CreateDate, dto.CreatorId); var s = new ContentNodeKit @@ -446,7 +446,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource if (dto.EditData == null && dto.EditDataRaw == null) throw new InvalidOperationException("No data for media " + dto.Id); - var deserializedMedia = serializer.Deserialize(dto.ContentTypeId, dto.EditData, dto.EditDataRaw); + var deserializedMedia = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw); var p = new ContentData { @@ -460,7 +460,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource CultureInfos = deserializedMedia.CultureData }; - var n = new ContentNode(dto.Id, dto.Uid, + var n = new ContentNode(dto.Id, dto.Key, dto.Level, dto.Path, dto.SortOrder, dto.ParentId, dto.CreateDate, dto.CreatorId); var s = new ContentNodeKit diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs index f628c8981b..d1a83d8452 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs @@ -1,4 +1,4 @@ -using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Models; namespace Umbraco.Web.PublishedCache.NuCache.DataSource { @@ -14,19 +14,12 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// /// Deserialize the data into a /// - /// - /// - /// - /// - ContentCacheDataModel Deserialize(int contentTypeId, string stringData, byte[] byteData); + ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData); /// /// Serializes the /// - /// - /// - /// - ContentCacheDataSerializationResult Serialize(int contentTypeId, ContentCacheDataModel model); + ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs index e7bb5656bb..c4d40f721f 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; +using Umbraco.Core.Models; using Umbraco.Core.Serialization; namespace Umbraco.Web.PublishedCache.NuCache.DataSource @@ -21,7 +22,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource DateFormatString = "o" }; - public ContentCacheDataModel Deserialize(int contentTypeId, string stringData, byte[] byteData) + public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData) { if (stringData == null && byteData != null) throw new NotSupportedException($"{typeof(JsonContentNestedDataSerializer)} does not support byte[] serialization"); @@ -29,7 +30,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return JsonConvert.DeserializeObject(stringData, _jsonSerializerSettings); } - public ContentCacheDataSerializationResult Serialize(int contentTypeId, ContentCacheDataModel model) + public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model) { // note that numeric values (which are Int32) are serialized without their // type (eg "value":1234) and JsonConvert by default deserializes them as Int64 diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index 6ffa130da0..944d93107d 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -4,6 +4,7 @@ using MessagePack.Resolvers; using System; using System.Linq; using System.Text; +using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PublishedCache.NuCache.DataSource @@ -15,9 +16,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public class MsgPackContentNestedDataSerializer : IContentCacheDataSerializer { private readonly MessagePackSerializerOptions _options; - private readonly IPropertyCompressionOptions _propertyOptions; + private readonly IPropertyCacheCompression _propertyOptions; - public MsgPackContentNestedDataSerializer(IPropertyCompressionOptions propertyOptions) + public MsgPackContentNestedDataSerializer(IPropertyCacheCompression propertyOptions) { _propertyOptions = propertyOptions ?? throw new ArgumentNullException(nameof(propertyOptions)); @@ -46,21 +47,21 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return json; } - public ContentCacheDataModel Deserialize(int contentTypeId, string stringData, byte[] byteData) + public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData) { if (byteData != null) { - var content = MessagePackSerializer.Deserialize(byteData, _options); - Expand(contentTypeId, content); - return content; + var cacheModel = MessagePackSerializer.Deserialize(byteData, _options); + Expand(content, cacheModel); + return cacheModel; } else if (stringData != null) { // NOTE: We don't really support strings but it's possible if manually used (i.e. tests) var bin = Convert.FromBase64String(stringData); - var content = MessagePackSerializer.Deserialize(bin, _options); - Expand(contentTypeId, content); - return content; + var cacheModel = MessagePackSerializer.Deserialize(bin, _options); + Expand(content, cacheModel); + return cacheModel; } else { @@ -68,9 +69,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } } - public ContentCacheDataSerializationResult Serialize(int contentTypeId, ContentCacheDataModel model) + public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model) { - Compress(contentTypeId, model); + Compress(content, model); var bytes = MessagePackSerializer.Serialize(model, _options); return new ContentCacheDataSerializationResult(null, bytes); } @@ -86,11 +87,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// read/decompressed as a string to be displayed on the front-end. This allows for potentially a significant /// memory savings but could also affect performance of first rendering pages while decompression occurs. /// - private void Compress(int contentTypeId, ContentCacheDataModel model) + private void Compress(IReadOnlyContentBase content, ContentCacheDataModel model) { foreach(var propertyAliasToData in model.PropertyData) { - if (_propertyOptions.IsCompressed(contentTypeId, propertyAliasToData.Key)) + if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key)) { foreach(var property in propertyAliasToData.Value.Where(x => x.Value != null && x.Value is string)) { @@ -104,11 +105,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// Used during deserialization to map the property data as lazy or expand the value /// /// - private void Expand(int contentTypeId, ContentCacheDataModel nestedData) + private void Expand(IReadOnlyContentBase content, ContentCacheDataModel nestedData) { foreach (var propertyAliasToData in nestedData.PropertyData) { - if (_propertyOptions.IsCompressed(contentTypeId, propertyAliasToData.Key)) + if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key)) { foreach (var property in propertyAliasToData.Value.Where(x => x.Value != null)) { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs index b509334604..fcc3fa2bb8 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs @@ -12,14 +12,21 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource private readonly IMediaTypeService _mediaTypeService; private readonly IMemberTypeService _memberTypeService; private readonly PropertyEditorCollection _propertyEditors; - private readonly ConcurrentDictionary<(int, string), CompressedStorageAttribute> _compressedStoragePropertyEditorCache = new ConcurrentDictionary<(int, string), CompressedStorageAttribute>(); + private readonly IPropertyCacheCompressionOptions _compressionOptions; + private readonly ConcurrentDictionary<(int, string), bool> _isCompressedCache = new ConcurrentDictionary<(int, string), bool>(); - public MsgPackContentNestedDataSerializerFactory(IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService, PropertyEditorCollection propertyEditors) + public MsgPackContentNestedDataSerializerFactory( + IContentTypeService contentTypeService, + IMediaTypeService mediaTypeService, + IMemberTypeService memberTypeService, + PropertyEditorCollection propertyEditors, + IPropertyCacheCompressionOptions compressionOptions) { _contentTypeService = contentTypeService; _mediaTypeService = mediaTypeService; _memberTypeService = memberTypeService; _propertyEditors = propertyEditors; + _compressionOptions = compressionOptions; } public IContentCacheDataSerializer Create(ContentCacheDataSerializerEntityType types) @@ -53,8 +60,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } } - var options = new CompressedStoragePropertyEditorCompressionOptions(contentTypes, _propertyEditors, _compressedStoragePropertyEditorCache); - var serializer = new MsgPackContentNestedDataSerializer(options); + var compression = new PropertyCacheCompression(_compressionOptions, contentTypes, _propertyEditors, _isCompressedCache); + var serializer = new MsgPackContentNestedDataSerializer(compression); return serializer; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs index faeb4f90b4..98d8b91386 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs @@ -20,9 +20,11 @@ namespace Umbraco.Web.PublishedCache.NuCache } else { - composition.RegisterUnique(); + composition.RegisterUnique(); } - + + composition.RegisterUnique(); + composition.RegisterUnique(factory => new ContentDataSerializer(new DictionaryOfPropertyDataSerializer())); // register the NuCache database data source diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index ad8705ef47..dbbcb4ee9b 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1456,7 +1456,7 @@ namespace Umbraco.Web.PublishedCache.NuCache UrlSegment = content.GetUrlSegment(_urlSegmentProviders) }; - var serialized = serializer.Serialize(content.ContentTypeId, contentCacheData); + var serialized = serializer.Serialize(ReadOnlyContentBaseAdapter.Create(content), contentCacheData); var dto = new ContentNuDto { From 5a80b30289ff25b5640145331b7ca0659220c4da Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 28 Jan 2021 14:28:09 +1100 Subject: [PATCH 070/289] Fixes SQL (oops), wasn't appending the joins --- .../NuCache/DataSource/DatabaseDataSource.cs | 116 ++++++++++-------- 1 file changed, 63 insertions(+), 53 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index 4584344891..0ca882b014 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -28,7 +28,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // we want arrays, we want them all loaded, not an enumerable - private Sql SqlContentSourcesSelect(IScope scope, Func, Sql> joins = null) + private Sql SqlContentSourcesSelect(IScope scope, Func> joins = null) { var sqlTemplate = scope.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.ContentSourcesSelect, tsql => tsql.Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Uid"), @@ -56,7 +56,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // TODO: I'm unsure how we can format the below into SQL templates also because right.Current and right.Published end up being parameters if (joins != null) - sql = joins(sql); + sql = sql.Append(joins(sql.SqlContext)); sql = sql .InnerJoin().On((left, right) => left.NodeId == right.NodeId) @@ -75,48 +75,58 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return sql; } - private Sql SqlContentSourcesSelectUmbracoNodeJoin(Sql s) + private Sql SqlContentSourcesSelectUmbracoNodeJoin(ISqlContext sqlContext) { - var syntax = s.SqlContext.SqlSyntax; - var sqlTemplate = s.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.SourcesSelectUmbracoNodeJoin, s => - s.InnerJoin("x") + var syntax = sqlContext.SqlSyntax; + + var sqlTemplate = sqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.SourcesSelectUmbracoNodeJoin, builder => + builder.InnerJoin("x") .On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")); + var sql = sqlTemplate.Sql(); return sql; } - private Sql SqlWhereNodeId(Sql s, int id) + private Sql SqlWhereNodeId(ISqlContext sqlContext, int id) { - var syntax = s.SqlContext.SqlSyntax; - var sqlTemplate = s.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.WhereNodeId, s => - s.Where(x => x.NodeId == SqlTemplate.Arg("id"))); + var syntax = sqlContext.SqlSyntax; + + var sqlTemplate = sqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.WhereNodeId, builder => + builder.Where(x => x.NodeId == SqlTemplate.Arg("id"))); + var sql = sqlTemplate.Sql(id); return sql; } - private Sql SqlWhereNodeIdX(Sql s, int id) + private Sql SqlWhereNodeIdX(ISqlContext sqlContext, int id) { - var syntax = s.SqlContext.SqlSyntax; - var sqlTemplate = s.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.WhereNodeIdX, s => + var syntax = sqlContext.SqlSyntax; + + var sqlTemplate = sqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.WhereNodeIdX, s => s.Where(x => x.NodeId == SqlTemplate.Arg("id"), "x")); + var sql = sqlTemplate.Sql(id); return sql; } - private Sql SqlOrderByLevelIdSortOrder(Sql s) + private Sql SqlOrderByLevelIdSortOrder(ISqlContext sqlContext) { - var syntax = s.SqlContext.SqlSyntax; - var sqlTemplate = s.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.OrderByLevelIdSortOrder, s => + var syntax = sqlContext.SqlSyntax; + + var sqlTemplate = sqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.OrderByLevelIdSortOrder, s => s.OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder)); + var sql = sqlTemplate.Sql(); return sql; } - private Sql SqlObjectTypeNotTrashed(Sql s, Guid nodeObjectType) + private Sql SqlObjectTypeNotTrashed(ISqlContext sqlContext, Guid nodeObjectType) { - var syntax = s.SqlContext.SqlSyntax; - var sqlTemplate = s.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.ObjectTypeNotTrashedFilter, s => + var syntax = sqlContext.SqlSyntax; + + var sqlTemplate = sqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.ObjectTypeNotTrashedFilter, s => s.Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.Trashed == SqlTemplate.Arg("trashed"))); + var sql = sqlTemplate.Sql(nodeObjectType, false); return sql; } @@ -126,7 +136,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// /// /// - private Sql SqlContentSourcesCount(IScope scope, Func, Sql> joins = null) + private Sql SqlContentSourcesCount(IScope scope, Func> joins = null) { var sqlTemplate = scope.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.ContentSourcesCount, tsql => tsql.Select(x => Alias(x.NodeId, "Id")) @@ -137,7 +147,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource var sql = sqlTemplate.Sql(); if (joins != null) - sql = joins(sql); + sql = sql.Append(joins(sql.SqlContext)); // TODO: We can't use a template with this one because of the 'right.Current' and 'right.Published' ends up being a parameter so not sure how we can do that sql = sql @@ -150,7 +160,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return sql; } - private Sql SqlMediaSourcesSelect(IScope scope, Func, Sql> joins = null) + private Sql SqlMediaSourcesSelect(IScope scope, Func> joins = null) { var sqlTemplate = scope.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.MediaSourcesSelect, tsql => tsql.Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Uid"), @@ -165,7 +175,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource var sql = sqlTemplate.Sql(); if (joins != null) - sql = joins(sql); + sql = sql.Append(joins(sql.SqlContext)); // TODO: We can't use a template with this one because of the 'right.Published' ends up being a parameter so not sure how we can do that sql = sql @@ -175,7 +185,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return sql; } - private Sql SqlMediaSourcesCount(IScope scope, Func, Sql> joins = null) + private Sql SqlMediaSourcesCount(IScope scope, Func> joins = null) { var sqlTemplate = scope.SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.MediaSourcesCount, tsql => tsql.Select(x => Alias(x.NodeId, "Id")).From()); @@ -183,7 +193,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource var sql = sqlTemplate.Sql(); if (joins != null) - sql = joins(sql); + sql = sql.Append(joins(sql.SqlContext)); // TODO: We can't use a template with this one because of the 'right.Current' ends up being a parameter so not sure how we can do that sql = sql @@ -196,9 +206,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public ContentNodeKit GetContentSource(IScope scope, int id) { var sql = SqlContentSourcesSelect(scope) - .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Document)) - .Append(SqlWhereNodeId(scope.SqlContext.Sql(), id)) - .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext.Sql())); + .Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Document)) + .Append(SqlWhereNodeId(scope.SqlContext, id)) + .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext)); var dto = scope.Database.Fetch(sql).FirstOrDefault(); @@ -211,12 +221,12 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public IEnumerable GetAllContentSources(IScope scope) { var sql = SqlContentSourcesSelect(scope) - .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Document)) - .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext.Sql())); + .Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Document)) + .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext)); // Use a more efficient COUNT query var sqlCountQuery = SqlContentSourcesCount(scope) - .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Document)); + .Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Document)); var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); @@ -234,14 +244,14 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public IEnumerable GetBranchContentSources(IScope scope, int id) { var sql = SqlContentSourcesSelect(scope, SqlContentSourcesSelectUmbracoNodeJoin) - .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Document)) - .Append(SqlWhereNodeIdX(scope.SqlContext.Sql(), id)) - .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext.Sql())); + .Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Document)) + .Append(SqlWhereNodeIdX(scope.SqlContext, id)) + .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext)); // Use a more efficient COUNT query var sqlCountQuery = SqlContentSourcesCount(scope, SqlContentSourcesSelectUmbracoNodeJoin) - .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Document)) - .Append(SqlWhereNodeIdX(scope.SqlContext.Sql(), id)); + .Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Document)) + .Append(SqlWhereNodeIdX(scope.SqlContext, id)); var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); @@ -260,13 +270,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource if (!ids.Any()) yield break; var sql = SqlContentSourcesSelect(scope) - .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Document)) + .Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Document)) .WhereIn(x => x.ContentTypeId, ids) - .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext.Sql())); + .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext)); // Use a more efficient COUNT query var sqlCountQuery = SqlContentSourcesCount(scope) - .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Document)) + .Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Document)) .WhereIn(x => x.ContentTypeId, ids); var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); @@ -284,9 +294,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public ContentNodeKit GetMediaSource(IScope scope, int id) { var sql = SqlMediaSourcesSelect(scope) - .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Media)) - .Append(SqlWhereNodeId(scope.SqlContext.Sql(), id)) - .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext.Sql())); + .Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Media)) + .Append(SqlWhereNodeId(scope.SqlContext, id)) + .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext)); var dto = scope.Database.Fetch(sql).FirstOrDefault(); @@ -299,12 +309,12 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public IEnumerable GetAllMediaSources(IScope scope) { var sql = SqlMediaSourcesSelect(scope) - .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Media)) - .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext.Sql())); + .Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Media)) + .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext)); // Use a more efficient COUNT query var sqlCountQuery = SqlMediaSourcesCount(scope) - .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Media)); + .Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Media)); var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); @@ -321,14 +331,14 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource public IEnumerable GetBranchMediaSources(IScope scope, int id) { var sql = SqlMediaSourcesSelect(scope, SqlContentSourcesSelectUmbracoNodeJoin) - .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Media)) - .Append(SqlWhereNodeIdX(scope.SqlContext.Sql(), id)) - .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext.Sql())); + .Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Media)) + .Append(SqlWhereNodeIdX(scope.SqlContext, id)) + .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext)); // Use a more efficient COUNT query var sqlCountQuery = SqlMediaSourcesCount(scope, SqlContentSourcesSelectUmbracoNodeJoin) - .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Media)) - .Append(SqlWhereNodeIdX(scope.SqlContext.Sql(), id)); + .Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Media)) + .Append(SqlWhereNodeIdX(scope.SqlContext, id)); var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); @@ -347,13 +357,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource if (!ids.Any()) yield break; var sql = SqlMediaSourcesSelect(scope) - .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Media)) + .Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Media)) .WhereIn(x => x.ContentTypeId, ids) - .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext.Sql())); + .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext)); // Use a more efficient COUNT query var sqlCountQuery = SqlMediaSourcesCount(scope) - .Append(SqlObjectTypeNotTrashed(scope.SqlContext.Sql(), Constants.ObjectTypes.Media)) + .Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Media)) .WhereIn(x => x.ContentTypeId, ids); var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); From 91830e55533a8f09e92366aec612ae960f504fb9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 28 Jan 2021 14:42:02 +1100 Subject: [PATCH 071/289] Adds error checking --- .../PublishedCache/NuCache/PublishedContent.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs index ac5719c015..24747c43ad 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs @@ -298,7 +298,18 @@ namespace Umbraco.Web.PublishedCache.NuCache throw new PanicException($"failed to get content with id={id}"); } - id = UnwrapIPublishedContent(content)._contentNode.NextSiblingContentId; + var next = UnwrapIPublishedContent(content)._contentNode.NextSiblingContentId; + +#if DEBUG + // I've seen this happen but I think that may have been due to corrupt DB data due to my own + // bugs, but I'm leaving this here just in case we encounter it again while we're debugging. + if (next == id) + { + throw new PanicException($"The current content id {id} is the same as it's next sibling id {next}"); + } +#endif + + id = next; } } } From 3185a285d93a2835044b0cdc7b95301dd2929fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 1 Feb 2021 11:56:51 +0100 Subject: [PATCH 072/289] Correcting merge --- .../common/directives/components/content/edit.controller.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index c3e23e4cfd..fb70143fa5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -481,13 +481,14 @@ syncTreeNode($scope.content, $scope.content.path); + if($scope.contentForm.$invalid !== true) { + resetNestedFieldValiation(fieldsToRollback); + } if (err.status === 400 && err.data) { // content was saved but is invalid. eventsService.emit("content.saved", { content: $scope.content, action: args.action, valid: false }); } - resetNestedFieldValiation(fieldsToRollback); - return $q.reject(err); }); } From 3dd84fa6ff9232892b747a1371a9d3f7fe0c32d0 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Sat, 6 Feb 2021 21:57:48 +1300 Subject: [PATCH 073/289] bugfix --- src/Umbraco.Core/Services/Implement/LocalizedTextService.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs b/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs index 246481a48b..c10ae59907 100644 --- a/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs +++ b/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs @@ -265,6 +265,10 @@ namespace Umbraco.Core.Services.Implement { areaDictionary.TryGetValue(key, out found); } + if(found == null) + { + _noAreaDictionarySource[culture].TryGetValue(key, out found); + } } From e7baa0878878bded2d92a6e48f535e91331244e6 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Sat, 6 Feb 2021 23:17:15 +1300 Subject: [PATCH 074/289] Improve getAllStoredValues. --- .../Services/ILocalizedTextService.cs | 7 +++++++ .../Implement/LocalizedTextService.cs | 21 ++++++++++++++++--- .../Editors/BackOfficeController.cs | 7 +++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Services/ILocalizedTextService.cs b/src/Umbraco.Core/Services/ILocalizedTextService.cs index a4e5141a42..73212546a5 100644 --- a/src/Umbraco.Core/Services/ILocalizedTextService.cs +++ b/src/Umbraco.Core/Services/ILocalizedTextService.cs @@ -16,6 +16,13 @@ namespace Umbraco.Core.Services /// This can be null /// string Localize(string area, string alias, CultureInfo culture, IDictionary tokens = null); + + + /// + /// Returns all key/values in storage for the given culture + /// + /// + IDictionary> GetAllStoredValuesByAreaAndAlias(CultureInfo culture); } /// diff --git a/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs b/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs index c10ae59907..79c568e374 100644 --- a/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs +++ b/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs @@ -145,15 +145,15 @@ namespace Umbraco.Core.Services.Implement // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode culture = ConvertToSupportedCultureWithRegionCode(culture); - var result = new Dictionary(); + if (_dictionarySource.ContainsKey(culture) == false) { _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture); - return result; + return new Dictionary(0); } - + IDictionary result = new Dictionary(); //convert all areas + keys to a single key with a '/' foreach (var area in _dictionarySource[culture]) { @@ -309,5 +309,20 @@ namespace Umbraco.Core.Services.Implement return value; } + public IDictionary> GetAllStoredValuesByAreaAndAlias(CultureInfo culture) + { + if (culture == null) throw new ArgumentNullException("culture"); + + // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode + culture = ConvertToSupportedCultureWithRegionCode(culture); + + if (_dictionarySource.ContainsKey(culture) == false) + { + _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture); + return new Dictionary>(0); + } + + return _dictionarySource[culture]; + } } } diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 46bd52978d..5da0cd0cc3 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -207,6 +207,13 @@ namespace Umbraco.Web.Editors : CultureInfo.GetCultureInfo(GlobalSettings.DefaultUILanguage) : CultureInfo.GetCultureInfo(culture); + + if(Services.TextService is ILocalizedTextService2 localizedText2) + { + var nestedDictionary2 = localizedText2.GetAllStoredValuesByAreaAndAlias(cultureInfo); + return new JsonNetResult { Data = nestedDictionary2, Formatting = Formatting.None }; + } + var allValues = Services.TextService.GetAllStoredValues(cultureInfo); var pathedValues = allValues.Select(kv => { From 3202fe445be2a7c8003c6d3a259d08151a49f94b Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Sat, 6 Feb 2021 23:26:13 +1300 Subject: [PATCH 075/289] Benchmark --- ...LocalizedTextServiceGetAllStoredValuesBenchmarks.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs index be45ffbd8c..f16df01ef4 100644 --- a/src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs +++ b/src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs @@ -41,6 +41,7 @@ namespace Umbraco.Tests.Benchmarks var result4 = _xmlService.GetAllStoredValues(culture); var result5 = _optimized.GetAllStoredValues(culture); var result6 = _xmlService.GetAllStoredValues(culture); + var result7 = _optimized.GetAllStoredValuesByAreaAndAlias(culture); } [Benchmark] @@ -100,6 +101,15 @@ namespace Umbraco.Tests.Benchmarks } } + [Benchmark] + public void OptimizedDictGetAllV2() + { + for (int i = 0; i < 10000; i++) + { + var result = _optimizedDict.GetAllStoredValuesByAreaAndAlias(culture); + } + } + [Benchmark()] public void OptimizedXmlLocalize() { From 094029134d8723cfe7e3996bc82f7f537dd44dd6 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Sun, 7 Feb 2021 00:06:32 +1300 Subject: [PATCH 076/289] Bugfix app key --- src/Umbraco.Web/Models/Mapping/CommonMapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Models/Mapping/CommonMapper.cs b/src/Umbraco.Web/Models/Mapping/CommonMapper.cs index d04579d5ee..2440ecc44c 100644 --- a/src/Umbraco.Web/Models/Mapping/CommonMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/CommonMapper.cs @@ -80,7 +80,7 @@ namespace Umbraco.Web.Models.Mapping // localize content app names foreach (var app in apps) { - var localizedAppName = _localizedTextService.Localize($"apps", "{app.Alias}"); + var localizedAppName = _localizedTextService.Localize("apps", app.Alias); if (localizedAppName.Equals($"[{app.Alias}]", StringComparison.OrdinalIgnoreCase) == false) { app.Name = localizedAppName; From 603b38bd6c3b1b7903c0c714d0a79fde9306402d Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Wed, 10 Feb 2021 12:47:35 +1300 Subject: [PATCH 077/289] implement lazy --- .../Implement/LocalizedTextService.cs | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs b/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs index 79c568e374..f5a85dda7e 100644 --- a/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs +++ b/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs @@ -15,8 +15,10 @@ namespace Umbraco.Core.Services.Implement { private readonly ILogger _logger; private readonly Lazy _fileSources; - private readonly IDictionary>> _dictionarySource; - private readonly IDictionary> _noAreaDictionarySource; + private IDictionary>> _dictionarySource => _dictionarySourceLazy.Value; + private IDictionary> _noAreaDictionarySource => _noAreaDictionarySourceLazy.Value; + private readonly Lazy>>> _dictionarySourceLazy; + private readonly Lazy>> _noAreaDictionarySourceLazy; private readonly char[] _splitter = new[] { '/' }; /// /// Initializes with a file sources instance @@ -28,9 +30,8 @@ namespace Umbraco.Core.Services.Implement if (logger == null) throw new ArgumentNullException("logger"); _logger = logger; if (fileSources == null) throw new ArgumentNullException("fileSources"); - var dictionaries = FileSourcesToDictionarySources(fileSources.Value); - _dictionarySource = dictionaries.WithArea; - _noAreaDictionarySource = dictionaries.WithoutArea; + _dictionarySourceLazy = new Lazy>>>(()=> FileSourcesToDictionarySources(fileSources.Value).WithArea); + _noAreaDictionarySourceLazy = new Lazy>>(() => FileSourcesToDictionarySources(fileSources.Value).WithoutArea); _fileSources = fileSources; } @@ -72,9 +73,9 @@ namespace Umbraco.Core.Services.Implement if (source == null) throw new ArgumentNullException("source"); if (logger == null) throw new ArgumentNullException("logger"); _logger = logger; - var dictionaries = XmlSourcesToDictionarySources(source); - _dictionarySource = dictionaries.WithArea; - _noAreaDictionarySource = dictionaries.WithoutArea; + _dictionarySourceLazy = new Lazy>>>(()=> XmlSourcesToDictionarySources(source).WithArea); + _noAreaDictionarySourceLazy = new Lazy>>(() => XmlSourcesToDictionarySources(source).WithoutArea); + } @@ -85,10 +86,11 @@ namespace Umbraco.Core.Services.Implement /// public LocalizedTextService(IDictionary>> source, ILogger logger) { - _dictionarySource = source ?? throw new ArgumentNullException(nameof(source)); + var dictionarySource = source ?? throw new ArgumentNullException(nameof(source)); + _dictionarySourceLazy = new Lazy>>>(() => dictionarySource); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); var cultureNoAreaDictionary = new Dictionary>(); - foreach (var cultureDictionary in _dictionarySource) + foreach (var cultureDictionary in dictionarySource) { var areaAliaValue = GetAreaStoredTranslations(source, cultureDictionary.Key); var aliasValue = new Dictionary(); @@ -104,7 +106,7 @@ namespace Umbraco.Core.Services.Implement } cultureNoAreaDictionary.Add(cultureDictionary.Key, aliasValue); } - _noAreaDictionarySource = cultureNoAreaDictionary; + _noAreaDictionarySourceLazy = new Lazy>>(() => cultureNoAreaDictionary); } public string Localize(string key, CultureInfo culture, IDictionary tokens = null) From fb93e15c1046597c1675b3e5266a5b94501e739a Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Fri, 12 Feb 2021 14:50:21 +1300 Subject: [PATCH 078/289] fix key --- .../PublishedCache/NuCache/DataSource/DatabaseDataSource.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index e39f649eaf..9c6a799312 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -34,7 +34,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { var sql = scope.SqlContext.Sql() - .Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Uid"), + .Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Key"), x => Alias(x.Level, "Level"), x => Alias(x.Path, "Path"), x => Alias(x.SortOrder, "SortOrder"), x => Alias(x.ParentId, "ParentId"), x => Alias(x.CreateDate, "CreateDate"), x => Alias(x.UserId, "CreatorId")) .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) @@ -130,7 +130,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { var sql = scope.SqlContext.Sql() - .Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Uid"), + .Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Key"), x => Alias(x.Level, "Level"), x => Alias(x.Path, "Path"), x => Alias(x.SortOrder, "SortOrder"), x => Alias(x.ParentId, "ParentId"), x => Alias(x.CreateDate, "CreateDate"), x => Alias(x.UserId, "CreatorId")) .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) From 2a8d043445fe180380f9dbd1ec01c2f29dc1ce81 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Fri, 12 Feb 2021 15:01:46 +1300 Subject: [PATCH 079/289] support bulk insert for binary in sql ce. Fix column ordering in contentnudto. --- src/Umbraco.Core/Persistence/Dtos/ContentNuDto.cs | 6 ++++-- .../Persistence/NPocoDatabaseExtensions-Bulk.cs | 11 ++++++++++- .../Persistence/SqlSyntax/SqlCeSyntaxProvider.cs | 9 ++++++++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentNuDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentNuDto.cs index 664d188a10..a2f36584e0 100644 --- a/src/Umbraco.Core/Persistence/Dtos/ContentNuDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/ContentNuDto.cs @@ -28,11 +28,13 @@ namespace Umbraco.Core.Persistence.Dtos [NullSetting(NullSetting = NullSettings.Null)] public string Data { get; set; } + [Column("rv")] + public long Rv { get; set; } + [Column("dataRaw")] [NullSetting(NullSetting = NullSettings.Null)] public byte[] RawData { get; set; } - [Column("rv")] - public long Rv { get; set; } + } } diff --git a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs index 10db1ca18e..bff682d095 100644 --- a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs +++ b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Data; using System.Data.SqlClient; using System.Data.SqlServerCe; +using System.Data.SqlTypes; using System.Linq; using NPoco; using Umbraco.Core.Persistence.SqlSyntax; @@ -210,7 +211,15 @@ namespace Umbraco.Core.Persistence if (IncludeColumn(pocoData, columns[i])) { var val = columns[i].Value.GetValue(record); - updatableRecord.SetValue(i, val); + if (val is byte[]) + { + var bytes = val as byte[]; + updatableRecord.SetSqlBinary(i, new SqlBinary(bytes)); + } + else + { + updatableRecord.SetValue(i, val); + } } } resultSet.Insert(updatableRecord); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs index 0b564212ce..9c5edf595b 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs @@ -261,6 +261,13 @@ where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault() return "NTEXT"; return base.GetSpecialDbType(dbTypes); } - + public override SqlDbType GetSqlDbType(DbType dbType) + { + if (DbType.Binary == dbType) + { + return SqlDbType.Image; + } + return base.GetSqlDbType(dbType); + } } } From d425fbe05e97db92f9ea33a2e800f95e4bfa2d27 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Fri, 12 Feb 2021 15:41:46 +1300 Subject: [PATCH 080/289] rebuild nucache if serializer changes --- .../NuCache/NuCacheSerializerComponent.cs | 66 +++++++++++++++++++ .../NuCache/NuCacheSerializerComposer.cs | 21 ++++++ src/Umbraco.Web/Umbraco.Web.csproj | 2 + src/umbraco.sln | 24 ++++++- 4 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComposer.cs diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs new file mode 100644 index 0000000000..4b4ccd295a --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; +using Umbraco.Core.Services; + +namespace Umbraco.Web.PublishedCache.NuCache +{ + /// + /// Rebuilds the database cache if required when the serializer changes + /// + public class NuCacheSerializerComponent : IComponent + { + private const string Nucache_Serializer_Key = "Umbraco.Web.PublishedCache.NuCache.Serializer"; + private const string JSON_SERIALIZER_VALUE = "JSON"; + private readonly IPublishedSnapshotService _service; + private readonly IKeyValueService _keyValueService; + private readonly IProfilingLogger _profilingLogger; + + public NuCacheSerializerComponent(IPublishedSnapshotService service, IKeyValueService keyValueService,IProfilingLogger profilingLogger) + { + // service: nothing - this just ensures that the service is created at boot time + _service = service; + _keyValueService = keyValueService; + _profilingLogger = profilingLogger; + } + + public void Initialize() + { + RebuildDatabaseCacheIfSerializerChanged(); + } + + private void RebuildDatabaseCacheIfSerializerChanged() + { + var serializer = ConfigurationManager.AppSettings[Nucache_Serializer_Key]; + var currentSerializer = _keyValueService.GetValue(Nucache_Serializer_Key); + + if (currentSerializer == null) + { + currentSerializer = JSON_SERIALIZER_VALUE; + } + if (serializer == null) + { + serializer = JSON_SERIALIZER_VALUE; + } + + if (serializer != currentSerializer) + { + _profilingLogger.Info($"Database NuCache was serialized using {currentSerializer}. Currently configured NuCache serializer {serializer}. Rebuilding Nucache"); + using (_profilingLogger.TraceDuration($"Rebuilding NuCache database with {currentSerializer} serializer")) + { + + _service.Rebuild(); + _keyValueService.SetValue(Nucache_Serializer_Key, serializer); + } + } + } + + public void Terminate() + { } + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComposer.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComposer.cs new file mode 100644 index 0000000000..59a206bc47 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComposer.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.PublishedCache.NuCache +{ + + [ComposeAfter(typeof(NuCacheComposer))] + [RuntimeLevel(MinLevel = RuntimeLevel.Run)] + public class NuCacheSerializerComposer : ICoreComposer + { + public void Compose(Composition composition) + { + composition.Components().Append(); + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 088fb0eeb3..0878867162 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -251,6 +251,8 @@ + + diff --git a/src/umbraco.sln b/src/umbraco.sln index 63fb856b5d..72b06a517a 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -58,8 +58,24 @@ Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.Client", "ht StartServerOnDebug = "false" EndProjectSection EndProject -Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTest\", "Umbraco.Tests.AcceptanceTest\", "{9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}" +Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTest(1)", "Umbraco.Tests.AcceptanceTest\", "{9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}" ProjectSection(WebsiteProperties) = preProject + TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.0" + Debug.AspNetCompiler.VirtualPath = "/localhost_62926" + Debug.AspNetCompiler.PhysicalPath = "Umbraco.Tests.AcceptanceTest\" + Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_62926\" + Debug.AspNetCompiler.Updateable = "true" + Debug.AspNetCompiler.ForceOverwrite = "true" + Debug.AspNetCompiler.FixedNames = "false" + Debug.AspNetCompiler.Debug = "True" + Release.AspNetCompiler.VirtualPath = "/localhost_62926" + Release.AspNetCompiler.PhysicalPath = "Umbraco.Tests.AcceptanceTest\" + Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_62926\" + Release.AspNetCompiler.Updateable = "true" + Release.AspNetCompiler.ForceOverwrite = "true" + Release.AspNetCompiler.FixedNames = "false" + Release.AspNetCompiler.Debug = "False" + VWDPort = "62926" SlnRelativePath = "Umbraco.Tests.AcceptanceTest\" EndProjectSection EndProject @@ -123,6 +139,10 @@ Global {4C4C194C-B5E4-4991-8F87-4373E24CC19F}.Release|Any CPU.Build.0 = Release|Any CPU {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Release|Any CPU.Build.0 = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Debug|Any CPU.Build.0 = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -157,6 +177,7 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {227C3B55-80E5-4E7E-A802-BE16C5128B9D} = {2849E9D4-3B4E-40A3-A309-F3CB4F0E125F} + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {5D3B8245-ADA6-453F-A008-50ED04BFE770} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {E3F9F378-AFE1-40A5-90BD-82833375DBFE} = {227C3B55-80E5-4E7E-A802-BE16C5128B9D} {5B03EF4E-E0AC-4905-861B-8C3EC1A0D458} = {227C3B55-80E5-4E7E-A802-BE16C5128B9D} @@ -164,7 +185,6 @@ Global {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {C7311C00-2184-409B-B506-52A5FAEA8736} = {FD962632-184C-4005-A5F3-E705D92FC645} {FB5676ED-7A69-492C-B802-E7B24144C0FC} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC} From bfa6bc43d8c55818b021da5f49ca1fbb6d681a58 Mon Sep 17 00:00:00 2001 From: nzdev Date: Tue, 16 Feb 2021 20:56:44 +1300 Subject: [PATCH 081/289] Implement selectfields for examine --- build/NuSpecs/UmbracoCms.Web.nuspec | 2 +- src/Umbraco.Examine/Umbraco.Examine.csproj | 2 +- src/Umbraco.Examine/UmbracoContentIndex.cs | 7 +++-- src/Umbraco.Tests/Umbraco.Tests.csproj | 2 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 2 +- src/Umbraco.Web/IPublishedContentQuery.cs | 25 ++++++++++++++++ src/Umbraco.Web/PublishedContentQuery.cs | 12 ++++++-- src/Umbraco.Web/Search/ExamineComponent.cs | 4 ++- .../Search/GenericIndexDiagnostics.cs | 4 ++- .../Search/IUmbracoTreeSearcherFields2.cs | 29 +++++++++++++++++++ src/Umbraco.Web/Search/UmbracoTreeSearcher.cs | 16 +++++++++- .../Search/UmbracoTreeSearcherFields.cs | 26 ++++++++++++++++- src/Umbraco.Web/Umbraco.Web.csproj | 3 +- 13 files changed, 120 insertions(+), 14 deletions(-) create mode 100644 src/Umbraco.Web/Search/IUmbracoTreeSearcherFields2.cs diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index 82d15d2b95..59bd7d75aa 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -28,7 +28,7 @@ - + diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index 0e0ee62139..517edf354c 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -49,7 +49,7 @@ - + 1.0.0-beta2-19324-01 runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Umbraco.Examine/UmbracoContentIndex.cs b/src/Umbraco.Examine/UmbracoContentIndex.cs index 88033b1407..468e794110 100644 --- a/src/Umbraco.Examine/UmbracoContentIndex.cs +++ b/src/Umbraco.Examine/UmbracoContentIndex.cs @@ -11,7 +11,7 @@ using Lucene.Net.Store; using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Examine.LuceneEngine; - +using Examine.Search; namespace Umbraco.Examine { /// @@ -21,7 +21,7 @@ namespace Umbraco.Examine { public const string VariesByCultureFieldName = SpecialFieldPrefix + "VariesByCulture"; protected ILocalizationService LanguageService { get; } - + private readonly ISet _idOnlyFieldSet = new HashSet { "id" }; #region Constructors /// @@ -131,8 +131,9 @@ namespace Umbraco.Examine var searcher = GetSearcher(); var c = searcher.CreateQuery(); var filtered = c.NativeQuery(rawQuery); - var results = filtered.Execute(); + var selectedFields = filtered.SelectFields(_idOnlyFieldSet); + var results = selectedFields.Execute(); ProfilingLogger.Debug(GetType(), "DeleteFromIndex with query: {Query} (found {TotalItems} results)", rawQuery, results.TotalItemCount); //need to queue a delete item for each one found diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 2ac28aa7d7..97604df0c6 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -79,7 +79,7 @@ - + 1.8.14 diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 01d029cae0..69bdeba643 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -88,7 +88,7 @@ - + diff --git a/src/Umbraco.Web/IPublishedContentQuery.cs b/src/Umbraco.Web/IPublishedContentQuery.cs index 7066475dc9..f513c1ac02 100644 --- a/src/Umbraco.Web/IPublishedContentQuery.cs +++ b/src/Umbraco.Web/IPublishedContentQuery.cs @@ -9,6 +9,31 @@ using Umbraco.Core.Xml; namespace Umbraco.Web { using Examine = global::Examine; + public interface IPublishedContentQuery2 : IPublishedContentQuery + { + /// + /// Searches content. + /// + /// The term to search. + /// The amount of results to skip. + /// The amount of results to take/return. + /// The total amount of records. + /// The culture (defaults to a culture insensitive search). + /// The name of the index to search (defaults to ). + /// The fields to load in the results of the search (defaults to all fields loaded). + /// + /// The search results. + /// + /// + /// + /// When the is not specified or is *, all cultures are searched. + /// To search for only invariant documents and fields use null. + /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. + /// + /// While enumerating results, the ambient culture is changed to be the searched culture. + /// + IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Umbraco.Core.Constants.UmbracoIndexes.ExternalIndexName, ISet loadedFields = null); + } /// /// Query methods used for accessing strongly typed content in templates diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index d697898f33..760a4b1c82 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -16,7 +16,7 @@ namespace Umbraco.Web /// /// A class used to query for published content, media items /// - public class PublishedContentQuery : IPublishedContentQuery + public class PublishedContentQuery : IPublishedContentQuery2 { private readonly IPublishedSnapshot _publishedSnapshot; private readonly IVariationContextAccessor _variationContextAccessor; @@ -190,6 +190,10 @@ namespace Umbraco.Web /// public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName) + => Search(term, skip, take, out totalRecords, culture, indexName, null); + + /// + public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName, ISet loadedFields = null) { if (skip < 0) { @@ -212,7 +216,7 @@ namespace Umbraco.Web } var query = umbIndex.GetSearcher().CreateQuery(IndexTypes.Content); - + IQueryExecutor queryExecutor; if (culture == "*") { @@ -231,6 +235,10 @@ namespace Umbraco.Web var fields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); // Get all index fields suffixed with the culture name supplied queryExecutor = query.ManagedQuery(term, fields); } + if (loadedFields != null && queryExecutor is IBooleanOperation booleanOperation) + { + queryExecutor = booleanOperation.SelectFields(loadedFields); + } var results = skip == 0 && take == 0 ? queryExecutor.Execute() diff --git a/src/Umbraco.Web/Search/ExamineComponent.cs b/src/Umbraco.Web/Search/ExamineComponent.cs index c9d7b7cf56..4fa2427fac 100644 --- a/src/Umbraco.Web/Search/ExamineComponent.cs +++ b/src/Umbraco.Web/Search/ExamineComponent.cs @@ -19,6 +19,7 @@ using Umbraco.Core.Composing; using System.ComponentModel; using System.Threading; using Umbraco.Web.Scheduling; +using Examine.Search; namespace Umbraco.Web.Search { @@ -39,6 +40,7 @@ namespace Umbraco.Web.Search private readonly IProfilingLogger _logger; private readonly IUmbracoIndexesCreator _indexCreator; private readonly BackgroundTaskRunner _indexItemTaskRunner; + private readonly ISet _idOnlyFieldSet = new HashSet { "id" }; // the default enlist priority is 100 @@ -425,7 +427,7 @@ namespace Umbraco.Web.Search while (page * pageSize < total) { //paging with examine, see https://shazwazza.com/post/paging-with-examine/ - var results = searcher.CreateQuery().Field("nodeType", id.ToInvariantString()).Execute(maxResults: pageSize * (page + 1)); + var results = searcher.CreateQuery().Field("nodeType", id.ToInvariantString()).SelectFields(_idOnlyFieldSet).Execute(maxResults: pageSize * (page + 1)); total = results.TotalItemCount; var paged = results.Skip(page * pageSize); diff --git a/src/Umbraco.Web/Search/GenericIndexDiagnostics.cs b/src/Umbraco.Web/Search/GenericIndexDiagnostics.cs index cb25e1242a..c8d3a72a96 100644 --- a/src/Umbraco.Web/Search/GenericIndexDiagnostics.cs +++ b/src/Umbraco.Web/Search/GenericIndexDiagnostics.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Examine; +using Examine.Search; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Examine; @@ -17,6 +18,7 @@ namespace Umbraco.Web.Search private readonly IIndex _index; private static readonly string[] IgnoreProperties = { "Description" }; + private readonly ISet _idOnlyFieldSet = new HashSet { "id" }; public GenericIndexDiagnostics(IIndex index) { _index = index; @@ -34,7 +36,7 @@ namespace Umbraco.Web.Search try { var searcher = _index.GetSearcher(); - var result = searcher.Search("test"); + var result = searcher.CreateQuery().ManagedQuery("test").SelectFields(_idOnlyFieldSet).Execute(1); return Attempt.Succeed(); //if we can search we'll assume it's healthy } catch (Exception e) diff --git a/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields2.cs b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields2.cs new file mode 100644 index 0000000000..42f592a965 --- /dev/null +++ b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields2.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Web.Search +{ + public interface IUmbracoTreeSearcherFields2 : IUmbracoTreeSearcherFields + { + /// + /// Set of fields for all node types to be loaded + /// + ISet GetBackOfficeFieldsToLoad(); + /// + /// Set list of fields for Members to be loaded + /// + ISet GetBackOfficeMembersFieldsToLoad(); + /// + /// Set of fields for Media to be loaded + /// + ISet GetBackOfficeMediaFieldsToLoad(); + + /// + /// Set of fields for Documents to be loaded + /// + ISet GetBackOfficeDocumentFieldsToLoad(); + } +} diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 410b654e32..44ba0766e0 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; using Examine; +using Examine.Search; using Umbraco.Core; using Umbraco.Core.Mapping; using Umbraco.Core.Models; @@ -82,6 +83,7 @@ namespace Umbraco.Web.Search string type; var indexName = Constants.UmbracoIndexes.InternalIndexName; var fields = _umbracoTreeSearcherFields.GetBackOfficeFields().ToList(); + ISet fieldsToLoad = null; // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string // manipulation for things like start paths, member types, etc... @@ -102,6 +104,10 @@ namespace Umbraco.Web.Search indexName = Constants.UmbracoIndexes.MembersIndexName; type = "member"; fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeMembersFields()); + if (_umbracoTreeSearcherFields is IUmbracoTreeSearcherFields2 umbracoTreeSearcherFieldMember) + { + fieldsToLoad = umbracoTreeSearcherFieldMember.GetBackOfficeMembersFieldsToLoad(); + } if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && searchFrom.Trim() != "-1") { sb.Append("+__NodeTypeAlias:"); @@ -112,12 +118,20 @@ namespace Umbraco.Web.Search case UmbracoEntityTypes.Media: type = "media"; fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeMediaFields()); + if (_umbracoTreeSearcherFields is IUmbracoTreeSearcherFields2 umbracoTreeSearcherFieldsMedia) + { + fieldsToLoad = umbracoTreeSearcherFieldsMedia.GetBackOfficeMediaFieldsToLoad(); + } var allMediaStartNodes = _umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(_entityService); AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; case UmbracoEntityTypes.Document: type = "content"; fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeDocumentFields()); + if (_umbracoTreeSearcherFields is IUmbracoTreeSearcherFields2 umbracoTreeSearcherFieldsDocument) + { + fieldsToLoad = umbracoTreeSearcherFieldsDocument.GetBackOfficeDocumentFieldsToLoad(); + } var allContentStartNodes = _umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(_entityService); AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; @@ -136,7 +150,7 @@ namespace Umbraco.Web.Search return Enumerable.Empty(); } - var result = internalSearcher.CreateQuery().NativeQuery(sb.ToString()) + var result = internalSearcher.CreateQuery().NativeQuery(sb.ToString()).SelectFields(fieldsToLoad) //only return the number of items specified to read up to the amount of records to fill from 0 -> the number of items on the page requested .Execute(Convert.ToInt32(pageSize * (pageIndex + 1))); diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs index f90d7bc6b6..5a2fd91d18 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs @@ -4,7 +4,7 @@ using Umbraco.Examine; namespace Umbraco.Web.Search { - public class UmbracoTreeSearcherFields : IUmbracoTreeSearcherFields + public class UmbracoTreeSearcherFields : IUmbracoTreeSearcherFields2 { private IReadOnlyList _backOfficeFields = new List {"id", "__NodeId", "__Key"}; public IEnumerable GetBackOfficeFields() @@ -27,5 +27,29 @@ namespace Umbraco.Web.Search { return Enumerable.Empty(); } + + private readonly ISet _backOfficeFieldsToLoad = new HashSet { "id", "__NodeId", "__Key" }; + public ISet GetBackOfficeFieldsToLoad() + { + return _backOfficeFieldsToLoad; + } + + private readonly ISet _backOfficeMembersFieldsToLoad = new HashSet { "id", "__NodeId", "__Key", "email", "loginName" }; + public ISet GetBackOfficeMembersFieldsToLoad() + { + return _backOfficeMembersFieldsToLoad; + } + + private readonly ISet _backOfficeMediaFieldsToLoad = new HashSet { "id", "__NodeId", "__Key", UmbracoExamineIndex.UmbracoFileFieldName }; + public ISet GetBackOfficeMediaFieldsToLoad() + { + return _backOfficeMediaFieldsToLoad; + } + private readonly ISet _backOfficeDocumentFieldsToLoad = new HashSet { "id", "__NodeId", "__Key" }; + + public ISet GetBackOfficeDocumentFieldsToLoad() + { + return _backOfficeDocumentFieldsToLoad; + } } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 8890d9cf25..189d2e02d1 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -63,7 +63,7 @@ - + 4.0.217 @@ -284,6 +284,7 @@ + From 7d135899be68a4b79bfada012c2b438461772042 Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Sun, 21 Feb 2021 20:45:03 +1300 Subject: [PATCH 082/289] load only once --- .../Sync/DatabaseServerMessenger.cs | 90 ++++++++++++------- .../Sync/ISyncBootStateAccessor.cs | 35 ++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../PublishedContent/NuCacheChildrenTests.cs | 4 +- .../PublishedContent/NuCacheTests.cs | 4 +- .../Scoping/ScopedNuCacheTests.cs | 3 +- .../ContentTypeServiceVariantsTests.cs | 4 +- .../TestHelpers/TestSyncBootStateAccessor.cs | 23 +++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + ...aseServerRegistrarAndMessengerComponent.cs | 1 + .../NuCache/PublishedSnapshotService.cs | 17 +++- 11 files changed, 146 insertions(+), 37 deletions(-) create mode 100644 src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs create mode 100644 src/Umbraco.Tests/TestHelpers/TestSyncBootStateAccessor.cs mode change 100755 => 100644 src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index 7442169b44..8264ddd79c 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -28,7 +28,7 @@ namespace Umbraco.Core.Sync // but only processes instructions coming from remote servers, // thus ensuring that instructions run only once // - public class DatabaseServerMessenger : ServerMessengerBase + public class DatabaseServerMessenger : ServerMessengerBase, ISyncBootStateAccessor { private readonly IRuntimeState _runtime; private readonly ManualResetEvent _syncIdle; @@ -172,35 +172,7 @@ namespace Umbraco.Core.Sync lock (_locko) { if (_released) return; - - var coldboot = false; - if (_lastId < 0) // never synced before - { - // we haven't synced - in this case we aren't going to sync the whole thing, we will assume this is a new - // server and it will need to rebuild it's own caches, eg Lucene or the xml cache file. - Logger.Warn("No last synced Id found, this generally means this is a new server/install." - + " The server will build its caches and indexes, and then adjust its last synced Id to the latest found in" - + " the database and maintain cache updates based on that Id."); - - coldboot = true; - } - else - { - //check for how many instructions there are to process, each row contains a count of the number of instructions contained in each - //row so we will sum these numbers to get the actual count. - var count = database.ExecuteScalar("SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new {lastId = _lastId}); - if (count > Options.MaxProcessingInstructionCount) - { - //too many instructions, proceed to cold boot - Logger.Warn( - "The instruction count ({InstructionCount}) exceeds the specified MaxProcessingInstructionCount ({MaxProcessingInstructionCount})." - + " The server will skip existing instructions, rebuild its caches and indexes entirely, adjust its last synced Id" - + " to the latest found in the database and maintain cache updates based on that Id.", - count, Options.MaxProcessingInstructionCount); - - coldboot = true; - } - } + var coldboot = IsColdBoot(database); if (coldboot) { @@ -223,6 +195,40 @@ namespace Umbraco.Core.Sync } } + private bool IsColdBoot(IUmbracoDatabase database) + { + var coldboot = false; + if (_lastId < 0) // never synced before + { + // we haven't synced - in this case we aren't going to sync the whole thing, we will assume this is a new + // server and it will need to rebuild it's own caches, eg Lucene or the xml cache file. + Logger.Warn("No last synced Id found, this generally means this is a new server/install." + + " The server will build its caches and indexes, and then adjust its last synced Id to the latest found in" + + " the database and maintain cache updates based on that Id."); + + coldboot = true; + } + else + { + //check for how many instructions there are to process, each row contains a count of the number of instructions contained in each + //row so we will sum these numbers to get the actual count. + var count = database.ExecuteScalar("SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new {lastId = _lastId}); + if (count > Options.MaxProcessingInstructionCount) + { + //too many instructions, proceed to cold boot + Logger.Warn( + "The instruction count ({InstructionCount}) exceeds the specified MaxProcessingInstructionCount ({MaxProcessingInstructionCount})." + + " The server will skip existing instructions, rebuild its caches and indexes entirely, adjust its last synced Id" + + " to the latest found in the database and maintain cache updates based on that Id.", + count, Options.MaxProcessingInstructionCount); + + coldboot = true; + } + } + + return coldboot; + } + /// /// Synchronize the server (throttled). /// @@ -548,6 +554,30 @@ namespace Umbraco.Core.Sync #endregion + public SyncBootState GetSyncBootState() + { + try + { + ReadLastSynced(); // get _lastId + using (var scope = ScopeProvider.CreateScope()) + { + EnsureInstructions(scope.Database); + bool isColdBoot = IsColdBoot(scope.Database); + + if (isColdBoot) + { + return SyncBootState.ColdBoot; + } + return SyncBootState.HasSyncState; + } + } + catch(Exception ex) + { + Logger.Warn("Error determining Sync Boot State", ex); + return SyncBootState.Unknown; + } + } + #region Notify refreshers private static ICacheRefresher GetRefresher(Guid id) diff --git a/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs new file mode 100644 index 0000000000..a7b7c58235 --- /dev/null +++ b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Sync +{ + /// + /// Retrieve the state of the sync service + /// + public interface ISyncBootStateAccessor + { + /// + /// Get the boot state + /// + /// + SyncBootState GetSyncBootState(); + } + public enum SyncBootState + { + /// + /// Unknown state. Treat as HasSyncState + /// + Unknown = 0, + /// + /// Cold boot. No Sync state + /// + ColdBoot = 1, + /// + /// Warm boot. Sync state present + /// + HasSyncState = 2 + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 465ddee6ee..5c3dd074dd 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -173,6 +173,7 @@ + diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index 834d211994..068a161268 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -17,6 +17,7 @@ using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; using Umbraco.Core.Strings; +using Umbraco.Core.Sync; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing.Objects; using Umbraco.Tests.Testing.Objects.Accessors; @@ -155,7 +156,8 @@ namespace Umbraco.Tests.PublishedContent globalSettings, Mock.Of(), Mock.Of(), - new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() })); + new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), + new TestSyncBootStateAccessor(SyncBootState.HasSyncState)); // invariant is the current default _variationAccesor.VariationContext = new VariationContext(); diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs index 0e05e6baad..652891c476 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs @@ -17,6 +17,7 @@ using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; using Umbraco.Core.Strings; +using Umbraco.Core.Sync; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing.Objects; using Umbraco.Tests.Testing.Objects.Accessors; @@ -201,7 +202,8 @@ namespace Umbraco.Tests.PublishedContent globalSettings, Mock.Of(), Mock.Of(), - new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() })); + new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), + new TestSyncBootStateAccessor(SyncBootState.HasSyncState)); // invariant is the current default _variationAccesor.VariationContext = new VariationContext(); diff --git a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs index c7c403b260..e204bd7b5f 100644 --- a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs @@ -99,7 +99,8 @@ namespace Umbraco.Tests.Scoping Factory.GetInstance(), Factory.GetInstance(), Mock.Of(), - new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() })); + new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), + new TestSyncBootStateAccessor(SyncBootState.HasSyncState)); } protected UmbracoContext GetUmbracoContextNu(string url, int templateId = 1234, RouteData routeData = null, bool setSingleton = false, IUmbracoSettingsSection umbracoSettings = null, IEnumerable urlProviders = null) diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index 9391b7442f..9cd4bb63e8 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -17,6 +17,7 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Core.Sync; +using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; using Umbraco.Web.PublishedCache; @@ -70,7 +71,8 @@ namespace Umbraco.Tests.Services Factory.GetInstance(), Factory.GetInstance(), Mock.Of(), - new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() })); + new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), + new TestSyncBootStateAccessor(SyncBootState.HasSyncState)); } public class LocalServerMessenger : ServerMessengerBase diff --git a/src/Umbraco.Tests/TestHelpers/TestSyncBootStateAccessor.cs b/src/Umbraco.Tests/TestHelpers/TestSyncBootStateAccessor.cs new file mode 100644 index 0000000000..e5f6989381 --- /dev/null +++ b/src/Umbraco.Tests/TestHelpers/TestSyncBootStateAccessor.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Sync; + +namespace Umbraco.Tests.TestHelpers +{ + class TestSyncBootStateAccessor : ISyncBootStateAccessor + { + private readonly SyncBootState _syncBootState; + + public TestSyncBootStateAccessor(SyncBootState syncBootState) + { + _syncBootState = syncBootState; + } + public SyncBootState GetSyncBootState() + { + return _syncBootState; + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 2ac28aa7d7..6c4f7415ea 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -175,6 +175,7 @@ + diff --git a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs index 2fa9d80779..688fc268b0 100644 --- a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs +++ b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs @@ -72,6 +72,7 @@ namespace Umbraco.Web.Compose composition.SetDatabaseServerMessengerOptions(GetDefaultOptions); composition.SetServerMessenger(); + composition.Register(Lifetime.Singleton); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs old mode 100755 new mode 100644 index a39e26e2b1..a592fed4be --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -24,6 +24,7 @@ using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; +using Umbraco.Core.Sync; using Umbraco.Web.Cache; using Umbraco.Web.Install; using Umbraco.Web.PublishedCache.NuCache.DataSource; @@ -63,6 +64,8 @@ namespace Umbraco.Web.PublishedCache.NuCache private bool _localContentDbExists; private bool _localMediaDbExists; + private readonly ISyncBootStateAccessor _syncBootStateAccessor; + // define constant - determines whether to use cache when previewing // to store eg routes, property converted values, anything - caching // means faster execution, but uses memory - not sure if we want it @@ -81,7 +84,8 @@ namespace Umbraco.Web.PublishedCache.NuCache IDataSource dataSource, IGlobalSettings globalSettings, IEntityXmlSerializer entitySerializer, IPublishedModelFactory publishedModelFactory, - UrlSegmentProviderCollection urlSegmentProviders) + UrlSegmentProviderCollection urlSegmentProviders, + ISyncBootStateAccessor syncBootStateAccessor) : base(publishedSnapshotAccessor, variationContextAccessor) { //if (Interlocked.Increment(ref _singletonCheck) > 1) @@ -99,6 +103,8 @@ namespace Umbraco.Web.PublishedCache.NuCache _globalSettings = globalSettings; _urlSegmentProviders = urlSegmentProviders; + _syncBootStateAccessor = syncBootStateAccessor; + // we need an Xml serializer here so that the member cache can support XPath, // for members this is done by navigating the serialized-to-xml member _entitySerializer = entitySerializer; @@ -217,7 +223,12 @@ namespace Umbraco.Web.PublishedCache.NuCache { var okContent = false; var okMedia = false; - + if (_syncBootStateAccessor.GetSyncBootState() == SyncBootState.ColdBoot) + { + _logger.Warn("Sync Service is in a Cold Boot state. Skip LoadCachesOnStartup as the Sync Service will trigger a full reload"); + _isReady = true; + return; + } try { if (_localContentDbExists) @@ -233,7 +244,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (!okMedia) _logger.Warn("Loading media from local db raised warnings, will reload from database."); } - + if (!okContent) LockAndLoadContent(scope => LoadContentFromDatabaseLocked(scope, true)); From 92d11845f511a97ea00b4836af3672ec22803689 Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Fri, 12 Mar 2021 14:00:25 +1100 Subject: [PATCH 083/289] Reverting accidental change --- src/umbraco.sln | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/umbraco.sln b/src/umbraco.sln index 72b06a517a..4d630a8ead 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -58,24 +58,8 @@ Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.Client", "ht StartServerOnDebug = "false" EndProjectSection EndProject -Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTest(1)", "Umbraco.Tests.AcceptanceTest\", "{9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}" +Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTest\", "Umbraco.Tests.AcceptanceTest\", "{9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}" ProjectSection(WebsiteProperties) = preProject - TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.0" - Debug.AspNetCompiler.VirtualPath = "/localhost_62926" - Debug.AspNetCompiler.PhysicalPath = "Umbraco.Tests.AcceptanceTest\" - Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_62926\" - Debug.AspNetCompiler.Updateable = "true" - Debug.AspNetCompiler.ForceOverwrite = "true" - Debug.AspNetCompiler.FixedNames = "false" - Debug.AspNetCompiler.Debug = "True" - Release.AspNetCompiler.VirtualPath = "/localhost_62926" - Release.AspNetCompiler.PhysicalPath = "Umbraco.Tests.AcceptanceTest\" - Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_62926\" - Release.AspNetCompiler.Updateable = "true" - Release.AspNetCompiler.ForceOverwrite = "true" - Release.AspNetCompiler.FixedNames = "false" - Release.AspNetCompiler.Debug = "False" - VWDPort = "62926" SlnRelativePath = "Umbraco.Tests.AcceptanceTest\" EndProjectSection EndProject From 097a46189f3d72fdc5c42877c3900185cf88df70 Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Fri, 12 Mar 2021 14:01:04 +1100 Subject: [PATCH 084/289] Reverting accidental change --- src/umbraco.sln | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/umbraco.sln b/src/umbraco.sln index 4d630a8ead..eec774344b 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -123,10 +123,6 @@ Global {4C4C194C-B5E4-4991-8F87-4373E24CC19F}.Release|Any CPU.Build.0 = Release|Any CPU {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Release|Any CPU.Build.0 = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Debug|Any CPU.Build.0 = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Release|Any CPU.ActiveCfg = Release|Any CPU From b606b925dd5554731798a4eea551f7065ba9ca08 Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Fri, 12 Mar 2021 14:01:51 +1100 Subject: [PATCH 085/289] Reverting accidental change --- src/umbraco.sln | 1 - 1 file changed, 1 deletion(-) diff --git a/src/umbraco.sln b/src/umbraco.sln index eec774344b..9f2b19c2dd 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -157,7 +157,6 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {227C3B55-80E5-4E7E-A802-BE16C5128B9D} = {2849E9D4-3B4E-40A3-A309-F3CB4F0E125F} - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {5D3B8245-ADA6-453F-A008-50ED04BFE770} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {E3F9F378-AFE1-40A5-90BD-82833375DBFE} = {227C3B55-80E5-4E7E-A802-BE16C5128B9D} {5B03EF4E-E0AC-4905-861B-8C3EC1A0D458} = {227C3B55-80E5-4E7E-A802-BE16C5128B9D} From efbfa0ba30ed1ab11ba86a5ab75c886a3a9c877d Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Fri, 12 Mar 2021 14:02:48 +1100 Subject: [PATCH 086/289] Reverting accidental change --- src/umbraco.sln | 1 + 1 file changed, 1 insertion(+) diff --git a/src/umbraco.sln b/src/umbraco.sln index 9f2b19c2dd..63fb856b5d 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -164,6 +164,7 @@ Global {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {C7311C00-2184-409B-B506-52A5FAEA8736} = {FD962632-184C-4005-A5F3-E705D92FC645} {FB5676ED-7A69-492C-B802-E7B24144C0FC} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC} From a62112ab823f266ba40503cb3eb02dce4aa85837 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 12 Mar 2021 16:33:09 +1100 Subject: [PATCH 087/289] Don't call the same method over and over --- .../PublishedCache/NuCache/PublishedSnapshotService.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index dbbcb4ee9b..d26e0c67f7 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1492,9 +1492,12 @@ namespace Umbraco.Web.PublishedCache.NuCache scope.ReadLock(Constants.Locks.ContentTree); scope.ReadLock(Constants.Locks.MediaTree); scope.ReadLock(Constants.Locks.MemberTree); - RebuildContentDbCacheLocked(serializer, scope, GetSqlPagingSize(), null); - RebuildMediaDbCacheLocked(serializer, scope, GetSqlPagingSize(), null); - RebuildMemberDbCacheLocked(serializer, scope, GetSqlPagingSize(), null); + + var groupSize = GetSqlPagingSize(); + + RebuildContentDbCacheLocked(serializer, scope, groupSize, null); + RebuildMediaDbCacheLocked(serializer, scope, groupSize, null); + RebuildMemberDbCacheLocked(serializer, scope, groupSize, null); scope.Complete(); } } From e6b56d9a3c88d4cf9ee1ec83dd18097420d7b350 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 12 Mar 2021 16:33:37 +1100 Subject: [PATCH 088/289] missing null check --- src/Umbraco.Web/UmbracoApplicationBase.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web/UmbracoApplicationBase.cs b/src/Umbraco.Web/UmbracoApplicationBase.cs index 32a949e972..ef773ab56d 100644 --- a/src/Umbraco.Web/UmbracoApplicationBase.cs +++ b/src/Umbraco.Web/UmbracoApplicationBase.cs @@ -169,6 +169,8 @@ namespace Umbraco.Web { var exception = Server.GetLastError(); + if (exception == null) return; + // ignore HTTP errors if (exception.GetType() == typeof(HttpException)) return; From 8c7abc60aa4b5c53265dba228b1dbba9d8c23269 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 12 Mar 2021 16:50:29 +1100 Subject: [PATCH 089/289] inject lazy service just to be safe --- .../NuCache/NuCacheSerializerComponent.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs index 4b4ccd295a..19499dacf9 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs @@ -17,13 +17,15 @@ namespace Umbraco.Web.PublishedCache.NuCache { private const string Nucache_Serializer_Key = "Umbraco.Web.PublishedCache.NuCache.Serializer"; private const string JSON_SERIALIZER_VALUE = "JSON"; - private readonly IPublishedSnapshotService _service; + private readonly Lazy _service; private readonly IKeyValueService _keyValueService; private readonly IProfilingLogger _profilingLogger; - public NuCacheSerializerComponent(IPublishedSnapshotService service, IKeyValueService keyValueService,IProfilingLogger profilingLogger) + public NuCacheSerializerComponent(Lazy service, IKeyValueService keyValueService, IProfilingLogger profilingLogger) { - // service: nothing - this just ensures that the service is created at boot time + // We are using lazy here as a work around because the service does quite a lot of initialization in the ctor which + // we want to avoid where possible. Since we only need the service if we are rebuilding, we don't want to eagerly + // initialize anything unless we need to. _service = service; _keyValueService = keyValueService; _profilingLogger = profilingLogger; @@ -50,11 +52,11 @@ namespace Umbraco.Web.PublishedCache.NuCache if (serializer != currentSerializer) { - _profilingLogger.Info($"Database NuCache was serialized using {currentSerializer}. Currently configured NuCache serializer {serializer}. Rebuilding Nucache"); + _profilingLogger.Warn($"Database NuCache was serialized using {currentSerializer}. Currently configured NuCache serializer {serializer}. Rebuilding Nucache"); + using (_profilingLogger.TraceDuration($"Rebuilding NuCache database with {currentSerializer} serializer")) { - - _service.Rebuild(); + _service.Value.Rebuild(); _keyValueService.SetValue(Nucache_Serializer_Key, serializer); } } From d2a26bf4da986e712a8b18fadf4e142ac768b082 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Thu, 25 Mar 2021 11:49:38 +0100 Subject: [PATCH 090/289] Caching RecycleBinSmells --- src/Umbraco.Core/Cache/CacheKeys.cs | 3 +++ .../Cache/ContentCacheRefresher.cs | 1 + src/Umbraco.Web/Cache/MediaCacheRefresher.cs | 1 + .../Trees/ContentTreeController.cs | 2 ++ .../Trees/ContentTreeControllerBase.cs | 25 +++++++++++++++++-- src/Umbraco.Web/Trees/MediaTreeController.cs | 1 + 6 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index 0e9a9a3862..642bef6d0c 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -17,5 +17,8 @@ public const string UserAllMediaStartNodesPrefix = "AllMediaStartNodes"; public const string UserMediaStartNodePathsPrefix = "MediaStartNodePaths"; public const string UserContentStartNodePathsPrefix = "ContentStartNodePaths"; + + public const string ContentRecycleBinCacheKey = "recycleBin_content"; + public const string MediaRecycleBinCacheKey = "recycleBin_media"; } } diff --git a/src/Umbraco.Web/Cache/ContentCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentCacheRefresher.cs index 6abad820c9..5e8bd83c5d 100644 --- a/src/Umbraco.Web/Cache/ContentCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ContentCacheRefresher.cs @@ -46,6 +46,7 @@ namespace Umbraco.Web.Cache public override void Refresh(JsonPayload[] payloads) { AppCaches.RuntimeCache.ClearOfType(); + AppCaches.RuntimeCache.ClearByKey(CacheKeys.ContentRecycleBinCacheKey); var idsRemoved = new HashSet(); var isolatedCache = AppCaches.IsolatedCaches.GetOrCreate(); diff --git a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs index 1f54b62c5b..b0845f2a9a 100644 --- a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs @@ -48,6 +48,7 @@ namespace Umbraco.Web.Cache if (anythingChanged) { Current.AppCaches.ClearPartialViewCache(); + AppCaches.RuntimeCache.ClearByKey(CacheKeys.MediaRecycleBinCacheKey); var mediaCache = AppCaches.IsolatedCaches.Get(); diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index d82166b9a3..3a4033e724 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -44,6 +44,8 @@ namespace Umbraco.Web.Trees protected override bool RecycleBinSmells => Services.ContentService.RecycleBinSmells(); + public override string RecycleBinSmellsCacheKey => CacheKeys.ContentRecycleBinCacheKey; + private int[] _userStartNodes; protected override int[] UserStartNodes diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index 57c7027598..c2a9019544 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -26,9 +26,11 @@ namespace Umbraco.Web.Trees { public abstract class ContentTreeControllerBase : TreeController { + private readonly AppCaches _appCaches; protected ContentTreeControllerBase(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) { + _appCaches = appCaches; } protected ContentTreeControllerBase() @@ -148,6 +150,11 @@ namespace Umbraco.Web.Trees /// protected abstract bool RecycleBinSmells { get; } + /// + /// Gets the name of the recycle bin cache key. + /// + public abstract string RecycleBinSmellsCacheKey { get; } + /// /// Returns the user's start node for this tree /// @@ -327,15 +334,29 @@ namespace Umbraco.Web.Trees //and for some reason when there are no dashboards, this parameter is missing if (IsDialog(queryStrings) == false && id == Constants.System.RootString && queryStrings.HasKey("application")) { + var cache = _appCaches.RuntimeCache; + + var hasChildren = cache.GetCacheItem(RecycleBinSmellsCacheKey); + bool recycleBinSmells; + + if (!(hasChildren is null)) + { + recycleBinSmells = (bool) hasChildren; + } + else + { + recycleBinSmells = RecycleBinSmells; + cache.InsertCacheItem(RecycleBinSmellsCacheKey, () => recycleBinSmells); + } + nodes.Add(CreateTreeNode( RecycleBinId.ToInvariantString(), id, queryStrings, Services.TextService.Localize("general/recycleBin"), "icon-trash", - RecycleBinSmells, + recycleBinSmells, queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/recyclebin")); - } return nodes; diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs index 43b5a83282..93e1fc22b6 100644 --- a/src/Umbraco.Web/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTreeController.cs @@ -47,6 +47,7 @@ namespace Umbraco.Web.Trees protected override int RecycleBinId => Constants.System.RecycleBinMedia; protected override bool RecycleBinSmells => Services.MediaService.RecycleBinSmells(); + public override string RecycleBinSmellsCacheKey => CacheKeys.MediaRecycleBinCacheKey; private int[] _userStartNodes; protected override int[] UserStartNodes From 19c17836aaae59b23428cadc190de927c6bc81c7 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Thu, 25 Mar 2021 12:54:46 +0100 Subject: [PATCH 091/289] Adding recycle bin smells caches at repository level --- .../Repositories/IDocumentRepository.cs | 5 ++++ .../Repositories/IMediaRepository.cs | 1 + .../Implement/DocumentRepository.cs | 21 +++++++++++++++++ .../Repositories/Implement/MediaRepository.cs | 23 +++++++++++++++++++ .../Services/ContentServiceExtensions.cs | 20 ---------------- src/Umbraco.Core/Services/IContentService.cs | 5 ++++ src/Umbraco.Core/Services/IMediaService.cs | 5 ++++ .../Services/Implement/ContentService.cs | 9 ++++++++ .../Services/Implement/MediaService.cs | 9 ++++++++ .../Trees/ContentTreeController.cs | 2 -- .../Trees/ContentTreeControllerBase.cs | 22 +----------------- src/Umbraco.Web/Trees/MediaTreeController.cs | 1 - 12 files changed, 79 insertions(+), 44 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs index 0971b2047a..6746d6a429 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs @@ -73,5 +73,10 @@ namespace Umbraco.Core.Persistence.Repositories /// /// void AddOrUpdatePermissions(ContentPermissionSet permission); + + /// + /// Returns true if there is any content in the recycle bin + /// + bool RecycleBinSmells(); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs index d4ec08a0df..6f36102a40 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs @@ -6,5 +6,6 @@ namespace Umbraco.Core.Persistence.Repositories public interface IMediaRepository : IContentRepository, IReadRepository { IMedia GetMediaByPath(string mediaPath); + bool RecycleBinSmells(); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 09d41a49a0..4303456096 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -912,6 +912,27 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public override int RecycleBinId => Constants.System.RecycleBinContent; + public bool RecycleBinSmells() + { + var cache = _appCaches.RuntimeCache; + var cacheKey = CacheKeys.ContentRecycleBinCacheKey; + + var hasChildren = cache.GetCacheItem(cacheKey); + bool recycleBinSmells; + + if (!(hasChildren is null)) + { + recycleBinSmells = (bool) hasChildren; + } + else + { + recycleBinSmells = CountChildren(Constants.System.RecycleBinContent) > 0; + cache.InsertCacheItem(cacheKey, () => recycleBinSmells); + } + + return recycleBinSmells; + } + #endregion #region Read Repository implementation for Guid keys diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index 02bef366cb..b8b891b251 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -24,6 +24,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// internal class MediaRepository : ContentRepositoryBase, IMediaRepository { + private readonly AppCaches _cache; private readonly IMediaTypeRepository _mediaTypeRepository; private readonly ITagRepository _tagRepository; private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; @@ -32,6 +33,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Lazy propertyEditorCollection, DataValueReferenceFactoryCollection dataValueReferenceFactories) : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFactories) { + _cache = cache; _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); _mediaByGuidReadRepository = new MediaByGuidReadRepository(this, scopeAccessor, cache, logger); @@ -369,6 +371,27 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public override int RecycleBinId => Constants.System.RecycleBinMedia; + public bool RecycleBinSmells() + { + var cache = _cache.RuntimeCache; + var cacheKey = CacheKeys.MediaRecycleBinCacheKey; + + var hasChildren = cache.GetCacheItem(cacheKey); + bool recycleBinSmells; + + if (!(hasChildren is null)) + { + recycleBinSmells = (bool) hasChildren; + } + else + { + recycleBinSmells = CountChildren(Constants.System.RecycleBinMedia) > 0; + cache.InsertCacheItem(cacheKey, () => recycleBinSmells); + } + + return recycleBinSmells; + } + #endregion #region Read Repository implementation for Guid keys diff --git a/src/Umbraco.Core/Services/ContentServiceExtensions.cs b/src/Umbraco.Core/Services/ContentServiceExtensions.cs index 1d980b036b..6ca894e160 100644 --- a/src/Umbraco.Core/Services/ContentServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentServiceExtensions.cs @@ -89,25 +89,5 @@ namespace Umbraco.Core.Services { contentService.SetPermissions(new EntityPermissionSet(contentId, new EntityPermissionCollection())); } - - /// - /// Returns true if there is any content in the recycle bin - /// - /// - /// - public static bool RecycleBinSmells(this IContentService contentService) - { - return contentService.CountChildren(Constants.System.RecycleBinContent) > 0; - } - - /// - /// Returns true if there is any media in the recycle bin - /// - /// - /// - public static bool RecycleBinSmells(this IMediaService mediaService) - { - return mediaService.CountChildren(Constants.System.RecycleBinMedia) > 0; - } } } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 58279fb4da..c291500533 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -326,6 +326,11 @@ namespace Umbraco.Core.Services /// Optional Id of the User emptying the Recycle Bin OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId); + /// + /// Returns true if there is any content in the recycle bin + /// + bool RecycleBinSmells(); + /// /// Sorts documents. /// diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index 3fecb20035..5bbfb76735 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -172,6 +172,11 @@ namespace Umbraco.Core.Services /// Optional Id of the User emptying the Recycle Bin OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId); + /// + /// Returns true if there is any media in the recycle bin + /// + bool RecycleBinSmells(); + /// /// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin. /// diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index a809b83f23..82b14dd45c 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -2118,6 +2118,15 @@ namespace Umbraco.Core.Services.Implement return OperationResult.Succeed(evtMsgs); } + public bool RecycleBinSmells() + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + return _documentRepository.RecycleBinSmells(); + } + } + #endregion #region Others diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index ac9c83458d..1ce0eb8bfd 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -1088,6 +1088,15 @@ namespace Umbraco.Core.Services.Implement return OperationResult.Succeed(evtMsgs); } + public bool RecycleBinSmells() + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.MediaTree); + return _mediaRepository.RecycleBinSmells(); + } + } + #endregion #region Others diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 3a4033e724..d82166b9a3 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -44,8 +44,6 @@ namespace Umbraco.Web.Trees protected override bool RecycleBinSmells => Services.ContentService.RecycleBinSmells(); - public override string RecycleBinSmellsCacheKey => CacheKeys.ContentRecycleBinCacheKey; - private int[] _userStartNodes; protected override int[] UserStartNodes diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index c2a9019544..1b47eaba55 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -150,11 +150,6 @@ namespace Umbraco.Web.Trees /// protected abstract bool RecycleBinSmells { get; } - /// - /// Gets the name of the recycle bin cache key. - /// - public abstract string RecycleBinSmellsCacheKey { get; } - /// /// Returns the user's start node for this tree /// @@ -334,28 +329,13 @@ namespace Umbraco.Web.Trees //and for some reason when there are no dashboards, this parameter is missing if (IsDialog(queryStrings) == false && id == Constants.System.RootString && queryStrings.HasKey("application")) { - var cache = _appCaches.RuntimeCache; - - var hasChildren = cache.GetCacheItem(RecycleBinSmellsCacheKey); - bool recycleBinSmells; - - if (!(hasChildren is null)) - { - recycleBinSmells = (bool) hasChildren; - } - else - { - recycleBinSmells = RecycleBinSmells; - cache.InsertCacheItem(RecycleBinSmellsCacheKey, () => recycleBinSmells); - } - nodes.Add(CreateTreeNode( RecycleBinId.ToInvariantString(), id, queryStrings, Services.TextService.Localize("general/recycleBin"), "icon-trash", - recycleBinSmells, + RecycleBinSmells, queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/recyclebin")); } diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs index 93e1fc22b6..43b5a83282 100644 --- a/src/Umbraco.Web/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTreeController.cs @@ -47,7 +47,6 @@ namespace Umbraco.Web.Trees protected override int RecycleBinId => Constants.System.RecycleBinMedia; protected override bool RecycleBinSmells => Services.MediaService.RecycleBinSmells(); - public override string RecycleBinSmellsCacheKey => CacheKeys.MediaRecycleBinCacheKey; private int[] _userStartNodes; protected override int[] UserStartNodes From 05e9710ca679bd20e786401ede9a0f072145c168 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Thu, 25 Mar 2021 13:29:27 +0100 Subject: [PATCH 092/289] Using an atomic operation when calling GetCacheItem - a callback is specified when the item isn't found --- .../Implement/DocumentRepository.cs | 16 ++-------------- .../Repositories/Implement/MediaRepository.cs | 18 +++--------------- 2 files changed, 5 insertions(+), 29 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 4303456096..e196a8a13c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -916,21 +916,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var cache = _appCaches.RuntimeCache; var cacheKey = CacheKeys.ContentRecycleBinCacheKey; - - var hasChildren = cache.GetCacheItem(cacheKey); - bool recycleBinSmells; - if (!(hasChildren is null)) - { - recycleBinSmells = (bool) hasChildren; - } - else - { - recycleBinSmells = CountChildren(Constants.System.RecycleBinContent) > 0; - cache.InsertCacheItem(cacheKey, () => recycleBinSmells); - } - - return recycleBinSmells; + // always cache either true or false + return cache.GetCacheItem(cacheKey, () => CountChildren(Constants.System.RecycleBinContent) > 0); } #endregion diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index b8b891b251..c9001eed31 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -375,21 +375,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var cache = _cache.RuntimeCache; var cacheKey = CacheKeys.MediaRecycleBinCacheKey; - - var hasChildren = cache.GetCacheItem(cacheKey); - bool recycleBinSmells; - if (!(hasChildren is null)) - { - recycleBinSmells = (bool) hasChildren; - } - else - { - recycleBinSmells = CountChildren(Constants.System.RecycleBinMedia) > 0; - cache.InsertCacheItem(cacheKey, () => recycleBinSmells); - } - - return recycleBinSmells; + // always cache either true or false + return cache.GetCacheItem(cacheKey, () => CountChildren(Constants.System.RecycleBinMedia) > 0); } #endregion @@ -523,7 +511,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) { - content[i] = (Models.Media) cached; + content[i] = (Models.Media)cached; continue; } } From 5f9d126ab7d087a204e32aed78148b07bc12ea2b Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Fri, 26 Mar 2021 16:13:47 +1300 Subject: [PATCH 093/289] fix support for non run states --- .../Sync/ISyncBootStateAccessor.cs | 15 ------------ .../Sync/NonRuntimeLevelBootStateAccessor.cs | 19 +++++++++++++++ src/Umbraco.Core/Sync/SyncBootState.cs | 24 +++++++++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 2 ++ ...aseServerRegistrarAndMessengerComponent.cs | 2 +- .../PublishedCache/NuCache/NuCacheComposer.cs | 4 ++++ 6 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs create mode 100644 src/Umbraco.Core/Sync/SyncBootState.cs diff --git a/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs index a7b7c58235..4b8500f2d9 100644 --- a/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs +++ b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs @@ -17,19 +17,4 @@ namespace Umbraco.Core.Sync /// SyncBootState GetSyncBootState(); } - public enum SyncBootState - { - /// - /// Unknown state. Treat as HasSyncState - /// - Unknown = 0, - /// - /// Cold boot. No Sync state - /// - ColdBoot = 1, - /// - /// Warm boot. Sync state present - /// - HasSyncState = 2 - } } diff --git a/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs b/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs new file mode 100644 index 0000000000..70cec6cc96 --- /dev/null +++ b/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Sync +{ + /// + /// Boot state implementation for when umbraco is not in the run state + /// + public class NonRuntimeLevelBootStateAccessor : ISyncBootStateAccessor + { + public SyncBootState GetSyncBootState() + { + return SyncBootState.Unknown; + } + } +} diff --git a/src/Umbraco.Core/Sync/SyncBootState.cs b/src/Umbraco.Core/Sync/SyncBootState.cs new file mode 100644 index 0000000000..4abc53abba --- /dev/null +++ b/src/Umbraco.Core/Sync/SyncBootState.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Sync +{ + public enum SyncBootState + { + /// + /// Unknown state. Treat as HasSyncState + /// + Unknown = 0, + /// + /// Cold boot. No Sync state + /// + ColdBoot = 1, + /// + /// Warm boot. Sync state present + /// + HasSyncState = 2 + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 5c3dd074dd..e0c0d78112 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -174,6 +174,8 @@ + + diff --git a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs index 688fc268b0..26ba0db324 100644 --- a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs +++ b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs @@ -72,7 +72,7 @@ namespace Umbraco.Web.Compose composition.SetDatabaseServerMessengerOptions(GetDefaultOptions); composition.SetServerMessenger(); - composition.Register(Lifetime.Singleton); + composition.Register(factory=> factory.GetInstance() as BatchedDatabaseServerMessenger, Lifetime.Singleton); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs index f748fd555c..99f2786d49 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs @@ -1,5 +1,6 @@ using Umbraco.Core; using Umbraco.Core.Composing; +using Umbraco.Core.Sync; using Umbraco.Web.PublishedCache.NuCache.DataSource; namespace Umbraco.Web.PublishedCache.NuCache @@ -10,6 +11,9 @@ namespace Umbraco.Web.PublishedCache.NuCache { base.Compose(composition); + //Overriden on Run state in DatabaseServerRegistrarAndMessengerComposer + composition.Register(Lifetime.Singleton); + // register the NuCache database data source composition.Register(); From 8d8fb2d15c087943a5b5fb5bb8da7d42f48e550a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 12 Apr 2021 12:27:12 +0200 Subject: [PATCH 094/289] =?UTF-8?q?Dont=20show=20block=20catalogue=20if=20?= =?UTF-8?q?only=20one=20block=20is=20available.=20Enable=20ente=E2=80=A6?= =?UTF-8?q?=20(#9575)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Dont show block catalogue if only one block is available. Enable entering clipboard directly. * corrected button states * jump clipboard icon when adding items to the clipboard. * fix merge issue * add missing eventsService * correcting missing parts from Merge Co-authored-by: Niels Lyngsø Co-authored-by: Mads Rasmussen --- .../blockpicker/blockpicker.controller.js | 11 +- .../umb-block-list-property-editor.html | 32 +++-- .../umb-block-list-property-editor.less | 110 ++++++++++++++---- .../umbBlockListPropertyEditor.component.js | 97 ++++++++++++--- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 3 + src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 2 + .../Umbraco/config/lang/en_us.xml | 2 + 7 files changed, 207 insertions(+), 50 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js index 90803a3765..5e6613c0f4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js @@ -17,7 +17,6 @@ angular.module("umbraco") "alias": "empty", "name": data[0], "icon": "icon-add", - "active": true, "view": "" }, { @@ -28,10 +27,16 @@ angular.module("umbraco") "disabled": vm.model.clipboardItems.length === 0 }]; - vm.activeTab = vm.navigation[0]; + if (vm.model.openClipboard === true) { + vm.activeTab = vm.navigation[1]; + } else { + vm.activeTab = vm.navigation[0]; + } + + vm.activeTab.active = true; } ); - + vm.onNavigationChanged = function (tab) { vm.activeTab.active = false; vm.activeTab = tab; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html index 9726daf5e6..87c0026d56 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html @@ -10,7 +10,7 @@ + + class="btn-reset umb-block-list__clipboard-button umb-outline" + ng-class="{'--jump': vm.jumpClipboardButton}" + ng-disabled="vm.clipboardItems.length === 0" + ng-click="vm.requestShowClipboard(vm.layout.length, $event)" + localize="title" + title="@blockEditor_tabClipboard"> + + + Clipboard + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less index e290c6e856..019a772fdd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less @@ -236,30 +236,98 @@ ng-form.ng-invalid-val-server-match-settings > .umb-block-list__block > .umb-blo } } } -.umb-block-list__create-button { - position: relative; + +.umb-block-list__actions { display: flex; - width: 100%; - align-items: center; - justify-content: center; + border: 1px dashed @ui-action-discreet-border; - color: @ui-action-discreet-type; - font-weight: bold; - margin: 2px 0; - padding: 5px 15px; - box-sizing: border-box; border-radius: @baseBorderRadius; -} + box-sizing: border-box; -.umb-block-list__create-button:hover { - color: @ui-action-discreet-type-hover; - border-color: @ui-action-discreet-border-hover; - text-decoration: none; -} + &:hover { + border-color: transparent; + > button { -.umb-block-list__create-button.--disabled, -.umb-block-list__create-button.--disabled:hover { - color: @gray-7; - border-color: @gray-7; - cursor: default; + border-color: @ui-action-discreet-border; + + &.umb-block-list__clipboard-button { + opacity: 1; + } + } + + } + + > button { + position: relative; + display: flex; + //width: 100%; + align-items: center; + justify-content: center; + + color: @ui-action-discreet-type; + font-weight: bold; + margin: -1px; + padding: 5px 15px; + + border: 1px dashed transparent; + border-radius: @baseBorderRadius; + box-sizing: border-box; + + &:hover { + color: @ui-action-discreet-type-hover; + border-color: @ui-action-discreet-border-hover; + text-decoration: none; + z-index: 1; + } + + &[disabled], + &[disabled]:hover { + color: @gray-7; + border-color: @gray-7; + cursor: default; + } + + &.umb-block-list__create-button { + flex-grow: 1; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + &.umb-block-list__clipboard-button { + margin-left: 0; + padding: 5px 12px; + font-size: 18px;// Align with block action buttons. + border-top-left-radius: 0; + border-bottom-left-radius: 0; + + opacity: 0; + &:hover, &:focus { + opacity: 1; + } + + &.--jump { + + @keyframes umb-block-list__jump-clipboard-button { + 0% { opacity: 0; } + 10% { opacity: 1; } + 90% { opacity: 1; } + 100% { opacity: 0; } + } + animation: umb-block-list__jump-clipboard-button 2s; + + i{ + @keyframes umb-block-list__jump-clipboard-button-i { + 10% { transform: scale(1); } + 10% { transform: scale(1.33); } + 20% { transform: scale(.82); } + 30% { transform: scale(1.24); } + 40% { transform: scale(.94); } + 50% { transform: scale(1); } + } + animation: umb-block-list__jump-clipboard-button-i 2s; + } + } + + } + } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js index 613e6a8c6a..2d9b13ec7a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js @@ -28,7 +28,7 @@ } }); - function BlockListController($scope, editorService, clipboardService, localizationService, overlayService, blockEditorService, udiService, serverValidationManager, angularHelper) { + function BlockListController($scope, $timeout, editorService, clipboardService, localizationService, overlayService, blockEditorService, udiService, serverValidationManager, angularHelper, eventsService) { var unsubscribe = []; var modelObject; @@ -53,6 +53,8 @@ }; vm.supportCopy = clipboardService.isSupported(); + vm.clipboardItems = []; + unsubscribe.push(eventsService.on("clipboardService.storageUpdate", updateClipboard)); vm.layout = []; // The layout object specific to this Block Editor, will be a direct reference from Property Model. vm.availableBlockTypes = []; // Available block entries of this property editor. @@ -187,6 +189,8 @@ vm.availableContentTypesAliases = modelObject.getAvailableAliasesForBlockContent(); vm.availableBlockTypes = modelObject.getAvailableBlocksForBlockPicker(); + updateClipboard(true); + vm.loading = false; $scope.$evalAsync(); @@ -406,9 +410,34 @@ editorService.open(blockEditorModel); } - vm.showCreateDialog = showCreateDialog; + vm.requestShowCreate = requestShowCreate; + function requestShowCreate(createIndex, mouseEvent) { - function showCreateDialog(createIndex, $event) { + if (vm.blockTypePicker) { + return; + } + + if (vm.availableBlockTypes.length === 1) { + var wasAdded = false; + var blockType = vm.availableBlockTypes[0]; + + wasAdded = addNewBlock(createIndex, blockType.blockConfigModel.contentElementTypeKey); + + if(wasAdded && !(mouseEvent.ctrlKey || mouseEvent.metaKey)) { + userFlowWhenBlockWasCreated(createIndex); + } + } else { + showCreateDialog(createIndex); + } + + } + vm.requestShowClipboard = requestShowClipboard; + function requestShowClipboard(createIndex, mouseEvent) { + showCreateDialog(createIndex, true); + } + + vm.showCreateDialog = showCreateDialog; + function showCreateDialog(createIndex, openClipboard) { if (vm.blockTypePicker) { return; @@ -424,6 +453,7 @@ $parentForm: vm.propertyForm, // pass in a $parentForm, this maintains the FormController hierarchy with the infinite editing view (if it contains a form) availableItems: vm.availableBlockTypes, title: vm.labels.grid_addElement, + openClipboard: openClipboard, orderBy: "$index", view: "views/common/infiniteeditors/blockpicker/blockpicker.html", size: (amountOfAvailableTypes > 8 ? "medium" : "small"), @@ -444,19 +474,15 @@ } }, submit: function(blockPickerModel, mouseEvent) { - var added = false; + var wasAdded = false; if (blockPickerModel && blockPickerModel.selectedItem) { - added = addNewBlock(createIndex, blockPickerModel.selectedItem.blockConfigModel.contentElementTypeKey); + wasAdded = addNewBlock(createIndex, blockPickerModel.selectedItem.blockConfigModel.contentElementTypeKey); } if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { editorService.close(); - if (added && vm.layout.length > createIndex) { - if (inlineEditing === true) { - activateBlock(vm.layout[createIndex].$block); - } else if (inlineEditing === false && vm.layout[createIndex].$block.hideContentInOverlay !== true) { - editBlock(vm.layout[createIndex].$block, false, createIndex, blockPickerModel.$parentForm, {createFlow: true}); - } + if (wasAdded) { + userFlowWhenBlockWasCreated(createIndex); } } }, @@ -475,7 +501,28 @@ clipboardService.clearEntriesOfType(clipboardService.TYPES.BLOCK, vm.availableContentTypesAliases); }; - blockPickerModel.clipboardItems = []; + blockPickerModel.clipboardItems = vm.clipboardItems; + + // open block picker overlay + editorService.open(blockPickerModel); + + }; + function userFlowWhenBlockWasCreated(createIndex) { + if (vm.layout.length > createIndex) { + var blockObject = vm.layout[createIndex].$block; + if (inlineEditing === true) { + blockObject.activate(); + } else if (inlineEditing === false && blockObject.hideContentInOverlay !== true) { + blockObject.edit(); + } + } + } + + function updateClipboard(firstTime) { + + var oldAmount = vm.clipboardItems.length; + + vm.clipboardItems = []; var entriesForPaste = clipboardService.retriveEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, vm.availableContentTypesAliases); entriesForPaste.forEach(function (entry) { @@ -511,19 +558,33 @@ if(Array.isArray(entry.data) === false) { pasteEntry.blockConfigModel = modelObject.getBlockConfiguration(entry.data.data.contentTypeKey); } - blockPickerModel.clipboardItems.push(pasteEntry); + vm.clipboardItems.push(pasteEntry); }); - blockPickerModel.clipboardItems.sort( (a, b) => { + vm.clipboardItems.sort( (a, b) => { return b.date - a.date }); - // open block picker overlay - editorService.open(blockPickerModel); + if(firstTime !== true && vm.clipboardItems.length > oldAmount) { + jumpClipboard(); + } + } - }; + var jumpClipboardTimeout; + function jumpClipboard() { - var requestCopyAllBlocks = function () { + if(jumpClipboardTimeout) { + return; + } + + vm.jumpClipboardButton = true; + jumpClipboardTimeout = $timeout(() => { + vm.jumpClipboardButton = false; + jumpClipboardTimeout = null; + }, 2000); + } + + function requestCopyAllBlocks() { var aliases = []; diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 39a6fee671..737181c668 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1891,7 +1891,10 @@ Mange hilsner fra Umbraco robotten Error! The ElementType of this block does not exist anymore + Tilføj indhold + Tilføj %0% Feltet %0% bruger editor %1% som ikke er supporteret for blokke. + Hvad er Indholdsskabeloner? diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 5865d39a75..3f6c985a0f 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2543,6 +2543,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Error! The ElementType of this block does not exist anymore + Add content + Add %0% Property '%0%' uses editor '%1%' which is not supported in blocks. diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 102f04371d..87b58e5063 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2562,6 +2562,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Error! The ElementType of this block does not exist anymore + Add content + Add %0% Property '%0%' uses editor '%1%' which is not supported in blocks. From 18fa631f1c225abd877555a2ccae81bf3b51c32d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 12 Apr 2021 14:38:52 +0200 Subject: [PATCH 095/289] use paste-resolvers (#10121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Niels Lyngsø --- .../nestedcontent/nestedcontent.controller.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index e1238313cc..5d7d37f7bf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -8,7 +8,7 @@ angular.module('umbraco').run(['clipboardService', function (clipboardService) { - function clearNestedContentPropertiesForStorage(prop, propClearingMethod) { + function resolveNestedContentPropertiesForPaste(prop, propClearingMethod) { // if prop.editor is "Umbraco.NestedContent" if ((typeof prop === 'object' && prop.editor === "Umbraco.NestedContent")) { @@ -17,8 +17,8 @@ for (var i = 0; i < value.length; i++) { var obj = value[i]; - // remove the key - delete obj.key; + // generate a new key. + obj.key = String.CreateGuid(); // Loop through all inner properties: for (var k in obj) { @@ -28,10 +28,10 @@ } } - clipboardService.registerClearPropertyResolver(clearNestedContentPropertiesForStorage, clipboardService.TYPES.ELEMENT_TYPE) + clipboardService.registerPastePropertyResolver(resolveNestedContentPropertiesForPaste, clipboardService.TYPES.ELEMENT_TYPE) - function clearInnerNestedContentPropertiesForStorage(prop, propClearingMethod) { + function resolveInnerNestedContentPropertiesForPaste(prop, propClearingMethod) { // if we got an array, and it has a entry with ncContentTypeAlias this meants that we are dealing with a NestedContent property data. if ((Array.isArray(prop) && prop.length > 0 && prop[0].ncContentTypeAlias !== undefined)) { @@ -39,8 +39,8 @@ for (var i = 0; i < prop.length; i++) { var obj = prop[i]; - // remove the key - delete obj.key; + // generate a new key. + obj.key = String.CreateGuid(); // Loop through all inner properties: for (var k in obj) { @@ -50,7 +50,7 @@ } } - clipboardService.registerClearPropertyResolver(clearInnerNestedContentPropertiesForStorage, clipboardService.TYPES.RAW) + clipboardService.registerPastePropertyResolver(resolveInnerNestedContentPropertiesForPaste, clipboardService.TYPES.RAW) }]); angular From de4a7eff855efddf343865f3f93e350075f172cb Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Mon, 12 Apr 2021 23:19:32 +1000 Subject: [PATCH 096/289] Fixes some property editors ensuring ngModelController(#10013) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Niels Lyngsø --- src/Umbraco.Web.UI.Client/package-lock.json | 376 +++++++++--------- .../components/buttons/umbtoggle.directive.js | 2 + .../property/umbproperty.directive.js | 6 + .../validation/valformmanager.directive.js | 5 +- .../validation/valpropertymsg.directive.js | 9 +- .../boolean/boolean.controller.js | 10 +- .../propertyeditors/boolean/boolean.html | 24 +- .../colorpicker/colorpicker.controller.js | 2 +- .../colorpicker/colorpicker.html | 24 +- .../multicolorpicker.controller.js | 12 +- .../contentpicker/contentpicker.controller.js | 18 +- .../contentpicker/contentpicker.html | 20 +- .../datepicker/datepicker.controller.js | 9 +- .../imagecropper/imagecropper.controller.js | 18 +- .../imagecropper/imagecropper.html | 3 + .../markdowneditor.controller.js | 10 +- .../markdowneditor/markdowneditor.html | 9 +- .../mediapicker/mediapicker.controller.js | 42 +- .../mediapicker/mediapicker.html | 14 +- .../membergrouppicker.controller.js | 15 +- .../membergrouppicker/membergrouppicker.html | 19 +- .../membergroups/membergroups.controller.js | 8 + .../membergroups/membergroups.html | 5 + .../memberpicker/memberpicker.controller.js | 15 +- .../memberpicker/memberpicker.html | 21 +- .../multiurlpicker.controller.js | 16 +- .../multiurlpicker/multiurlpicker.html | 8 +- .../nestedcontent/nestedcontent.controller.js | 7 +- .../slider/slider.controller.js | 10 +- .../views/propertyeditors/slider/slider.html | 18 +- .../userpicker/userpicker.controller.js | 10 +- .../userpicker/userpicker.html | 29 +- 32 files changed, 460 insertions(+), 334 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 4245cf3b23..e17a6fe57c 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -104,7 +104,7 @@ "@babel/helper-annotate-as-pure": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz", - "integrity": "sha1-Mj053QtQ4Qx8Bsp9djjmhk2MXDI=", + "integrity": "sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==", "dev": true, "requires": { "@babel/types": "^7.0.0" @@ -113,7 +113,7 @@ "@babel/helper-builder-binary-assignment-operator-visitor": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz", - "integrity": "sha1-a2lijf5Ah3mODE7Zjj1Kay+9L18=", + "integrity": "sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==", "dev": true, "requires": { "@babel/helper-explode-assignable-expression": "^7.1.0", @@ -145,7 +145,7 @@ "@babel/helper-explode-assignable-expression": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz", - "integrity": "sha1-U3+hP28WdN90WwwA7I/k6ZaByPY=", + "integrity": "sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==", "dev": true, "requires": { "@babel/traverse": "^7.1.0", @@ -155,7 +155,7 @@ "@babel/helper-function-name": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", - "integrity": "sha1-oM6wFoX3M1XUNgwSR/WCv6/I/1M=", + "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.0.0", @@ -166,7 +166,7 @@ "@babel/helper-get-function-arity": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", - "integrity": "sha1-g1ctQyDipGVyY3NBE8QoaLZOScM=", + "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", "dev": true, "requires": { "@babel/types": "^7.0.0" @@ -193,7 +193,7 @@ "@babel/helper-module-imports": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", - "integrity": "sha1-lggbcRHkhtpNLNlxrRpP4hbMLj0=", + "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", "dev": true, "requires": { "@babel/types": "^7.0.0" @@ -216,7 +216,7 @@ "@babel/helper-optimise-call-expression": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz", - "integrity": "sha1-opIMVwKwc8Fd5REGIAqoytIEl9U=", + "integrity": "sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==", "dev": true, "requires": { "@babel/types": "^7.0.0" @@ -225,7 +225,7 @@ "@babel/helper-plugin-utils": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", - "integrity": "sha1-u7P77phmHFaQNCN8wDlnupm08lA=", + "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", "dev": true }, "@babel/helper-regex": { @@ -240,7 +240,7 @@ "@babel/helper-remap-async-to-generator": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz", - "integrity": "sha1-Nh2AghtvONp1vT8HheziCojF/n8=", + "integrity": "sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.0.0", @@ -265,7 +265,7 @@ "@babel/helper-simple-access": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz", - "integrity": "sha1-Ze65VMjCRb6qToWdphiPOdceWFw=", + "integrity": "sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==", "dev": true, "requires": { "@babel/template": "^7.1.0", @@ -991,7 +991,7 @@ "accord": { "version": "0.29.0", "resolved": "https://registry.npmjs.org/accord/-/accord-0.29.0.tgz", - "integrity": "sha1-t0HBdtAENcWSnUZt/oz2vukzseQ=", + "integrity": "sha512-3OOR92FTc2p5/EcOzPcXp+Cbo+3C15nV9RXHlOUBCBpHhcB+0frbSNR9ehED/o7sTcyGVtqGJpguToEdlXhD0w==", "dev": true, "requires": { "convert-source-map": "^1.5.0", @@ -1027,7 +1027,7 @@ "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -1038,7 +1038,7 @@ "ace-builds": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.2.tgz", - "integrity": "sha1-avwuQ6e17/3ETYQHQ2EShSVo6A0=" + "integrity": "sha512-M1JtZctO2Zg+1qeGUFZXtYKsyaRptqQtqpVzlj80I0NzGW9MF3um0DBuizIvQlrPYUlTdm+wcOPZpZoerkxQdA==" }, "acorn": { "version": "7.1.0", @@ -1136,7 +1136,7 @@ "angular-animate": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/angular-animate/-/angular-animate-1.7.5.tgz", - "integrity": "sha1-H/xsKpze4ieiunnMbNj3HsRNtdw=" + "integrity": "sha512-kU/fHIGf2a4a3bH7E1tzALTHk+QfoUSCK9fEcMFisd6ZWvNDwPzXWAilItqOC3EDiAXPmGHaNc9/aXiD9xrAxQ==" }, "angular-aria": { "version": "1.7.9", @@ -1166,12 +1166,12 @@ "angular-cookies": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/angular-cookies/-/angular-cookies-1.7.5.tgz", - "integrity": "sha1-HFqzwFzcQ/F3e+lQbmRYfLNUNjQ=" + "integrity": "sha512-/8xvvSl/Z9Vwu8ChRm+OQE3vmli8Icwl8uTYkHqD7j7cknJP9kNaf7SgsENlsLVtOqLE/I7TCFYrSx3bmSeNQA==" }, "angular-dynamic-locale": { "version": "0.1.37", "resolved": "https://registry.npmjs.org/angular-dynamic-locale/-/angular-dynamic-locale-0.1.37.tgz", - "integrity": "sha1-fon70uxFvdaryJ82zaiJODjkk1Q=", + "integrity": "sha512-m5Kyk8W8/mOZSqRxuByOwHBjv8labLBAgvl0Z3iQx2xT/tWCqb94imKUPwumudszdPDjxeopwyucQvm8Sw7ogw==", "requires": { "@types/angular": "^1.6.25" } @@ -1179,7 +1179,7 @@ "angular-i18n": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/angular-i18n/-/angular-i18n-1.7.5.tgz", - "integrity": "sha1-Lie2Thl3qMa2sFHFHQF1xtTcglI=" + "integrity": "sha512-52+Jpt8HRJV2bqSbSU6fWkwOvGzj/DxbNpKXxnTuCS9heuJrlm77BS/lhrF4BA8+Uudnh7npr5/yRELobP+8Yw==" }, "angular-local-storage": { "version": "0.7.1", @@ -1189,32 +1189,32 @@ "angular-messages": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/angular-messages/-/angular-messages-1.7.5.tgz", - "integrity": "sha1-fC/XgTFaQ6GYOLEX2gFCqYhFThQ=" + "integrity": "sha512-YDpJpFLyrIgZjE/sIAjgww1y6r3QqXBJbNDI0QjftD37vHXLkwvAOo3A4bxPw8BikyGLcJrFrgf6hRAzntJIWA==" }, "angular-mocks": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/angular-mocks/-/angular-mocks-1.7.5.tgz", - "integrity": "sha1-yLq6WgbtYLk0aXAmtJIWliavOEs=" + "integrity": "sha512-I+Ue2Bkx6R9W5178DYrNvzjIdGh4wKKoCWsgz8dc7ysH4mA70Q3M9v5xRF0RUu7r+2CZj+nDeUecvh2paxcYvg==" }, "angular-route": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/angular-route/-/angular-route-1.7.5.tgz", - "integrity": "sha1-NKNkjEB6FKAw0HXPSFMY4zuiPw4=" + "integrity": "sha512-7KfyEVVOWTI+jTY/j5rUNCIHGRyeCOx7YqZI/Ci3IbDK7GIsy6xH+hS5ai0Xi0sLjzDZ0PUDO4gBn+K0dVtlOg==" }, "angular-sanitize": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/angular-sanitize/-/angular-sanitize-1.7.5.tgz", - "integrity": "sha1-ddSeFQccqccFgedtIJQPJjcuJNI=" + "integrity": "sha512-wjKCJOIwrkEvfD0keTnKGi6We13gtoCAQIHcdoqyoo3gwvcgNfYymVQIS3+iCGVcjfWz0jHuS3KgB4ysRWsTTA==" }, "angular-touch": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/angular-touch/-/angular-touch-1.7.5.tgz", - "integrity": "sha1-7SYyKmhfApmyPLauqYNMEZQk2kY=" + "integrity": "sha512-XNAZNG0RA1mtdwBJheViCF1H/7wOygp4MLIfs5y1K+rne6AeaYKZcV6EJs9fvgfLKLO6ecm1+3J8hoCkdhhxQw==" }, "angular-ui-sortable": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/angular-ui-sortable/-/angular-ui-sortable-0.19.0.tgz", - "integrity": "sha1-SsQ5H8TU3lcRDbS10xp8GY0xT9A=", + "integrity": "sha512-u/uc981Nzg4XN1bMU9qKleMTSt7F1XjMWnyGw6gxPLIeQeLZm8jWNy7tj8y2r2HmvzXFbQVq2z6rObznFKAekQ==", "requires": { "angular": ">=1.2.x", "jquery": ">=3.1.x", @@ -1277,7 +1277,7 @@ "ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha1-QfuyAkPlCxK+DwS43tvwdSDOhB0=", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { "color-convert": "^1.9.0" @@ -1556,13 +1556,13 @@ "array-slice": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", - "integrity": "sha1-42jqFfibxwaff/uJrsOmx9SsItQ=", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", "dev": true }, "array-sort": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-1.0.0.tgz", - "integrity": "sha1-5MBTVkU/VvU1EqfR1hI/LFTAqIo=", + "integrity": "sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==", "dev": true, "requires": { "default-compare": "^1.0.0", @@ -1573,7 +1573,7 @@ "kind-of": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha1-cpyR4thXt6QZofmqZWhcTDP1hF0=", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", "dev": true } } @@ -1599,7 +1599,7 @@ "arraybuffer.slice": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", - "integrity": "sha1-O7xCdd1YTMGxCAm4nU6LY6aednU=", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", "dev": true }, "asap": { @@ -1612,7 +1612,7 @@ "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha1-jSR136tVO7M+d7VOWeiAu4ziMTY=", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", "dev": true, "requires": { "safer-buffer": "~2.1.0" @@ -1633,7 +1633,7 @@ "astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha1-bIw/uCfdQ+45GPJ7gngqt2WKb9k=", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, "async": { @@ -1695,7 +1695,7 @@ "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha1-bZUX654DDSQ2ZmZR6GvZ9vE1M8k=", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, "autoprefixer": { @@ -1730,7 +1730,7 @@ "aws4": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha1-8OAD2cqef1nHpQiUXXsu+aBKVC8=", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", "dev": true }, "babel-plugin-dynamic-import-node": { @@ -2170,7 +2170,7 @@ "blob": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", - "integrity": "sha1-1oDu7yX4zZGtUz9bAe7UjmTK9oM=", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==", "dev": true }, "bluebird": { @@ -2200,7 +2200,7 @@ "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "requires": { "ms": "2.0.0" @@ -2345,7 +2345,7 @@ "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8=", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, "bufferstreams": { @@ -2460,7 +2460,7 @@ }, "callsites": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "resolved": "http://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", "dev": true }, @@ -2493,7 +2493,7 @@ "caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha1-Xk2Q4idJYdRikZl99Znj7QCO5MA=", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", "dev": true, "requires": { "browserslist": "^4.0.0", @@ -2551,7 +2551,7 @@ "chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha1-kAlISfCTfy7twkJdDSip5fDLrZ4=", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, "chart.js": { @@ -2603,7 +2603,7 @@ "anymatch": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha1-vLJLTzeTTZqnrBe0ra+J58du8us=", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", "dev": true, "requires": { "micromatch": "^3.1.4", @@ -2664,7 +2664,7 @@ "clean-css": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", - "integrity": "sha1-LUEe92uFabbQyEBo2r6FsKpeXBc=", + "integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==", "dev": true, "requires": { "source-map": "~0.6.0" @@ -2673,7 +2673,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true } } @@ -2710,7 +2710,7 @@ "clipboard": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.4.tgz", - "integrity": "sha1-g22v1mzw/qXXHOXVsL9ulYAJES0=", + "integrity": "sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ==", "requires": { "good-listener": "^1.2.2", "select": "^1.1.2", @@ -2877,7 +2877,7 @@ "color-string": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", - "integrity": "sha1-ybvF8BtYtUkvPWhXRZy2WQziBMw=", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", "dev": true, "requires": { "color-name": "^1.0.0", @@ -2985,7 +2985,7 @@ "concat-stream": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha1-kEvfGUzTEi/Gdcd/xKw9T/D9GjQ=", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "dev": true, "requires": { "buffer-from": "^1.0.0", @@ -3018,7 +3018,7 @@ "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { "safe-buffer": "~5.1.0" @@ -3069,7 +3069,7 @@ "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "requires": { "ms": "2.0.0" @@ -3112,13 +3112,13 @@ "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", "dev": true }, "convert-source-map": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha1-UbU3qMQ+DwTewZk7/83VBOdYrCA=", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", "dev": true, "requires": { "safe-buffer": "~5.1.1" @@ -3139,7 +3139,7 @@ "copy-props": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.4.tgz", - "integrity": "sha1-k7scrfr9MdpbuKnUtB9HHsOnLf4=", + "integrity": "sha512-7cjuUME+p+S3HZlbllgsn2CDwS+5eCCX16qBgNC4jgSTf49qR1VKy/Zhl400m0IQXl/bPGEVqncgUUMjrr4s8A==", "dev": true, "requires": { "each-props": "^1.3.0", @@ -3211,7 +3211,7 @@ "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha1-Sl7Hxk364iw6FBJNus3uhG2Ay8Q=", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", "dev": true, "requires": { "nice-try": "^1.0.4", @@ -3250,7 +3250,7 @@ "css-declaration-sorter": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", - "integrity": "sha1-wZiUD2OnbX42wecQGLABchBUyyI=", + "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", "dev": true, "requires": { "postcss": "^7.0.1", @@ -3260,7 +3260,7 @@ "css-select": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.0.2.tgz", - "integrity": "sha1-q0OGzsnh9miFVWSxfDcztDsqXt4=", + "integrity": "sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==", "dev": true, "requires": { "boolbase": "^1.0.0", @@ -3272,7 +3272,7 @@ "css-select-base-adapter": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", - "integrity": "sha1-Oy/0lyzDYquIVhUHqVQIoUMhNdc=", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", "dev": true }, "css-tree": { @@ -3300,7 +3300,7 @@ "cssesc": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", - "integrity": "sha1-OxO9G7HLNuG8taTc0n9UxdyzVwM=", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", "dev": true }, "cssnano": { @@ -3368,7 +3368,7 @@ "cssnano-util-raw-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", - "integrity": "sha1-sm1f1fcqEd/np4RvtMZyYPlr8oI=", + "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", "dev": true, "requires": { "postcss": "^7.0.0" @@ -3377,13 +3377,13 @@ "cssnano-util-same-parent": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", - "integrity": "sha1-V0CC+yhZ0ttDOFWDXZqEVuoYu/M=", + "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", "dev": true }, "csso": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz", - "integrity": "sha1-e564vmFiiXPBsmHhadLwJACOdYs=", + "integrity": "sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==", "dev": true, "requires": { "css-tree": "1.0.0-alpha.29" @@ -3392,7 +3392,7 @@ "css-tree": { "version": "1.0.0-alpha.29", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.29.tgz", - "integrity": "sha1-P6nU7zFCy9HDAedmTB81K9gvWjk=", + "integrity": "sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==", "dev": true, "requires": { "mdn-data": "~1.1.0", @@ -3705,7 +3705,7 @@ "default-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", - "integrity": "sha1-y2ETGESthNhHiPto/QFoHKd4Gi8=", + "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", "dev": true, "requires": { "kind-of": "^5.0.2" @@ -3714,7 +3714,7 @@ "kind-of": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha1-cpyR4thXt6QZofmqZWhcTDP1hF0=", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", "dev": true } } @@ -3728,7 +3728,7 @@ "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha1-z4jabL7ib+bbcJT2HYcMvYTO6fE=", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "dev": true, "requires": { "object-keys": "^1.0.12" @@ -3784,7 +3784,7 @@ "delegate": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", - "integrity": "sha1-tmtxwxWFIuirV0T3INjKDCr1kWY=" + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" }, "depd": { "version": "1.1.2", @@ -3824,7 +3824,7 @@ "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha1-gAwN0eCov7yVg1wgKtIg/jF+WhI=" + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" }, "dir-glob": { "version": "3.0.1", @@ -3917,7 +3917,7 @@ "domutils": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha1-Vuo0HoNOBuZ0ivehyyXaZ+qfjCo=", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", "dev": true, "requires": { "dom-serializer": "0", @@ -3927,7 +3927,7 @@ "dot-prop": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha1-HxngwuGqDjJ5fEl5nyg3rGr2nFc=", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", "dev": true, "requires": { "is-obj": "^1.0.0" @@ -4042,7 +4042,7 @@ "each-props": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", - "integrity": "sha1-6kWkFNFt1c+kGbGoFyDVygaJIzM=", + "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", "dev": true, "requires": { "is-plain-object": "^2.0.1", @@ -4121,7 +4121,7 @@ "engine.io": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.1.tgz", - "integrity": "sha1-tgKBw1SEpw7gNR6g6/+D7IyVIqI=", + "integrity": "sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==", "dev": true, "requires": { "accepts": "~1.3.4", @@ -4135,7 +4135,7 @@ "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "dev": true, "requires": { "ms": "2.0.0" @@ -4177,7 +4177,7 @@ "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "dev": true, "requires": { "ms": "2.0.0" @@ -4194,7 +4194,7 @@ "engine.io-parser": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz", - "integrity": "sha1-dXq5cPvy37Mse3SwMyFtVznveaY=", + "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==", "dev": true, "requires": { "after": "0.8.2", @@ -4235,7 +4235,7 @@ "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha1-tKxAZIEH/c3PriQvQovqihTU8b8=", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, "requires": { "is-arrayish": "^0.2.1" @@ -4262,7 +4262,7 @@ "es-to-primitive": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha1-7fckeAM0VujdqO8J4ArZZQcH83c=", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", "dev": true, "requires": { "is-callable": "^1.1.4", @@ -4501,13 +4501,13 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha1-E7BM2z5sXRnfkatph6hpVhmwqnE=", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true }, "esquery": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha1-QGxRZYsfWZGl+bYrHcJbAOPlxwg=", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", "dev": true, "requires": { "estraverse": "^4.0.0" @@ -4516,7 +4516,7 @@ "esrecurse": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha1-AHo7n9vCs7uH5IeeoZyS/b05Qs8=", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", "dev": true, "requires": { "estraverse": "^4.1.0" @@ -4649,7 +4649,7 @@ "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "requires": { "ms": "2.0.0" @@ -4771,7 +4771,7 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, "extend-shallow": { @@ -4880,7 +4880,7 @@ "fancy-log": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", - "integrity": "sha1-28GRVPVYaQFQojlToK29A1vkX8c=", + "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", "dev": true, "requires": { "ansi-gray": "^0.1.1", @@ -5092,7 +5092,7 @@ "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "requires": { "ms": "2.0.0" @@ -5233,7 +5233,7 @@ "flatpickr": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.5.2.tgz", - "integrity": "sha1-R8itRyoJbl+350uAmwcDU1OD8g0=" + "integrity": "sha512-jDy4QYGpmiy7+Qk8QvKJ4spjDdxcx9cxMydmq1x427HkKWBw0qizLYeYM2F6tMcvvqGjU5VpJS55j4LnsaBblA==" }, "flatted": { "version": "2.0.1", @@ -5332,7 +5332,7 @@ "form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha1-3M5SwF9kTymManq5Nr1yTO/786Y=", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "dev": true, "requires": { "asynckit": "^0.4.0", @@ -5996,7 +5996,7 @@ "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha1-+Xj6TJDR3+f/LWvtoqUV5xO9z0o=", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", "dev": true }, "get-proxy": { @@ -6229,7 +6229,7 @@ "global-modules": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha1-bXcPDrUjrHgWTXK15xqIdyZcw+o=", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", "dev": true, "requires": { "global-prefix": "^1.0.1", @@ -6381,7 +6381,7 @@ "gulp-babel": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/gulp-babel/-/gulp-babel-8.0.0.tgz", - "integrity": "sha1-4NqW9PLsSojdOjAw9HbjirISbYc=", + "integrity": "sha512-oomaIqDXxFkg7lbpBou/gnUkX51/Y/M2ZfSjL2hdqXTAlSWZcgZtd2o0cOH0r/eE8LWD0+Q/PsLsr2DKOoqToQ==", "dev": true, "requires": { "plugin-error": "^1.0.1", @@ -6505,7 +6505,7 @@ "vinyl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha1-2FsH2pbkWNJbL/4Z/s6fLKoT7YY=", + "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", "dev": true, "requires": { "clone": "^2.1.1", @@ -6551,7 +6551,7 @@ "gulp-less": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/gulp-less/-/gulp-less-4.0.1.tgz", - "integrity": "sha1-NIwzpd3nogfFdxsdgmHRrBAhzu0=", + "integrity": "sha512-hmM2k0FfQp7Ptm3ZaqO2CkMX3hqpiIOn4OHtuSsCeFym63F7oWlEua5v6u1cIjVUKYsVIs9zPg9vbqTEb/udpA==", "dev": true, "requires": { "accord": "^0.29.0", @@ -6596,7 +6596,7 @@ }, "kind-of": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", "dev": true }, @@ -6630,7 +6630,7 @@ "gulp-notify": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/gulp-notify/-/gulp-notify-3.2.0.tgz", - "integrity": "sha1-KugiUAnfiB7vWb5d1aLxM3OHdk4=", + "integrity": "sha512-qEocs1UVoDKKUjfsxJNMNwkRla0PbsyJwsqNNXpzYWsLQ29LhxRMY3wnTGZcc4hMHtalnvah/Dwlwb4NijH/0A==", "dev": true, "requires": { "ansi-colors": "^1.0.1", @@ -6716,7 +6716,7 @@ "gulp-postcss": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/gulp-postcss/-/gulp-postcss-8.0.0.tgz", - "integrity": "sha1-jTdyzU0nvKVeyMtMjlduO95NxVA=", + "integrity": "sha512-Wtl6vH7a+8IS/fU5W9IbOpcaLqKxd5L1DUOzaPmlnCbX1CrG0aWdwVnC3Spn8th0m8D59YbysV5zPUe1n/GJYg==", "dev": true, "requires": { "fancy-log": "^1.3.2", @@ -6729,7 +6729,7 @@ "gulp-rename": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-1.4.0.tgz", - "integrity": "sha1-3hxxjnxAla6GH3KW708ySGSCQL0=", + "integrity": "sha512-swzbIGb/arEoFK89tPY58vg3Ok1bw+d35PfUNwWqdo7KM4jkmuGA78JiDNqR+JeZFaeeHnRg9N7aihX3YPmsyg==", "dev": true }, "gulp-sort": { @@ -6808,7 +6808,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -6830,7 +6830,7 @@ "gulp-watch": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/gulp-watch/-/gulp-watch-5.0.1.tgz", - "integrity": "sha1-g9N4dS9b+0baAj5zwX7R2nBmIV0=", + "integrity": "sha512-HnTSBdzAOFIT4wmXYPDUn783TaYAq9bpaN05vuZNP5eni3z3aRx0NAKbjhhMYtcq76x4R1wf4oORDGdlrEjuog==", "dev": true, "requires": { "ansi-colors": "1.1.0", @@ -6915,7 +6915,7 @@ "vinyl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha1-2FsH2pbkWNJbL/4Z/s6fLKoT7YY=", + "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", "dev": true, "requires": { "clone": "^2.1.1", @@ -7041,7 +7041,7 @@ "har-validator": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha1-HvievT5JllV2de7ZiTEQ3DUPoIA=", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", "dev": true, "requires": { "ajv": "^6.5.5", @@ -7051,7 +7051,7 @@ "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha1-ci18v8H2qoJB8W3YFOAR4fQeh5Y=", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "requires": { "function-bind": "^1.1.1" @@ -7069,7 +7069,7 @@ "has-binary2": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", - "integrity": "sha1-d3asYn8+p3JQz8My2rfd9eT10R0=", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", "dev": true, "requires": { "isarray": "2.0.1" @@ -7162,7 +7162,7 @@ "hex-color-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", - "integrity": "sha1-TAb8y0YC/iYCs8k9+C1+fb8aio4=", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", "dev": true }, "homedir-polyfill": { @@ -7195,7 +7195,7 @@ "html-comment-regex": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", - "integrity": "sha1-l9RoiutcgYhqNk+qDK0d2hTUM6c=", + "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", "dev": true }, "html-encoding-sniffer": { @@ -7312,7 +7312,7 @@ "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha1-ICK0sl+93CHS9SSXSkdKr+czkIs=", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" @@ -7328,7 +7328,7 @@ "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha1-dQ49tYYgh7RzfrrIIH/9HvJ7Jfw=", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, "image-size": { @@ -7495,7 +7495,7 @@ "ini": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc=", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true }, "inquirer": { @@ -7591,7 +7591,7 @@ "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha1-YQ88ksk1nOHbYW5TgAjSP/NRWOY=", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", "dev": true, "requires": { "loose-envify": "^1.0.0" @@ -7624,7 +7624,7 @@ "is-absolute": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha1-OV4a6EsR8mrReV5zwXN45IowFXY=", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", "dev": true, "requires": { "is-relative": "^1.0.0", @@ -7935,7 +7935,7 @@ "is-relative": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha1-obtpNc6MXboei5dUubLcwCDiJg0=", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", "dev": true, "requires": { "is-unc-path": "^1.0.0" @@ -7944,7 +7944,7 @@ "is-resolvable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha1-+xj4fOH+uSUWnJpAfBkxijIG7Yg=", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", "dev": true }, "is-retry-allowed": { @@ -7964,7 +7964,7 @@ "is-svg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz", - "integrity": "sha1-kyHb0pwhLlypnE+peUxxS8r6L3U=", + "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==", "dev": true, "requires": { "html-comment-regex": "^1.1.0" @@ -7973,7 +7973,7 @@ "is-symbol": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha1-oFX2rlcZLK7jKeeoYBGLSXqVDzg=", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", "dev": true, "requires": { "has-symbols": "^1.0.0" @@ -7988,7 +7988,7 @@ "is-unc-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha1-1zHoiY7QkKEsNSrS6u1Qla0yLJ0=", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", "dev": true, "requires": { "unc-path-regex": "^0.1.2" @@ -8027,7 +8027,7 @@ "isbinaryfile": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz", - "integrity": "sha1-XW3vPt6/boyoyunDAYOoBLX4voA=", + "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", "dev": true, "requires": { "buffer-alloc": "^1.2.0" @@ -8104,7 +8104,7 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha1-GSA/tZmR35jjoocFDUZHzerzJJk=", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, "js-yaml": { @@ -8245,7 +8245,7 @@ "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha1-gFZNLkg9rPbo7yCWUKZ98/DCg6Q=", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, "json-buffer": { @@ -8258,7 +8258,7 @@ "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha1-u4Z8+zRQ5pEHwTHRxRS6s9yLyqk=", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, "json-schema": { @@ -8270,7 +8270,7 @@ "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, "json-stable-stringify-without-jsonify": { @@ -8483,7 +8483,7 @@ "karma-jasmine": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-2.0.1.tgz", - "integrity": "sha1-JuPjHy+vJy3YDrsOGJiRTMOhl2M=", + "integrity": "sha512-iuC0hmr9b+SNn1DaUD2QEYtUxkS1J+bSJSn7ejdEexs7P8EYvA1CWkEdrDQ+8jVH3AgWlCNwjYsT1chjcNW9lA==", "dev": true, "requires": { "jasmine-core": "^3.3" @@ -8645,7 +8645,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "optional": true } @@ -8948,7 +8948,7 @@ "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha1-ce5R+nvkyuwaY4OffmgtgTLTDK8=", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -9024,7 +9024,7 @@ "make-iterator": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", - "integrity": "sha1-KbM/MSqo9UfEpeSQ9Wr87JkTOtY=", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", "dev": true, "requires": { "kind-of": "^6.0.2" @@ -9204,7 +9204,7 @@ "mimic-fn": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha1-ggyGo5M0ZA6ZUWkovQP8qIBX0CI=", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", "dev": true }, "mimic-response": { @@ -9225,7 +9225,7 @@ }, "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, @@ -9276,7 +9276,7 @@ "dependencies": { "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true } @@ -9305,7 +9305,7 @@ "mute-stdout": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", - "integrity": "sha1-rLAwDrTeI6fd7sAU4+lgRLNHIzE=", + "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", "dev": true }, "mute-stream": { @@ -9324,7 +9324,7 @@ "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha1-uHqKpPwN6P5r6IiVs4mD/yZb0Rk=", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", "dev": true, "requires": { "arr-diff": "^4.0.0", @@ -9354,7 +9354,7 @@ }, "next-tick": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, @@ -9366,7 +9366,7 @@ "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha1-ozeKdpbOfSI+iPybdkvX7xCJ42Y=", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, "node-notifier": { @@ -9431,7 +9431,7 @@ "normalize-url": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", - "integrity": "sha1-suHE3E98bVd0PfczpPWXjRhlBVk=", + "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", "dev": true }, "nouislider": { @@ -12575,7 +12575,7 @@ "nth-check": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha1-sr0pXDfj3VijvwcAN2Zjuk2c8Fw=", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", "dev": true, "requires": { "boolbase": "~1.0.0" @@ -12602,7 +12602,7 @@ "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha1-R6ewFrqmi1+g7PPe4IqFxnmsZFU=", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", "dev": true }, "object-assign": { @@ -12804,7 +12804,7 @@ "dependencies": { "minimist": { "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", "dev": true }, @@ -12895,7 +12895,7 @@ }, "os-locale": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", "dev": true, "requires": { @@ -13117,7 +13117,7 @@ "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha1-1i27VnlAXXLEc37FhgDp3c8G0kw=", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, "path-root": { @@ -13175,7 +13175,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, @@ -13197,7 +13197,7 @@ "plugin-error": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", - "integrity": "sha1-dwFr2JGdCsN3/c3QMiMolTyleBw=", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", "dev": true, "requires": { "ansi-colors": "^1.0.1", @@ -13235,7 +13235,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, "supports-color": { @@ -13252,7 +13252,7 @@ "postcss-calc": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.1.tgz", - "integrity": "sha1-Ntd7qwI7Dsu5eJ2E3LI8SUEUVDY=", + "integrity": "sha512-oXqx0m6tb4N3JGdmeMSc/i91KppbYsFZKdH0xMOqK8V1rJlzrKlTdokz8ozUXLVejydRN6u2IddxpcijRj2FqQ==", "dev": true, "requires": { "css-unit-converter": "^1.1.1", @@ -13277,7 +13277,7 @@ "postcss-convert-values": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", - "integrity": "sha1-yjgT7U2g+BL51DcDWE5Enr4Ymn8=", + "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", "dev": true, "requires": { "postcss": "^7.0.0", @@ -13296,7 +13296,7 @@ "postcss-discard-duplicates": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", - "integrity": "sha1-P+EzzTyCKC5VD8myORdqkge3hOs=", + "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", "dev": true, "requires": { "postcss": "^7.0.0" @@ -13305,7 +13305,7 @@ "postcss-discard-empty": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", - "integrity": "sha1-yMlR6fc+2UKAGUWERKAq2Qu592U=", + "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", "dev": true, "requires": { "postcss": "^7.0.0" @@ -13314,7 +13314,7 @@ "postcss-discard-overridden": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", - "integrity": "sha1-ZSrvipZybwKfXj4AFG7npOdV/1c=", + "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", "dev": true, "requires": { "postcss": "^7.0.0" @@ -13372,7 +13372,7 @@ "postcss-minify-font-values": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", - "integrity": "sha1-zUw0TM5HQ0P6xdgiBqssvLiv1aY=", + "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", "dev": true, "requires": { "postcss": "^7.0.0", @@ -13433,7 +13433,7 @@ "postcss-normalize-charset": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", - "integrity": "sha1-izWt067oOhNrBHHg1ZvlilAoXdQ=", + "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", "dev": true, "requires": { "postcss": "^7.0.0" @@ -13499,7 +13499,7 @@ "postcss-normalize-unicode": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", - "integrity": "sha1-hBvUj9zzAZrUuqdJOj02O1KuHPs=", + "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", "dev": true, "requires": { "browserslist": "^4.0.0", @@ -13510,7 +13510,7 @@ "postcss-normalize-url": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", - "integrity": "sha1-EOQ3+GvHx+WPe5ZS7YeNqqlfquE=", + "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", "dev": true, "requires": { "is-absolute-url": "^2.0.0", @@ -13590,7 +13590,7 @@ "postcss-unique-selectors": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", - "integrity": "sha1-lEaRHzKJv9ZMbWgPBzwDsfnuS6w=", + "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", "dev": true, "requires": { "alphanum-sort": "^1.0.0", @@ -13601,7 +13601,7 @@ "postcss-value-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha1-n/giVH4okyE88cMO+lGsX9G6goE=", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", "dev": true }, "prelude-ls": { @@ -13631,14 +13631,14 @@ }, "pretty-hrtime": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "resolved": "http://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", "dev": true }, "private": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha1-I4Hts2ifelPWUxkAYPz4ItLzaP8=", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", "dev": true }, "process-nextick-args": { @@ -13713,7 +13713,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha1-tYsBCsQMIsVldhbI0sLALHv0eew=", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, "q": { @@ -13725,7 +13725,7 @@ "qjobs": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", - "integrity": "sha1-xF6cYYAL0IfviNfiVkI73Unl0HE=", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", "dev": true }, "qs": { @@ -13758,7 +13758,7 @@ "randomatic": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", - "integrity": "sha1-t3bvxZN1mE42xTey9RofCv8Noe0=", + "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", "dev": true, "requires": { "is-number": "^4.0.0", @@ -13769,7 +13769,7 @@ "is-number": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha1-ACbjf1RU1z41bf5lZGmYZ8an8P8=", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", "dev": true } } @@ -13828,7 +13828,7 @@ "readdirp": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha1-DodiKjMlqjPokihcr4tOhGUppSU=", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", "dev": true, "requires": { "graceful-fs": "^4.1.11", @@ -13866,7 +13866,7 @@ "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { "safe-buffer": "~5.1.0" @@ -13897,7 +13897,7 @@ "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha1-SoVuxLVuQHfFV1icroXnpMiGmhE=", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", "dev": true }, "regenerate-unicode-properties": { @@ -13940,7 +13940,7 @@ "regexpp": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha1-jRnTHPYySCtYkEn4KB+T28uk0H8=", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", "dev": true }, "regexpu-core": { @@ -14010,7 +14010,7 @@ "repeat-element": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha1-eC4NglwMWjuzlzH4Tv7mt0Lmsc4=", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", "dev": true }, "repeat-string": { @@ -14049,7 +14049,7 @@ "request": { "version": "2.88.0", "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha1-nC/KT301tZLv5Xx/ClXoEFIST+8=", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", "dev": true, "optional": true, "requires": { @@ -14289,7 +14289,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -14311,7 +14311,7 @@ }, "kind-of": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", "dev": true }, @@ -14363,7 +14363,7 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, "sax": { @@ -14481,7 +14481,7 @@ "shellwords": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha1-1rkYHBpI05cyTISHHvvPxz/AZUs=", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", "dev": true }, "signal-exit": { @@ -14493,7 +14493,7 @@ "signalr": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/signalr/-/signalr-2.4.0.tgz", - "integrity": "sha1-kq8AjmtSetSzbpT7s0DhNQh6YNI=", + "integrity": "sha512-GPJHb3pcNk3IUui5/WG8lMuarEn+Vpc8wEvJ60w0KQ43W9FHnJcuNcF8dkZePr81eBslzicsRdyEunKNF7KjZQ==", "requires": { "jquery": ">=1.6.4" } @@ -14510,7 +14510,7 @@ "is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha1-RXSirlb3qyBolvtDHq7tBm/fjwM=", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "dev": true } } @@ -14559,7 +14559,7 @@ "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "requires": { "ms": "2.0.0" @@ -14665,7 +14665,7 @@ "socket.io": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz", - "integrity": "sha1-oGnF/qvuPmshSnW0DOBlLhz7mYA=", + "integrity": "sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==", "dev": true, "requires": { "debug": "~3.1.0", @@ -14679,7 +14679,7 @@ "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "dev": true, "requires": { "ms": "2.0.0" @@ -14702,7 +14702,7 @@ "socket.io-client": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz", - "integrity": "sha1-3LOBA0NqtFeN2wJmOK4vIbYjZx8=", + "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==", "dev": true, "requires": { "backo2": "1.0.2", @@ -14730,7 +14730,7 @@ "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "dev": true, "requires": { "ms": "2.0.0" @@ -14764,7 +14764,7 @@ "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "dev": true, "requires": { "ms": "2.0.0" @@ -14813,7 +14813,7 @@ "source-map-resolve": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha1-cuLMNAlVQ+Q7LGKyxMENSpBU8lk=", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", "dev": true, "requires": { "atob": "^2.1.1", @@ -14832,7 +14832,7 @@ "sparkles": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", - "integrity": "sha1-AI22XtzmxQ7sDF4ijhlFBh3QQ3w=", + "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", "dev": true }, "spdx-correct": { @@ -14908,7 +14908,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "optional": true, @@ -15019,7 +15019,7 @@ "debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha1-6D0X3hbYp++3cX7b5fsQE17uYps=", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "dev": true, "requires": { "ms": "^2.1.1" @@ -15195,7 +15195,7 @@ "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { "has-flag": "^3.0.0" @@ -15287,7 +15287,7 @@ "tar-stream": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", - "integrity": "sha1-jqVdqzeXIlPZqa+Q/c1VmuQ1xVU=", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", "dev": true, "optional": true, "requires": { @@ -15326,7 +15326,7 @@ "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "optional": true, "requires": { @@ -15374,7 +15374,7 @@ "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha1-AcHjnrMdB8t9A6lqcIIyYLIxMs0=", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, "requires": { "readable-stream": "~2.3.6", @@ -15474,7 +15474,7 @@ "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "dev": true, "requires": { "os-tmpdir": "~1.0.2" @@ -15569,7 +15569,7 @@ "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha1-U/Nto/R3g7CSWvoG/587FlKA94E=", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", "dev": true, "requires": { "psl": "^1.1.24", @@ -15734,7 +15734,7 @@ "ultron": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha1-n+FTahCmZKZSZqHjzPhf02MCvJw=", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", "dev": true }, "unbzip2-stream": { @@ -15757,7 +15757,7 @@ "underscore": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", - "integrity": "sha1-BtzjSg5op7q8KbNluOdLiSUgOWE=" + "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==" }, "undertaker": { "version": "1.2.1", @@ -15785,13 +15785,13 @@ "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", - "integrity": "sha1-JhmADEyCWADv3YNDr33Zkzy+KBg=", + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", "dev": true }, "unicode-match-property-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", - "integrity": "sha1-jtKjJWmWG86SJ9Cc0/+7j+1fAgw=", + "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", "dev": true, "requires": { "unicode-canonical-property-names-ecmascript": "^1.0.4", @@ -15917,7 +15917,7 @@ "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha1-lMVA4f93KVbiKZUHwBCupsiDjrA=", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", "dev": true, "requires": { "punycode": "^2.1.0" @@ -15949,7 +15949,7 @@ "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha1-1QyMrHmhn7wg8pEfVuuXP04QBw8=", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, "useragent": { @@ -16008,7 +16008,7 @@ "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha1-/JH2uce6FchX9MssXe/uw51PQQo=", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, "requires": { "spdx-correct": "^3.0.0", @@ -16297,7 +16297,7 @@ "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha1-pFBD1U9YBTFtqNYvn1CRjT2nCwo=", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, "requires": { "isexe": "^2.0.0" @@ -16328,7 +16328,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "requires": { @@ -16354,7 +16354,7 @@ "ws": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", - "integrity": "sha1-8c+E/i1ekB686U767OeF8YeiKPI=", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", "dev": true, "requires": { "async-limiter": "~1.0.0", diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbtoggle.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbtoggle.directive.js index bbda02806e..fb58b4287d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbtoggle.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbtoggle.directive.js @@ -129,6 +129,8 @@ replace: true, templateUrl: 'views/components/buttons/umb-toggle.html', scope: { + // TODO: This should have required ngModel so we can track and validate user input correctly + // https://docs.angularjs.org/api/ng/type/ngModel.NgModelController#custom-control-example checked: "=", disabled: "=", inputId: "@", diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js index 2b2f36dd7d..f4cfacbf70 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js @@ -35,6 +35,12 @@ vm.$onInit = onInit; + vm.setDirty = function () { + // NOTE: We need to use scope because we haven't changd it to vm.propertyForm in the html and that + // might mean a breaking change. + $scope.propertyForm.$setDirty(); + } + vm.setPropertyError = function (errorMsg) { vm.property.propertyErrorMessage = errorMsg; }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js index c7894da171..55878db2e9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js @@ -44,9 +44,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location this.isShowingValidation = () => $scope.showValidation === true; - this.notify = function () { - notify($scope); - } + this.notify = notify; this.isValid = function () { return !$scope.formCtrl.$invalid; @@ -112,6 +110,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location }); //watch the list of validation errors to notify the application of any validation changes + // TODO: Wouldn't it be easier/faster to watch formCtrl.$invalid ? scope.$watch(() => angularHelper.countAllFormErrors(formCtrl), function (e) { 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 26c0403f85..f9e2af584f 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 @@ -177,8 +177,9 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel watcher = scope.$watchCollection( () => formCtrl, function (updatedFormController) { - var ngModels = []; - collectAllNgModelControllersRecursively(updatedFormController.$getControls(), ngModels); + let childControls = updatedFormController.$getControls(); + let ngModels = []; + collectAllNgModelControllersRecursively(childControls, ngModels); ngModels.forEach(x => { if (!x.$validators.serverValidityResetter) { x.$validators.serverValidityResetter = resetServerValidityValidator(x); @@ -201,7 +202,7 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel hasError = false; formCtrl.$setValidity('valPropertyMsg', true); scope.errorMsg = ""; - + } // This deals with client side validation changes and is executed anytime validators change on the containing @@ -264,6 +265,8 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel //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. + // TODO: Really? Since valFormManager is watching a countof all errors which is more overhead than watching formCtrl.$invalid + // and there's a TODO there that it should just watch formCtrl.$invalid valFormManager.onValidationStatusChanged(function (evt, args) { checkValidationStatus(); }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js index dab8d2c6f8..018c2b72c1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js @@ -1,4 +1,4 @@ -function booleanEditorController($scope, angularHelper) { +function booleanEditorController($scope) { // Setup the default config // This allow to overwrite the configuration when property editor is re-used @@ -36,6 +36,12 @@ function booleanEditorController($scope, angularHelper) { } } + function setDirty() { + if ($scope.modelValueForm) { + $scope.modelValueForm.modelValue.$setDirty(); + } + } + setupViewModel(); if ($scope.model && !$scope.model.value) { @@ -51,7 +57,7 @@ function booleanEditorController($scope, angularHelper) { // Update the value when the toggle is clicked $scope.toggle = function(){ - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); if ($scope.renderModel.value){ $scope.model.value = $scope.model.config.falsevalue; setupViewModel(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.html index 75f3f5452a..aa47e0c667 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.html @@ -1,11 +1,17 @@
- - + + + + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js index 886d051f4d..8166255a26 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js @@ -63,7 +63,7 @@ function ColorPickerController($scope, $timeout) { // this is required to re-validate $timeout(function () { var newColor = color ? color.value : null; - $scope.propertyForm.selectedColor.$setViewValue(newColor); + vm.modelValueForm.selectedColor.$setViewValue(newColor); }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html index b7942b7f68..cb5c80a6f1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html @@ -1,17 +1,19 @@ 
-
- You haven't defined any colors -
+ +
+ You haven't defined any colors +
- - + + - + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js index 967137b930..cb5905e2c9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js @@ -104,7 +104,7 @@ return x.value === item.value && x.label === item.label; }); - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); } function add(evt) { @@ -130,7 +130,7 @@ $scope.newLabel = ""; $scope.hasError = false; $scope.focusOnNew = true; - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); return; } @@ -156,6 +156,12 @@ $scope.newLabel = defaultLabel; } + function setDirty() { + if (vm.modelValueForm) { + vm.modelValueForm.selectedColor.$setDirty(); + } + } + $scope.sortableOptions = { axis: 'y', containment: 'parent', @@ -164,7 +170,7 @@ items: '> div.control-group', tolerance: 'pointer', update: function (e, ui) { - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); } }; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 0f012810ba..e818fe9a23 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -15,7 +15,7 @@ * @param {any} editorService * @param {any} userService */ -function contentPickerController($scope, $q, $routeParams, $location, entityResource, editorState, iconHelper, angularHelper, navigationService, localizationService, editorService, userService, overlayService) { +function contentPickerController($scope, $q, $routeParams, $location, entityResource, editorState, iconHelper, navigationService, localizationService, editorService, userService, overlayService) { var vm = { labels: { @@ -112,7 +112,7 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso scroll: true, zIndex: 6000, update: function (e, ui) { - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); } }; @@ -180,7 +180,7 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso $scope.clear(); $scope.add(data); } - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); }, treeAlias: $scope.model.config.startNode.type, section: $scope.model.config.startNode.type, @@ -257,9 +257,9 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso _.each(model.selection, function (item, i) { $scope.add(item); }); - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); } - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); editorService.close(); } @@ -288,7 +288,7 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso var currIds = $scope.model.value ? $scope.model.value.split(',') : []; if (currIds.length > 0) { currIds.splice(index, 1); - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); $scope.model.value = currIds.join(); } @@ -375,6 +375,12 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso } }); + function setDirty() { + if ($scope.contentPickerForm && $scope.contentPickerForm.modelValue) { + $scope.contentPickerForm.modelValue.$setDirty(); + } + } + /** Syncs the renderModel based on the actual model.value and returns a promise */ function syncRenderModel(doValidation) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html index 1a17ea2698..8ebbaae91f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html @@ -19,14 +19,12 @@ - @@ -64,16 +62,18 @@ + + -
+
You need to add at least {{model.config.minNumber}} items
-
+
You can only have {{model.config.maxNumber}} items selected
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js index 30b6fc4c8f..af1dea167a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js @@ -161,7 +161,14 @@ function dateTimePickerController($scope, angularHelper, dateHelper, validationM else { $scope.model.value = null; } - angularHelper.getCurrentForm($scope).$setDirty(); + + setDirty(); + } + + function setDirty() { + if ($scope.datePickerForm) { + $scope.datePickerForm.datepicker.$setDirty(); + } } /** Sets the value of the date picker control adn associated viewModel objects based on the model value */ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js index 70c74d0391..4df8f7e596 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js @@ -31,7 +31,7 @@ angular.module('umbraco') }; //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); + setDirty(); } /** @@ -67,7 +67,13 @@ angular.module('umbraco') function onFileSelected(value, files) { setModelValueWithSrc(value); //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); + setDirty(); + } + + function setDirty() { + if ($scope.imageCropperForm) { + $scope.imageCropperForm.modelValue.$setDirty(); + } } function imageLoaded(isCroppable, hasDimensions) { @@ -84,7 +90,7 @@ angular.module('umbraco') if (files && files[0]) { $scope.imageSrc = files[0].fileSrc; //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); + setDirty(); } } @@ -138,7 +144,7 @@ angular.module('umbraco') $scope.currentPoint = null; //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); + setDirty(); } else { // we have a crop open already - close the crop (this will discard any changes made) @@ -168,7 +174,7 @@ angular.module('umbraco') $scope.close(); //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); + setDirty(); }; function reset() { @@ -201,7 +207,7 @@ angular.module('umbraco') } //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); + setDirty(); }; function isCustomCrop(crop) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html index 84ddf7ee3b..241d61660e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html @@ -2,6 +2,9 @@ ng-controller="Umbraco.PropertyEditors.ImageCropperController"> + + + -
- + +
-
+ + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js index f3a57224e2..ca46f30bb7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js @@ -1,7 +1,7 @@ //this controller simply tells the dialogs service to open a mediaPicker window //with a specified callback, this callback will receive an object with a selection on it angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerController", - function ($scope, entityResource, mediaHelper, $timeout, userService, localizationService, editorService, angularHelper, overlayService) { + function ($scope, entityResource, mediaHelper, $timeout, userService, localizationService, editorService, overlayService) { var vm = this; @@ -13,6 +13,9 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl vm.editItem = editItem; vm.showAdd = showAdd; + vm.mediaItems = []; + let selectedIds = []; + //check the pre-values for multi-picker var multiPicker = $scope.model.config.multiPicker && $scope.model.config.multiPicker !== '0' ? true : false; var onlyImages = $scope.model.config.onlyImages && $scope.model.config.onlyImages !== '0' ? true : false; @@ -22,9 +25,6 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl $scope.allowAddMedia = false; function setupViewModel() { - $scope.mediaItems = []; - $scope.ids = []; - $scope.isMultiPicker = multiPicker; if ($scope.model.value) { @@ -77,12 +77,12 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); } - $scope.mediaItems.push(media); + vm.mediaItems.push(media); if ($scope.model.config.idType === "udi") { - $scope.ids.push(media.udi); + selectedIds.push(media.udi); } else { - $scope.ids.push(media.id); + selectedIds.push(media.id); } }); @@ -92,12 +92,14 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl } function sync() { - $scope.model.value = $scope.ids.join(); - removeAllEntriesAction.isDisabled = $scope.ids.length === 0; + $scope.model.value = selectedIds.join(); + removeAllEntriesAction.isDisabled = selectedIds.length === 0; } function setDirty() { - angularHelper.getCurrentForm($scope).$setDirty(); + if (vm.modelValueForm) { + vm.modelValueForm.modelValue.$setDirty(); + } } function reloadUpdatedMediaItems(updatedMediaNodes) { @@ -105,7 +107,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl // reload. We only reload the images that is already picked but has been updated. // We have to get the entities from the server because the media // can be edited without being selected - $scope.mediaItems.forEach(media => { + vm.mediaItems.forEach(media => { if (updatedMediaNodes.indexOf(media.udi) !== -1) { media.loading = true; entityResource.getById(media.udi, "Media") @@ -155,8 +157,8 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl } function remove(index) { - $scope.mediaItems.splice(index, 1); - $scope.ids.splice(index, 1); + vm.mediaItems.splice(index, 1); + selectedIds.splice(index, 1); sync(); setDirty(); } @@ -174,7 +176,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl .then(function (mediaEntity) { // if an image is selecting more than once // we need to update all the media items - $scope.mediaItems.forEach(media => { + vm.mediaItems.forEach(media => { if (media.id === model.mediaNode.id) { angular.extend(media, mediaEntity); media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); @@ -208,13 +210,13 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); } - $scope.mediaItems.push(media); + vm.mediaItems.push(media); if ($scope.model.config.idType === "udi") { - $scope.ids.push(media.udi); + selectedIds.push(media.udi); } else { - $scope.ids.push(media.id); + selectedIds.push(media.id); } }); @@ -250,8 +252,8 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl overlayService.close(); }, submit: function () { - $scope.mediaItems.length = 0;// AngularJS way to empty the array. - $scope.ids.length = 0;// AngularJS way to empty the array. + vm.mediaItems.length = 0;// AngularJS way to empty the array. + selectedIds.length = 0;// AngularJS way to empty the array. sync(); setDirty(); overlayService.close(); @@ -291,7 +293,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl // TODO: Instead of doing this with a timeout would be better to use a watch like we do in the // content picker. Then we don't have to worry about setting ids, render models, models, we just set one and let the // watch do all the rest. - $scope.ids = $scope.mediaItems.map(media => $scope.model.config.idType === "udi" ? media.udi : media.id); + selectedIds = vm.mediaItems.map(media => $scope.model.config.idType === "udi" ? media.udi : media.id); sync(); }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html index c09d7c7613..22a683fa49 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html @@ -1,11 +1,11 @@
-

-

+

+

-
    -
  • +
      +
    • @@ -46,13 +46,15 @@

  • -
  • - + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js index 73def3cc65..5362cb1f10 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js @@ -18,6 +18,12 @@ function memberGroupPicker($scope, editorService, memberGroupResource){ }); } + function setDirty() { + if ($scope.modelValueForm) { + $scope.modelValueForm.modelValue.$setDirty(); + } + } + $scope.openMemberGroupPicker = function() { var memberGroupPicker = { multiPicker: true, @@ -32,6 +38,7 @@ function memberGroupPicker($scope, editorService, memberGroupResource){ if (newGroupIds && newGroupIds.length) { memberGroupResource.getByIds(newGroupIds).then(function (groups) { $scope.renderModel = _.union($scope.renderModel, groups); + setDirty(); editorService.close(); }); } @@ -47,10 +54,13 @@ function memberGroupPicker($scope, editorService, memberGroupResource){ editorService.memberGroupPicker(memberGroupPicker); }; - $scope.remove =function(index){ + // TODO: I don't believe this is used + $scope.remove = function(index){ $scope.renderModel.splice(index, 1); + setDirty(); }; + // TODO: I don't believe this is used $scope.add = function (item) { var currIds = _.map($scope.renderModel, function (i) { return i.id; @@ -58,11 +68,14 @@ function memberGroupPicker($scope, editorService, memberGroupResource){ if (currIds.indexOf(item) < 0) { $scope.renderModel.push({ name: item, id: item, icon: 'icon-users' }); + setDirty(); } }; + // TODO: I don't believe this is used $scope.clear = function() { $scope.renderModel = []; + setDirty(); }; function renderModelIds() { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html index b1cafafb0d..5a0788149e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html @@ -1,21 +1,22 @@
    - +
    - + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergroups/membergroups.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergroups/membergroups.controller.js index 6662a86571..2213841ece 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergroups/membergroups.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergroups/membergroups.controller.js @@ -10,6 +10,12 @@ return selected; }; + function setDirty() { + if ($scope.modelValueForm) { + $scope.modelValueForm.modelValue.$setDirty(); + } + } + $scope.pickGroup = function() { editorService.memberGroupPicker({ multiPicker: true, @@ -24,6 +30,7 @@ $scope.model.value[group.name] = true; }); }); + setDirty(); editorService.close(); }, close: function () { @@ -34,6 +41,7 @@ $scope.removeGroup = function (group) { $scope.model.value[group] = false; + setDirty(); } } angular.module('umbraco').controller("Umbraco.PropertyEditors.MemberGroupController", memberGroupController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergroups/membergroups.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergroups/membergroups.html index 2715e11b15..abfa628e90 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergroups/membergroups.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergroups/membergroups.html @@ -12,4 +12,9 @@ ng-click="pickGroup()"> Add + + + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.controller.js index c3137360e2..315eb18ee4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.controller.js @@ -1,6 +1,6 @@ //this controller simply tells the dialogs service to open a memberPicker window //with a specified callback, this callback will receive an object with a selection on it -function memberPickerController($scope, entityResource, iconHelper, angularHelper, editorService){ +function memberPickerController($scope, entityResource, iconHelper, editorService){ function trim(str, chr) { var rgxtrim = (!chr) ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); @@ -27,11 +27,16 @@ function memberPickerController($scope, entityResource, iconHelper, angularHelpe } else { $scope.clear(); $scope.add(data); - } - angularHelper.getCurrentForm($scope).$setDirty(); + } } }; + function setDirty() { + if ($scope.modelValueForm) { + $scope.modelValueForm.modelValue.$setDirty(); + } + } + //since most of the pre-value config's are used in the dialog options (i.e. maxNumber, minNumber, etc...) we'll merge the // pre-value config on to the dialog options if ($scope.model.config) { @@ -60,6 +65,7 @@ function memberPickerController($scope, entityResource, iconHelper, angularHelpe $scope.remove = function (index) { $scope.renderModel.splice(index, 1); + setDirty(); }; $scope.add = function (item) { @@ -76,7 +82,8 @@ function memberPickerController($scope, entityResource, iconHelper, angularHelpe if (currIds.indexOf(itemId) < 0) { item.icon = iconHelper.convertFromLegacyIcon(item.icon); - $scope.renderModel.push({ name: item.name, id: item.id, udi: item.udi, icon: item.icon}); + $scope.renderModel.push({ name: item.name, id: item.id, udi: item.udi, icon: item.icon }); + setDirty(); } }; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.html index 99f7fffba8..4efa7283da 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.html @@ -1,22 +1,23 @@
    - +
    - + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js index 172f9b2249..cfad66456d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js @@ -1,4 +1,4 @@ -function multiUrlPickerController($scope, angularHelper, localizationService, entityResource, iconHelper, editorService) { +function multiUrlPickerController($scope, localizationService, entityResource, iconHelper, editorService) { var vm = { labels: { @@ -16,8 +16,6 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en $scope.model.value = []; } - var currentForm = angularHelper.getCurrentForm($scope); - $scope.sortableOptions = { axis: "y", containment: "parent", @@ -27,7 +25,7 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en scroll: true, zIndex: 6000, update: function () { - currentForm.$setDirty(); + setDirty(); } }; @@ -66,7 +64,7 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en $scope.remove = function ($index) { $scope.renderModel.splice($index, 1); - currentForm.$setDirty(); + setDirty(); }; $scope.openLinkPicker = function (link, $index) { @@ -122,7 +120,7 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en link.published = true; } - currentForm.$setDirty(); + setDirty(); } editorService.close(); }, @@ -133,6 +131,12 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en editorService.linkPicker(linkPicker); }; + function setDirty() { + if ($scope.multiUrlPickerForm) { + $scope.multiUrlPickerForm.modelValue.$setDirty(); + } + } + function init() { localizationService.localizeMany(["general_recycleBin"]) .then(function (data) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.html index 503f4aac9b..12877bff88 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.html @@ -18,9 +18,9 @@ + - + + + From 2534f99cdba1fe7a34c675783652a97b89ee271f Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Wed, 14 Apr 2021 16:55:37 +0200 Subject: [PATCH 097/289] Adding new RecycleBin event handlers --- .../Implement/DocumentRepository.cs | 2 +- .../Repositories/Implement/MediaRepository.cs | 2 +- .../Cache/DistributedCacheBinder_Handlers.cs | 19 +++++++++++++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index e196a8a13c..a97569d571 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -918,7 +918,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var cacheKey = CacheKeys.ContentRecycleBinCacheKey; // always cache either true or false - return cache.GetCacheItem(cacheKey, () => CountChildren(Constants.System.RecycleBinContent) > 0); + return cache.GetCacheItem(cacheKey, () => CountChildren(RecycleBinId) > 0); } #endregion diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index c9001eed31..ac180d54ef 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -377,7 +377,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var cacheKey = CacheKeys.MediaRecycleBinCacheKey; // always cache either true or false - return cache.GetCacheItem(cacheKey, () => CountChildren(Constants.System.RecycleBinMedia) > 0); + return cache.GetCacheItem(cacheKey, () => CountChildren(RecycleBinId) > 0); } #endregion diff --git a/src/Umbraco.Web/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Web/Cache/DistributedCacheBinder_Handlers.cs index 3b2cf3e23d..c3cd53168e 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheBinder_Handlers.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheBinder_Handlers.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Core; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -121,6 +122,8 @@ namespace Umbraco.Web.Cache // bind to media events - handles all media changes Bind(() => MediaService.TreeChanged += MediaService_TreeChanged, () => MediaService.TreeChanged -= MediaService_TreeChanged); + Bind(() => MediaService.EmptiedRecycleBin += MediaService_EmptiedRecycleBin, + () => MediaService.EmptiedRecycleBin -= MediaService_EmptiedRecycleBin); // bind to content events Bind(() => ContentService.Saved += ContentService_Saved, // needed for permissions @@ -129,6 +132,8 @@ namespace Umbraco.Web.Cache () => ContentService.Copied -= ContentService_Copied); Bind(() => ContentService.TreeChanged += ContentService_TreeChanged,// handles all content changes () => ContentService.TreeChanged -= ContentService_TreeChanged); + Bind(() => ContentService.EmptiedRecycleBin += ContentService_EmptiedRecycleBin, + () => ContentService.EmptiedRecycleBin -= ContentService_EmptiedRecycleBin); // TreeChanged should also deal with this //Bind(() => ContentService.SavedBlueprint += ContentService_SavedBlueprint, @@ -200,7 +205,12 @@ namespace Umbraco.Web.Cache private void ContentService_Deleted(IContentService sender, DeleteEventArgs e) { } private void ContentService_Moved(IContentService sender, MoveEventArgs e) { } private void ContentService_Trashed(IContentService sender, MoveEventArgs e) { } - private void ContentService_EmptiedRecycleBin(IContentService sender, RecycleBinEventArgs e) { } + + private void ContentService_EmptiedRecycleBin(IContentService sender, RecycleBinEventArgs e) + { + var payloads = new[] { new ContentCacheRefresher.JsonPayload(Constants.System.RecycleBinContent, null, TreeChangeTypes.RefreshNode) }; + _distributedCache.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); + } private void ContentService_Published(IContentService sender, PublishEventArgs e) { } private void ContentService_Unpublished(IContentService sender, PublishEventArgs e) { } @@ -413,7 +423,12 @@ namespace Umbraco.Web.Cache private void MediaService_Deleted(IMediaService sender, DeleteEventArgs e) { } private void MediaService_Moved(IMediaService sender, MoveEventArgs e) { } private void MediaService_Trashed(IMediaService sender, MoveEventArgs e) { } - private void MediaService_EmptiedRecycleBin(IMediaService sender, RecycleBinEventArgs e) { } + + private void MediaService_EmptiedRecycleBin(IMediaService sender, RecycleBinEventArgs e) + { + var payloads = new[] { new MediaCacheRefresher.JsonPayload(Constants.System.RecycleBinMedia, null, TreeChangeTypes.RefreshNode) }; + _distributedCache.RefreshByPayload(MediaCacheRefresher.UniqueId, payloads); + } #endregion From 038afa566495e251a46f97ef8897701cb56f1b2d Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Thu, 15 Apr 2021 09:00:58 +0200 Subject: [PATCH 098/289] Fixing tests after adding the new event related to EmptiedRecycleBin --- .../Integration/ContentEventsTests.cs | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Tests/Integration/ContentEventsTests.cs b/src/Umbraco.Tests/Integration/ContentEventsTests.cs index af8ebe626e..fdf0b98705 100644 --- a/src/Umbraco.Tests/Integration/ContentEventsTests.cs +++ b/src/Umbraco.Tests/Integration/ContentEventsTests.cs @@ -1097,11 +1097,12 @@ namespace Umbraco.Tests.Integration ResetEvents(); ServiceContext.ContentService.EmptyRecycleBin(Constants.Security.SuperUserId); - Assert.AreEqual(2, _msgCount); - Assert.AreEqual(2, _events.Count); + Assert.AreEqual(3, _msgCount); + Assert.AreEqual(3, _events.Count); var i = 0; var m = 0; - Assert.AreEqual($"{m:000}: ContentRepository/Remove/{content.Id}", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Remove/{content.Id}", _events[i++].ToString()); + Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshNode/{Constants.System.RecycleBinContent}", _events[i++].ToString()); m++; Assert.AreEqual($"{m:000}: ContentCacheRefresher/Remove/{content.Id}", _events[i].ToString()); } @@ -1122,12 +1123,14 @@ namespace Umbraco.Tests.Integration ResetEvents(); ServiceContext.ContentService.EmptyRecycleBin(Constants.Security.SuperUserId); - Assert.AreEqual(3, _msgCount); - Assert.AreEqual(4, _events.Count); + Assert.AreEqual(4, _msgCount); + Assert.AreEqual(5, _events.Count); var i = 0; var m = 0; Assert.AreEqual($"{m++:000}: ContentRepository/Remove/{content1.Id}", _events[i++].ToString()); - Assert.AreEqual($"{m:000}: ContentRepository/Remove/{content2.Id}", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Remove/{content2.Id}", _events[i++].ToString()); + + Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshNode/{Constants.System.RecycleBinContent}", _events[i++].ToString()); m++; Assert.AreEqual($"{m:000}: ContentCacheRefresher/Remove/{content1.Id}", _events[i++].ToString()); Assert.AreEqual($"{m:000}: ContentCacheRefresher/Remove/{content2.Id}", _events[i].ToString()); @@ -1153,8 +1156,8 @@ namespace Umbraco.Tests.Integration ServiceContext.ContentService.EmptyRecycleBin(Constants.Security.SuperUserId); - Assert.AreEqual(14, _msgCount); - Assert.AreEqual(14, _events.Count); + Assert.AreEqual(15, _msgCount); + Assert.AreEqual(15, _events.Count); var i = 0; var m = 0; @@ -1170,7 +1173,9 @@ namespace Umbraco.Tests.Integration Assert.AreEqual($"{m++:000}: ContentRepository/Remove/{content2C[1].Id}", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Remove/{content2C[0].Id}", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Remove/{content1C[0].Id}", _events[i++].ToString()); - Assert.AreEqual($"{m:000}: ContentRepository/Remove/{content1.Id}", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Remove/{content1.Id}", _events[i++].ToString()); + + Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshNode/{Constants.System.RecycleBinContent}", _events[i++].ToString()); m++; Assert.AreEqual($"{m:000}: ContentCacheRefresher/Remove/{content1.Id}", _events[i].ToString()); } From 7d09de6d0fdc3ccbc48aa3b15afe5d6d883ae86e Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Thu, 15 Apr 2021 10:33:21 +0200 Subject: [PATCH 099/289] Removing unused param --- src/Umbraco.Web/Trees/ContentTreeControllerBase.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index f602ea30f3..6ad133c359 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -26,11 +26,9 @@ namespace Umbraco.Web.Trees { public abstract class ContentTreeControllerBase : TreeController { - private readonly AppCaches _appCaches; protected ContentTreeControllerBase(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) { - _appCaches = appCaches; } protected ContentTreeControllerBase() From 57ed4698f01be40328698c991c88de4e8a495064 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 19 Apr 2021 17:31:09 +0200 Subject: [PATCH 100/289] Revert "Revert "The Value() method for IPublishedContent was not working with the defaultValue parameter" (#9989)" This reverts commit 156c1c94161171f9592923f20de4e142c845469a. --- src/Umbraco.Web/PublishedContentExtensions.cs | 6 ++-- src/Umbraco.Web/PublishedPropertyExtension.cs | 33 ++++++------------- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index c851894149..6384b0f8d4 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -173,8 +173,8 @@ namespace Umbraco.Web return value; // else... if we have a property, at least let the converter return its own - // vision of 'no value' (could be an empty enumerable) - otherwise, default - return property == null ? default : property.Value(culture, segment, fallback, defaultValue); + // vision of 'no value' (could be an empty enumerable) - otherwise, defaultValue + return property == null ? defaultValue : property.Value(culture, segment, defaultValue: defaultValue); } #endregion diff --git a/src/Umbraco.Web/PublishedPropertyExtension.cs b/src/Umbraco.Web/PublishedPropertyExtension.cs index 6e8647db47..0c3aa57cc2 100644 --- a/src/Umbraco.Web/PublishedPropertyExtension.cs +++ b/src/Umbraco.Web/PublishedPropertyExtension.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Models.PublishedContent; @@ -36,20 +37,13 @@ namespace Umbraco.Web // we have a value // try to cast or convert it var value = property.GetValue(culture, segment); - if (value is T valueAsT) - { - return valueAsT; - } - + if (value is T valueAsT) return valueAsT; var valueConverted = value.TryConvertTo(); - if (valueConverted) - { - return valueConverted.Result; - } + if (valueConverted) return valueConverted.Result; - // cannot cast nor convert the value, nothing we can return but 'default' + // cannot cast nor convert the value, nothing we can return but 'defaultValue' // note: we don't want to fallback in that case - would make little sense - return default; + return defaultValue; } // we don't have a value, try fallback @@ -63,22 +57,15 @@ namespace Umbraco.Web var noValue = property.GetValue(culture, segment); if (noValue == null) { - return default; - } - - if (noValue is T noValueAsT) - { - return noValueAsT; + return defaultValue; } + if (noValue is T noValueAsT) return noValueAsT; var noValueConverted = noValue.TryConvertTo(); - if (noValueConverted) - { - return noValueConverted.Result; - } + if (noValueConverted) return noValueConverted.Result; - // cannot cast noValue nor convert it, nothing we can return but 'default' - return default; + // cannot cast noValue nor convert it, nothing we can return but 'defaultValue' + return defaultValue; } #endregion From f1e4fec4c4ad0299a787d350ea32e6ff2d26cf4e Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 19 Apr 2021 17:31:53 +0200 Subject: [PATCH 101/289] Revert "The Value() method for IPublishedContent was not working with the defaultValue parameter (#9888)" This reverts commit 52b670973d562b86a89665dd4b1b23fba8ec3900. --- src/Umbraco.Web/PublishedContentExtensions.cs | 4 ++-- src/Umbraco.Web/PublishedPropertyExtension.cs | 15 ++++----------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 6384b0f8d4..b43717d418 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -173,8 +173,8 @@ namespace Umbraco.Web return value; // else... if we have a property, at least let the converter return its own - // vision of 'no value' (could be an empty enumerable) - otherwise, defaultValue - return property == null ? defaultValue : property.Value(culture, segment, defaultValue: defaultValue); + // vision of 'no value' (could be an empty enumerable) - otherwise, default + return property == null ? default : property.Value(culture, segment); } #endregion diff --git a/src/Umbraco.Web/PublishedPropertyExtension.cs b/src/Umbraco.Web/PublishedPropertyExtension.cs index 0c3aa57cc2..b431f24828 100644 --- a/src/Umbraco.Web/PublishedPropertyExtension.cs +++ b/src/Umbraco.Web/PublishedPropertyExtension.cs @@ -41,31 +41,24 @@ namespace Umbraco.Web var valueConverted = value.TryConvertTo(); if (valueConverted) return valueConverted.Result; - // cannot cast nor convert the value, nothing we can return but 'defaultValue' + // cannot cast nor convert the value, nothing we can return but 'default' // note: we don't want to fallback in that case - would make little sense - return defaultValue; + return default; } // we don't have a value, try fallback if (PublishedValueFallback.TryGetValue(property, culture, segment, fallback, defaultValue, out var fallbackValue)) - { return fallbackValue; - } // we don't have a value - neither direct nor fallback // give a chance to the converter to return something (eg empty enumerable) var noValue = property.GetValue(culture, segment); - if (noValue == null) - { - return defaultValue; - } if (noValue is T noValueAsT) return noValueAsT; - var noValueConverted = noValue.TryConvertTo(); if (noValueConverted) return noValueConverted.Result; - // cannot cast noValue nor convert it, nothing we can return but 'defaultValue' - return defaultValue; + // cannot cast noValue nor convert it, nothing we can return but 'default' + return default; } #endregion From a3e3e83a3df2f450106c41909cf92eb1300c5104 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 19 Apr 2021 17:34:01 +0200 Subject: [PATCH 102/289] Bump version to 8.13.0 --- src/SolutionInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 2a7386cb45..1009ec125b 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -19,4 +19,4 @@ using System.Resources; // these are FYI and changed automatically [assembly: AssemblyFileVersion("8.13.0")] -[assembly: AssemblyInformationalVersion("8.13.0-rc")] +[assembly: AssemblyInformationalVersion("8.13.0")] From 12cde0c571318847e5a23ec58093c09b22ad7af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 20 Apr 2021 11:10:07 +0200 Subject: [PATCH 103/289] Use warning style when saving --- .../components/content/edit.controller.js | 7 ++- .../validation/valformmanager.directive.js | 57 ++++++++++++++++++- .../services/contenteditinghelper.service.js | 18 ++++-- .../src/less/alerts.less | 16 ++++++ .../umb-editor-navigation-item.less | 23 +++++--- .../less/components/umb-nested-content.less | 6 +- .../src/less/components/umb-tabs.less | 7 +++ src/Umbraco.Web.UI.Client/src/less/forms.less | 10 ++++ .../src/less/variables.less | 4 +- .../labelblock/labelblock.editor.less | 6 ++ 10 files changed, 137 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index da93450522..c7f56eeda6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -788,7 +788,12 @@ }).then(function () { $scope.page.saveButtonState = "success"; }, function (err) { - $scope.page.saveButtonState = "error"; + // Because this is the "save"-action, then we actually save though there was a validation error, therefor we will show success and display the validation errors politely. + if(err && err.data && err.data.ModelState && Object.keys(err.data.ModelState).length > 0) { + $scope.page.saveButtonState = "success"; + } else { + $scope.page.saveButtonState = "error"; + } handleHttpException(err); }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js index 55878db2e9..be84d45075 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js @@ -15,6 +15,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location, overlayService, eventsService, $routeParams, navigationService, editorService, localizationService, angularHelper) { var SHOW_VALIDATION_CLASS_NAME = "show-validation"; + var SHOW_VALIDATION_Type_CLASS_NAME = "show-validation-type-"; var SAVING_EVENT_NAME = "formSubmitting"; var SAVED_EVENT_NAME = "formSubmitted"; @@ -25,7 +26,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location function ValFormManagerController($scope) { //This exposes an API for direct use with this directive - // We need this as a way to reference this directive in the scope chain. Since this directive isn't a component and + // We need this as a way to reference this directive in the scope chain. Since this directive isn't a component and // because it's an attribute instead of an element, we can't use controllerAs or anything like that. Plus since this is // an attribute an isolated scope doesn't work so it's a bit weird. By doing this we are able to lookup the parent valFormManager // in the scope hierarchy even if the DOM hierarchy doesn't match (i.e. in infinite editing) @@ -44,6 +45,8 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location this.isShowingValidation = () => $scope.showValidation === true; + this.getValidationMessageType = () => $scope.valMsgType; + this.notify = notify; this.isValid = function () { @@ -94,6 +97,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location var parentFormMgr = scope.parentFormMgr = getAncestorValFormManager(scope, ctrls, 1); var subView = ctrls.length > 1 ? ctrls[2] : null; var labels = {}; + var valMsgType = 2;// error var labelKeys = [ "prompt_unsavedChanges", @@ -109,6 +113,46 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location labels.stayButton = values[3]; }); + var lastValidationMessageType = null; + function setValidationMessageType(type) { + + removeValidationMessageType(); + scope.valMsgType = type; + + // overall a copy of message types from notifications.service: + var postfix = ""; + switch(type) { + case 0: + //save + break; + case 1: + //info + postfix = "info"; + break; + case 2: + //error + postfix = "error"; + break; + case 3: + //success + postfix = "success"; + break; + case 4: + //warning + postfix = "warning"; + break; + } + var cssClass = SHOW_VALIDATION_Type_CLASS_NAME+postfix; + element.addClass(cssClass); + lastValidationMessageType = cssClass; + } + function removeValidationMessageType() { + if(lastValidationMessageType) { + element.removeClass(lastValidationMessageType); + lastValidationMessageType = null; + } + } + //watch the list of validation errors to notify the application of any validation changes // TODO: Wouldn't it be easier/faster to watch formCtrl.$invalid ? scope.$watch(() => angularHelper.countAllFormErrors(formCtrl), @@ -138,6 +182,8 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location if (serverValidationManager.items.length > 0 || (parentFormMgr && parentFormMgr.isShowingValidation())) { element.addClass(SHOW_VALIDATION_CLASS_NAME); scope.showValidation = true; + var parentValMsgType = parentFormMgr ? parentFormMgr.getValidationMessageType() : 2; + setValidationMessageType(parentValMsgType || 2); notifySubView(); } @@ -145,8 +191,16 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location //listen for the forms saving event unsubscribe.push(scope.$on(SAVING_EVENT_NAME, function (ev, args) { + + var messageType = 2;//error + switch (args.action) { + case "save": + messageType = 4;//warning + break; + } element.addClass(SHOW_VALIDATION_CLASS_NAME); scope.showValidation = true; + setValidationMessageType(messageType); notifySubView(); //set the flag so we can check to see if we should display the error. isSavingNewItem = $routeParams.create; @@ -156,6 +210,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location unsubscribe.push(scope.$on(SAVED_EVENT_NAME, function (ev, args) { //remove validation class element.removeClass(SHOW_VALIDATION_CLASS_NAME); + removeValidationMessageType(); scope.showValidation = false; notifySubView(); })); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index 8524b960c6..b7f765d8e0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -32,7 +32,8 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt return true; } - function showNotificationsForModelsState(ms) { + function showNotificationsForModelsState(ms, messageType) { + messageType = messageType || 2; for (const [key, value] of Object.entries(ms)) { var errorMsg = value[0]; @@ -42,12 +43,14 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt var idsToErrors = serverValidationManager.parseComplexEditorError(errorMsg, ""); idsToErrors.forEach(x => { if (x.modelState) { - showNotificationsForModelsState(x.modelState); + showNotificationsForModelsState(x.modelState, messageType); } }); } else if (value[0]) { - notificationsService.error("Validation", value[0]); + //notificationsService.error("Validation", value[0]); + console.log({type:messageType, header:"Validation", message:value[0]}) + notificationsService.showNotification({type:messageType, header:"Validation", message:value[0]}) } } } @@ -124,6 +127,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt showNotifications: args.showNotifications, softRedirect: args.softRedirect, err: err, + action: args.action, rebindCallback: function () { // if the error contains data, we want to map that back as we want to continue editing this save. Especially important when the content is new as the returned data will contain ID etc. if(err.data) { @@ -639,9 +643,15 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt //wire up the server validation errs formHelper.handleServerValidation(args.err.data.ModelState); + var messageType = 2;//error + console.log(args) + if (args.action === "save") { + messageType = 4;//warning + } + //add model state errors to notifications if (args.showNotifications) { - showNotificationsForModelsState(args.err.data.ModelState); + showNotificationsForModelsState(args.err.data.ModelState, messageType); } if (!this.redirectToCreatedContent(args.err.data.id, args.softRedirect) || args.softRedirect) { diff --git a/src/Umbraco.Web.UI.Client/src/less/alerts.less b/src/Umbraco.Web.UI.Client/src/less/alerts.less index 3539e21064..ab0ab9aa13 100644 --- a/src/Umbraco.Web.UI.Client/src/less/alerts.less +++ b/src/Umbraco.Web.UI.Client/src/less/alerts.less @@ -54,6 +54,15 @@ border-color: @errorBorder; color: @errorText; } + +.alert-warning() { + background-color: @warningBackground; + border-color: @warningBorder; + color: @warningText; +} +.alert-warning { + .alert-warning() +} .alert-danger h4, .alert-error h4 { color: @errorText; @@ -110,6 +119,13 @@ padding: 6px 16px 6px 12px; margin-bottom: 6px; + .show-validation-type-warning & { + .alert-warning(); + &.alert-error::after { + border-top-color: @warningBackground; + } + } + &::after { content:''; position: absolute; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less index 5e9772fb26..5fd743aaf0 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less @@ -86,6 +86,16 @@ } } } + + .show-validation.show-validation-type-warning &.-has-error { + color: @yellow-d2; + &:hover { + color: @yellow-d2 !important; + } + &::before { + background-color: @yellow-d2; + } + } } &__action:active, @@ -122,14 +132,6 @@ line-height: 16px; display: block; - &.-type-alert { - background-color: @red; - } - - &.-type-warning { - background-color: @yellow-d2; - } - &:empty { height: 12px; min-width: 12px; @@ -137,6 +139,11 @@ &.--error-badge { display: none; font-weight: 900; + background-color: @red; + + .show-validation-type-warning & { + background-color: @yellow-d2; + } } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less index bd787e2329..9dd40a4386 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -48,6 +48,10 @@ &.--error { border-color: @formErrorBorder !important; } + + .show-validation-type-warning &.--error { + border-color: @formWarningBorder !important; + } } .umb-nested-content__item.ui-sortable-placeholder { @@ -292,4 +296,4 @@ .umb-textarea, .umb-textstring { width:100%; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less index 15b317aa45..1b249f1c3a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less @@ -86,6 +86,13 @@ background-color: @red !important; border-color: @errorBorder; } +.show-validation.show-validation-type-warning .umb-tab--error > .umb-tab-button, +.show-validation.show-validation-type-warning .umb-tab--error > .umb-tab-button:hover, +.show-validation.show-validation-type-warning .umb-tab--error > .umb-tab-button:focus { + color: @white !important; + background-color: @yellow-d2 !important; + border-color: @warningBorder; +} .show-validation .umb-tab--error .umb-tab-button:before { content: "\e25d"; diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index 3782fca695..60561f9acc 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -506,10 +506,20 @@ input[type="checkbox"][readonly] { .formFieldState(@formErrorText, @formErrorText, @formErrorBackground); } +// ValidationError as a warning +.show-validation.show-validation-type-warning.ng-invalid .control-group.error, +.show-validation.show-validation-type-warning.ng-invalid .umb-editor-header__name-wrapper { + .formFieldState(@formWarningText, @formWarningText, @formWarningBackground); +} + //val-highlight directive styling .highlight-error { color: @formErrorText !important; border-color: @red-l1 !important; + .show-validation-type-warning & { + color: @formWarningText !important; + border-color: @yellow-d2 !important; + } } // FORM ACTIONS diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index cab0745a42..90cf24cf2d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -291,8 +291,8 @@ @btnSuccessBackground: @ui-btn-positive;// updated 2019 @btnSuccessBackgroundHighlight: @ui-btn-positive-hover;// updated 2019 -@btnWarningBackground: @orange; -@btnWarningBackgroundHighlight: lighten(@orange, 10%); +@btnWarningBackground: @yellow-d2; +@btnWarningBackgroundHighlight: lighten(@yellow-d2, 10%); @btnDangerBackground: @red; @btnDangerBackgroundHighlight: @red-l1; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less index 613a47b926..837fd3f564 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less @@ -42,6 +42,9 @@ ng-form.ng-invalid-val-server-match-content > .umb-block-list__block > .umb-block-list__block--content > div > & { color: @formErrorText; + .show-validation-type-warning & { + color: @formWarningText; + } } ng-form.ng-invalid-val-server-match-content > .umb-block-list__block:not(.--active) > .umb-block-list__block--content > div > & { > span { @@ -61,6 +64,9 @@ padding: 2px; line-height: 10px; background-color: @formErrorText; + .show-validation-type-warning & { + background-color: @formWarningText; + } font-weight: 900; animation-duration: 1.4s; From 021c0b82c185d82194360ed36f0d91984f0dd7e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 20 Apr 2021 11:35:40 +0200 Subject: [PATCH 104/289] final corrections --- .../components/content/edit.controller.js | 8 +++++++- src/Umbraco.Web.UI.Client/src/less/alerts.less | 1 + .../src/less/components/overlays.less | 3 +++ .../src/less/components/umb-list.less | 5 ++++- src/Umbraco.Web.UI.Client/src/less/variables.less | 2 +- .../src/views/content/overlays/save.html | 4 ++-- .../blocklist/umb-block-list-property-editor.less | 15 ++++++++++----- 7 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 61433c2b62..196c885b4e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -765,7 +765,13 @@ }, function (err) { clearDirtyState($scope.content.variants); - model.submitButtonState = "error"; + //model.submitButtonState = "error"; + // Because this is the "save"-action, then we actually save though there was a validation error, therefor we will show success and display the validation errors politely. + if(err && err.data && err.data.ModelState && Object.keys(err.data.ModelState).length > 0) { + model.submitButtonState = "success"; + } else { + model.submitButtonState = "error"; + } //re-map the dialog model since we've re-bound the properties dialog.variants = $scope.content.variants; handleHttpException(err); diff --git a/src/Umbraco.Web.UI.Client/src/less/alerts.less b/src/Umbraco.Web.UI.Client/src/less/alerts.less index ab0ab9aa13..94dcef6f25 100644 --- a/src/Umbraco.Web.UI.Client/src/less/alerts.less +++ b/src/Umbraco.Web.UI.Client/src/less/alerts.less @@ -121,6 +121,7 @@ .show-validation-type-warning & { .alert-warning(); + font-weight: bold; &.alert-error::after { border-top-color: @warningBackground; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less index 035bf02f91..12cce286d6 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less @@ -267,6 +267,9 @@ .umb-overlay .text-error { color: @formErrorText; } +.umb-overlay .text-warning { + color: @formWarningText; +} .umb-overlay .text-success { color: @formSuccessText; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less index 57ba73305a..c281f7f5ea 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less @@ -26,7 +26,10 @@ a.umb-list-item:focus { } .umb-list-item--error { - color: @red; + color: @formErrorText; +} +.umb-list-item--warning { + color: @formWarningText; } .umb-list-item:hover .umb-list-checkbox, diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index 90cf24cf2d..9d114b093e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -480,7 +480,7 @@ @formWarningBorder: darken(spin(@warningBackground, -10), 3%); @formErrorText: @errorBackground; -@formErrorBackground: lighten(@errorBackground, 55%); +@formErrorBackground: @errorBackground; @formErrorBorder: @red; @formSuccessText: @successBackground; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html index fa9ab8c437..d414f30dbf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html @@ -16,7 +16,7 @@
    -
    +
    - {{saveVariantSelectorForm.saveVariantSelector.errorMsg}} + {{saveVariantSelectorForm.saveVariantSelector.errorMsg}} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less index 019a772fdd..fbb7e1b32e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less @@ -23,10 +23,6 @@ > .umb-block-list__block--actions { opacity: 0; transition: opacity 120ms; - - .--error { - color: @formErrorBorder !important; - } } &:hover, @@ -100,6 +96,12 @@ ng-form.ng-invalid-val-server-match-settings > .umb-block-list__block > .umb-blo &:hover { color: @ui-action-discreet-type-hover; } + &.--error { + color: @errorBackground; + .show-validation-type-warning & { + color: @warningBackground; + } + } > .__error-badge { position: absolute; top: -2px; @@ -113,7 +115,10 @@ ng-form.ng-invalid-val-server-match-settings > .umb-block-list__block > .umb-blo font-weight: bold; padding: 2px; line-height: 8px; - background-color: @red; + background-color: @errorBackground; + .show-validation-type-warning & { + background-color: @warningBackground; + } display: none; font-weight: 900; } From 5f45e6fd83a957b9df357722f624caf81c4e550e Mon Sep 17 00:00:00 2001 From: Chad Currie Date: Tue, 20 Apr 2021 22:42:24 +1200 Subject: [PATCH 105/289] Provide capacity values for DictionaryOfPropertyDataSerializer --- .../BTree.DictionaryOfPropertyDataSerializer.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs index aa5dc9eb30..e5d8bfe780 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs @@ -10,10 +10,10 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { public IDictionary ReadFrom(Stream stream) { - var dict = new Dictionary(StringComparer.InvariantCultureIgnoreCase); // read properties count var pcount = PrimitiveSerializer.Int32.ReadFrom(stream); + var dict = new Dictionary(pcount,StringComparer.InvariantCultureIgnoreCase); // read each property for (var i = 0; i < pcount; i++) @@ -25,13 +25,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource var vcount = PrimitiveSerializer.Int32.ReadFrom(stream); // create pdata and add to the dictionary - var pdatas = new List(); + var pdatas = new PropertyData[vcount]; // for each value, read and add to pdata for (var j = 0; j < vcount; j++) { var pdata = new PropertyData(); - pdatas.Add(pdata); + pdatas[j] = pdata; // everything that can be null is read/written as object // even though - culture and segment should never be null here, as 'null' represents @@ -43,7 +43,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource pdata.Value = ReadObject(stream); } - dict[key] = pdatas.ToArray(); + dict[key] = pdatas; } return dict; } From 98d8971ce9170571a90a334e5180d8dfac28af1f Mon Sep 17 00:00:00 2001 From: Chad Date: Wed, 21 Apr 2021 11:05:01 +1200 Subject: [PATCH 106/289] Merge in v8/contrib to V8/feature/nucache perf sync (#10151) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Bump version to 8.6.8 * Initial rework of Lock dictionaries * [Issue 5277-146] accessibility - Close 'X' icon next to language drop… (#9264) * [Issue 5277-146] accessibility - Close 'X' icon next to language drop down is identified as "link" - screen reader * add new loacalization key * Fix issue with SqlMainDomLock that cannot use implicit lock timeouts … (#9973) * Fix issue with SqlMainDomLock that cannot use implicit lock timeouts … (#9973) (cherry picked from commit da5351dfcf23daad69fcd73eb74811456ffc34c0) * Adjust unit tests and apply fixes to scope * Add more unit tests, showing current issue * Counting Umbraco.ModelsBuilder and ModelsBuilder.Umbraco namespaces as external providers * Fix dead lock with TypeLoader * Fix errors shown in unit tests * Throw error if all scopes hasn't been disposed * Clean * Fixes and Updates for DB Scope and Ambient Context leaks (#9953) * Adds some scope tests (ported back from netcore) and provides a much better error message, ensure execution context is not flowed to child tasks that shouldn't leak any current ambient context * updates comment * Ensure SqlMainDomLock suppresses execution context too * Since we're awaiting a task in a library method, ConfigureAwait(false) * missing null check Co-authored-by: Elitsa Marinovska * Adds additional error checking and reporting to MainDom/SqlMainDomLock (#9954) Co-authored-by: Elitsa Marinovska * Add copy logic to Media Picker (#9957) * Add copy logic to Media Picker * Add action for copy all * Fix for selectable media item * Wrap calls to map in scopes * Autocomplete scopes * Remove unnecessary aria-hidden attribute from * Remove scope from method that calls another method that has a scope * Fixes #9993 - Cannot save empty image in Grid * Clean * Revert "The Value() method for IPublishedContent was not working with the defaultValue parameter" (#9989) * Use a hashset to keep track of acquired locks This simplifies disposing/checking for locks greatly. * Add images in grid - fixes 9982 (#9987) Co-authored-by: Sebastiaan Janssen * Only create the dicts and hashset when a lock is requested * Clean * Adds a config for configuring the access rules on the content dashboard - by default it granted for all user groups * Adds additional params indicating whether user is admin * Add images in grid - fixes 9982 (#9987) Co-authored-by: Sebastiaan Janssen (cherry picked from commit e2019777fbfc1f9221d040cb9f0b82c57f8552b9) * Bump version to 8.12.2 * #9964 Removed unneeded check for HttpContext * Fix for #9950 - HttpsCheck will now retry using the login background image if inital request returns 301/302. Excessvie Headers check will now check the root url instead of the backoffice * Merge pull request #9994 from umbraco/v8/bugfix/9993 Fixes #9993 - Cannot save empty image in Grid (cherry picked from commit 0ecc933921f2dea9a2a16d6f395b44a039663ec6) * Apply suggestions from review * Fixes #9983 - Getting kicked, if document type has a Umbraco.UserPicker property (#10002) * Fixes #9983 Temporary fix for this issue. using the entityservice like before. * Needed to remove the call to usersResource here as well for displaying the picked items * Don't need usersResource for now * Fixes #9983 - Getting kicked, if document type has a Umbraco.UserPicker property (#10002) * Fixes #9983 Temporary fix for this issue. using the entityservice like before. * Needed to remove the call to usersResource here as well for displaying the picked items * Don't need usersResource for now (cherry picked from commit 45de0a101eaa2b8f16e21a765f32928c7cb968be) * 8539: Allow alias in image cropper (#9266) Co-authored-by: Owain Williams * Wrap dumping dictionaries in a method. * Create method for generating log message And remove forgotten comments. * Fix swedish translation for somethingElse. * Copy member type (#10020) * Add copy dialog for member type * Implement copy action for member type * Create specific localization for content type, media type and member type * Handle "foldersonly" querystring * Add button type attribute * Add a few missing changes of anchor to button element * Null check on scope and options to ensure backward compatibility * Improve performance, readability and handling of FollowInternalRedirects (#9889) * Improve performance, readability and handling of FollowInternalRedirects * Logger didn't like string param Passing string param to _logger.Debug wasn't happy. Changed to pass existing internalRedirectAsInt variable. Co-authored-by: Nathan Woulfe * Update casing of listview layout name * 9097 add contextual password helper (#9256) * update back-office forms * Display tip on reset password page as well * add directive for password tip * integrate directove in login screen * forgot the ng-keyup :-) * adapt tooltip directive to potential different Members and Users password settings * remove watcher Co-authored-by: Nathan Woulfe * Unbind listener Listening for splitViewRequest was only unbound if the split view editor was opened. Not cleaning up the listener caused a memory leak when changing between nodes as the spit view editor was detached but not garbage-collected * Replace icon in date picker with umb-icon component (#10040) * Replace icon in date picker with component * Adjust height of clear button * Update cypress and fix tests * Listview config icons (#10036) * Update icons to use component * Simplify markup and use disabled button * Use move cursor style on sortable handle * Add class for action column * Update setting auto focus * Increase font size of umb-panel-header-icon * Anchor noopener (#10009) * Set rel="noopener" for anchors with target="_blank" * Reverted unwanted changes to Default.cshtml * Align 'Add language' test to netcore * Add new cypress tests * Add indentation * Getting rid of the config file and implementing an appSetting instead * Implementation for IContentDashboardSettings * Cleanup * bool.Try * Taking AllowContentDashboardAccessToAllUsers prop from GlobalSettings to ContentDashboardSettings and saving AccessRulesFromConfig into a backing field * Handling multiple values per field in Examine Management * Add Root and Breadcrumbs extension methods for IPublishedContent (#9033) * Fix usage of obsolete CreatorName and WriterName properties * Add generic Root extension method * Add Breadcrumbs extension methods * Orders member type grouping of members alphabetically, matching the listing of member types. * Revert updating deprecated WriterName/CreatorName refs Changing the properties to use the extensions is a good thing (given the props are deprecated), but causes issues deep in tests. I'm reverting that change to fix the tests, and all refs to the deprecated properties should be updated in one sweep, to deal with any other test issues that might crop up. * Handle Invalid format for Upgrade check * Fixes tabbing-mode remains active after closing modal #9790 (#10074) * Allow to pass in boolean to preventEnterSubmit directive (#8639) * Pass in value to preventEnterSubmit directive * Set enabled similar to preventDefault and preventEnterSubmit directives * Update prevent enter submit value * Init value from controller * Use a different default input id prefix for umb-search-filter * Fix typo * Check for truthly value * Revert "Set enabled similar to preventDefault and preventEnterSubmit directives" This reverts commit 536ce855c4545ead82cea77b4013bf9010a8687b. * None pointer events when clicking icon * Use color variable * Fixes tabbing-mode remains active after closing modal #9790 (#10074) (cherry picked from commit c881fa9e7d08c11954e18489827f70cdafceb947) * Null check on scope and options to ensure backward compatibility (cherry picked from commit fe8cd239d2f4c528c1a8a3cf4c50e90bb43cacfc) * Fix validation of step size in integer/numeric field * 9962: Use $allowedEditors instead of allowed (#10086) * 9962: Use $allowedEditors instead of allowed * 9962: Remove redundant statement * fixes #10021 adds ng-form and val-form-manager to the documentation * Improved accessibility of link picker (#10099) * Added support for screeen reader alerts on the embed so that assitive technology knows when a url retrieve has been succesfull. Added labels for the controls Preview reload only triggered if the values for height and width change * Added control ids for the link picker * Add French translation * Accessibility: Alerts the user how many results have been returned on a tree search (#10100) * Added support for screeen reader alerts on the embed so that assitive technology knows when a url retrieve has been succesfull. Added labels for the controls Preview reload only triggered if the values for height and width change * Tree search details the number of search items returned * Add French translations * Updated LightInject to v6.4.0 * Remove HtmlSanitizer once more - see #9803 * Also make sure NuGet installs the correct version of the CodePages dependency * Bump version to 8.13 RC * Fixed copy preserving sort order (#10091) * Revert "Updated LightInject to v6.4.0" This reverts commit fc77252ec756cf90bb74e7fbbe6dd6d75cbdacfc. * Revert "Add copy logic to Media Picker (#9957)" This reverts commit f7c032af65cac83182782c758a3ab79c86b92e70. * Reintroduce old constructor to make non-breaking * Update cypress test to make macros in the grid work again * Attributes could be multiple items, test specifically if `Directory` is an attribute * Accessibility: Adding label fors and control ids for the macro picker (#10101) * Added support for screeen reader alerts on the embed so that assitive technology knows when a url retrieve has been succesfull. Added labels for the controls Preview reload only triggered if the values for height and width change * Added support for label fors for the macro picker and also gave the ,acro search box a title * Now displays a count of the matching macros returned. Please note the language file amends shared with #10100 * Removed src-only class for the display of the count of messages * Updating typo * Removed top-margin from switcher icon * Allow KeepAlive controller Ping method to be requested by non local requests (#10126) * Allow KeepAlive controller Ping method to be requested by non local requests and accept head requests * removed unused references * fix csproj Co-authored-by: Mole Co-authored-by: Sebastiaan Janssen Co-authored-by: Justin Shearer Co-authored-by: Bjarke Berg Co-authored-by: Callum Whyte Co-authored-by: Shannon Co-authored-by: Elitsa Marinovska Co-authored-by: patrickdemooij9 Co-authored-by: Bjarne Fyrstenborg Co-authored-by: Michael Latouche Co-authored-by: Nathan Woulfe Co-authored-by: Markus Johansson Co-authored-by: Jeavon Leopold Co-authored-by: Benjamin Carleski Co-authored-by: Owain Williams Co-authored-by: Jesper Löfgren Co-authored-by: Martin Bentancour Co-authored-by: Ronald Barendse Co-authored-by: Andy Butland Co-authored-by: BeardinaSuit Co-authored-by: Mads Rasmussen Co-authored-by: Rachel Breeze Co-authored-by: Dave de Moel Co-authored-by: ric <60885685+ricbrady@users.noreply.github.com> Co-authored-by: Carole Rennie Logan Co-authored-by: Dennis Öhman --- build/NuSpecs/UmbracoCms.Web.nuspec | 2 +- src/SolutionInfo.cs | 4 +- src/Umbraco.Core/Composing/TypeLoader.cs | 117 +++++-- src/Umbraco.Core/ConfigsExtensions.cs | 4 +- .../Configuration/GlobalSettings.cs | 41 ++- src/Umbraco.Core/Constants-AppSettings.cs | 5 + src/Umbraco.Core/Constants-Security.cs | 2 +- src/Umbraco.Core/Constants-SvgSanitizer.cs | 23 -- .../Dashboards/ContentDashboardSettings.cs | 24 ++ .../Dashboards/IContentDashboardSettings.cs | 14 + src/Umbraco.Core/IO/IOHelper.cs | 5 +- src/Umbraco.Core/Mapping/UmbracoMapper.cs | 38 +- .../Implement/UpgradeCheckRepository.cs | 5 + src/Umbraco.Core/Runtime/MainDom.cs | 9 +- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 108 +++--- src/Umbraco.Core/Scoping/Scope.cs | 331 +++++++++--------- src/Umbraco.Core/Scoping/ScopeProvider.cs | 4 + .../Services/Implement/ContentService.cs | 10 +- src/Umbraco.Core/StringExtensions.cs | 41 ++- src/Umbraco.Core/Umbraco.Core.csproj | 3 +- src/Umbraco.Examine/UmbracoExamineIndex.cs | 21 +- .../Compose/ModelsBuilderComposer.cs | 35 +- .../cypress/integration/Content/content.ts | 204 ++++++++++- .../cypress/integration/Settings/languages.ts | 5 +- .../cypress/integration/Settings/templates.ts | 8 +- .../integration/Tour/backofficeTour.ts | 2 +- src/Umbraco.Tests.AcceptanceTest/package.json | 4 +- src/Umbraco.Tests/Mapping/MappingTests.cs | 37 +- src/Umbraco.Tests/Scoping/ScopeTests.cs | 114 ++++++ src/Umbraco.Tests/Scoping/ScopeUnitTests.cs | 294 +++++++++++++--- .../Services/ContentServiceTests.cs | 26 ++ .../Importing/StandardMvc-Package.xml | 6 +- .../Services/PerformanceTests.cs | 4 +- .../Services/ThreadSafetyServiceTest.cs | 22 +- .../Testing/TestingTests/MockTests.cs | 18 +- .../application/umblogin.directive.js | 6 +- .../application/umbpasswordtip.directive.js | 71 ++++ .../umbvariantcontenteditors.directive.js | 3 + .../forms/prevententersubmit.directive.js | 2 +- .../forms/umbfocuslock.directive.js | 4 - .../forms/umbsearchfilter.directive.js | 10 +- .../components/tabs/umbtabsnav.directive.js | 37 +- .../tree/umbtreesearchbox.directive.js | 1 + .../users/changepassword.directive.js | 9 + .../mocks/services/localization.mocks.js | 6 +- .../common/resources/contenttype.resource.js | 8 +- .../common/resources/mediatype.resource.js | 4 +- .../common/resources/membertype.resource.js | 25 +- .../common/services/listviewhelper.service.js | 4 +- .../services/umbrequesthelper.service.js | 4 +- .../src/installer/installer.service.js | 6 +- .../src/installer/steps/upgrade.html | 2 +- .../src/less/application/umb-outline.less | 1 + .../src/less/buttons.less | 17 +- .../editor/umb-variant-switcher.less | 1 - .../less/components/umb-search-filter.less | 3 +- src/Umbraco.Web.UI.Client/src/less/forms.less | 17 +- src/Umbraco.Web.UI.Client/src/less/main.less | 4 + src/Umbraco.Web.UI.Client/src/less/panel.less | 1 + .../src/less/property-editors.less | 17 +- .../linkpicker/linkpicker.html | 67 ++-- .../macropicker/macropicker.controller.js | 31 +- .../macropicker/macropicker.html | 24 +- .../userpicker/userpicker.controller.js | 16 +- .../components/application/umb-login.html | 3 +- .../editor/umb-editor-content-header.html | 1 + .../components/forms/umb-search-filter.html | 31 +- .../components/tree/umb-tree-search-box.html | 1 + .../tree/umb-tree-search-results.html | 49 ++- .../components/users/change-password.html | 4 +- .../dashboard/media/mediadashboardvideos.html | 2 +- .../settings/examinemanagementresults.html | 4 +- .../src/views/documenttypes/copy.html | 2 +- .../src/views/documenttypes/export.html | 4 +- .../documenttypes/importdocumenttype.html | 6 +- .../src/views/documenttypes/move.html | 2 +- .../src/views/mediatypes/copy.html | 2 +- .../src/views/mediatypes/move.html | 2 +- .../src/views/membertypes/copy.controller.js | 61 ++++ .../src/views/membertypes/copy.html | 53 +++ .../changepassword.controller.js | 3 + .../datepicker/datepicker.html | 10 +- .../views/propertyeditors/email/email.html | 5 +- .../grid/editors/media.controller.js | 40 +-- .../propertyeditors/grid/grid.controller.js | 4 +- .../propertyeditors/integer/integer.html | 9 +- .../listview/includeproperties.prevalues.html | 4 +- .../listview/layouts.prevalues.html | 18 +- .../propertyeditors/listview/listview.html | 1 - .../userpicker/userpicker.controller.js | 29 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 5 +- .../Umbraco/Views/Default.cshtml | 7 +- src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml | 10 +- src/Umbraco.Web.UI/Umbraco/config/lang/cy.xml | 12 +- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 21 +- src/Umbraco.Web.UI/Umbraco/config/lang/de.xml | 12 +- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 31 +- .../Umbraco/config/lang/en_us.xml | 31 +- src/Umbraco.Web.UI/Umbraco/config/lang/es.xml | 10 +- src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml | 31 +- src/Umbraco.Web.UI/Umbraco/config/lang/he.xml | 4 +- src/Umbraco.Web.UI/Umbraco/config/lang/it.xml | 4 +- src/Umbraco.Web.UI/Umbraco/config/lang/ja.xml | 4 +- src/Umbraco.Web.UI/Umbraco/config/lang/ko.xml | 4 +- src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml | 4 +- src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml | 6 +- src/Umbraco.Web.UI/Umbraco/config/lang/pt.xml | 4 +- src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml | 14 +- src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml | 6 +- src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml | 24 +- src/Umbraco.Web.UI/Umbraco/config/lang/zh.xml | 6 +- .../Umbraco/config/lang/zh_tw.xml | 6 +- .../config/splashes/noNodes.aspx | 4 +- src/Umbraco.Web.UI/web.Template.config | 1 + .../Dashboards/ContentDashboard.cs | 59 +++- .../Editors/BackOfficeServerVariables.cs | 6 +- .../Binders/ContentModelBinderHelper.cs | 9 +- src/Umbraco.Web/Editors/CodeFileController.cs | 5 +- .../Editors/DashboardController.cs | 4 +- .../Editors/ExamineManagementController.cs | 13 +- .../Editors/KeepAliveController.cs | 4 +- .../Editors/MemberTypeController.cs | 12 + .../Checks/Security/ExcessiveHeadersCheck.cs | 4 +- .../HealthCheck/Checks/Security/HttpsCheck.cs | 21 +- .../Models/ContentEditing/SearchResult.cs | 2 +- .../PropertyEditors/GridPropertyEditor.cs | 4 +- .../PropertyEditors/ListViewConfiguration.cs | 2 +- src/Umbraco.Web/PublishedContentExtensions.cs | 90 ++++- src/Umbraco.Web/PublishedPropertyExtension.cs | 35 +- src/Umbraco.Web/Routing/PublishedRouter.cs | 62 ++-- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 10 - .../Scheduling/BackgroundTaskRunner.cs | 19 +- src/Umbraco.Web/Services/IconService.cs | 8 +- src/Umbraco.Web/Trees/MemberTreeController.cs | 10 +- .../MemberTypeAndGroupTreeControllerBase.cs | 12 +- .../Trees/MemberTypeTreeController.cs | 1 + src/Umbraco.Web/Umbraco.Web.csproj | 3 - 137 files changed, 2137 insertions(+), 860 deletions(-) delete mode 100644 src/Umbraco.Core/Constants-SvgSanitizer.cs create mode 100644 src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs create mode 100644 src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs create mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbpasswordtip.directive.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/membertypes/copy.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/membertypes/copy.html diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index de634b4884..46165df087 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -42,7 +42,7 @@ - + diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index c064920d34..2a7386cb45 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.12.0")] -[assembly: AssemblyInformationalVersion("8.12.0")] +[assembly: AssemblyFileVersion("8.13.0")] +[assembly: AssemblyInformationalVersion("8.13.0-rc")] diff --git a/src/Umbraco.Core/Composing/TypeLoader.cs b/src/Umbraco.Core/Composing/TypeLoader.cs index f5c75ff607..bee6436cd6 100644 --- a/src/Umbraco.Core/Composing/TypeLoader.cs +++ b/src/Umbraco.Core/Composing/TypeLoader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -45,7 +45,7 @@ namespace Umbraco.Core.Composing private IEnumerable _assemblies; private bool _reportedChange; private readonly string _localTempPath; - private string _fileBasePath; + private readonly Lazy _fileBasePath; /// /// Initializes a new instance of the class. @@ -70,6 +70,8 @@ namespace Umbraco.Core.Composing _localTempPath = localTempPath; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _fileBasePath = new Lazy(GetFileBasePath); + if (detectChanges) { //first check if the cached hash is string.Empty, if it is then we need @@ -160,7 +162,8 @@ namespace Umbraco.Core.Composing return _cachedAssembliesHash; var typesHashFilePath = GetTypesHashFilePath(); - if (!File.Exists(typesHashFilePath)) return string.Empty; + if (!File.Exists(typesHashFilePath)) + return string.Empty; var hash = File.ReadAllText(typesHashFilePath, Encoding.UTF8); @@ -339,7 +342,9 @@ namespace Umbraco.Core.Composing var typesListFilePath = GetTypesListFilePath(); if (File.Exists(typesListFilePath) == false) + { return cache; + } using (var stream = GetFileStream(typesListFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, ListFileOpenReadTimeout)) using (var reader = new StreamReader(stream)) @@ -347,11 +352,21 @@ namespace Umbraco.Core.Composing while (true) { var baseType = reader.ReadLine(); - if (baseType == null) return cache; // exit - if (baseType.StartsWith("<")) break; // old xml + if (baseType == null) + { + return cache; // exit + } + + if (baseType.StartsWith("<")) + { + break; // old xml + } var attributeType = reader.ReadLine(); - if (attributeType == null) break; + if (attributeType == null) + { + break; + } var types = new List(); while (true) @@ -370,7 +385,10 @@ namespace Umbraco.Core.Composing types.Add(type); } - if (types == null) break; + if (types == null) + { + break; + } } } @@ -379,28 +397,31 @@ namespace Umbraco.Core.Composing } // internal for tests - internal string GetTypesListFilePath() => GetFileBasePath() + ".list"; + internal string GetTypesListFilePath() => _fileBasePath.Value + ".list"; - private string GetTypesHashFilePath() => GetFileBasePath() + ".hash"; + private string GetTypesHashFilePath() => _fileBasePath.Value + ".hash"; + /// + /// Used to produce the Lazy value of _fileBasePath + /// + /// private string GetFileBasePath() { - lock (_locko) + var fileBasePath = Path.Combine(_localTempPath, "TypesCache", "umbraco-types." + NetworkHelper.FileSafeMachineName); + + // ensure that the folder exists + var directory = Path.GetDirectoryName(fileBasePath); + if (directory == null) { - if (_fileBasePath != null) - return _fileBasePath; - - _fileBasePath = Path.Combine(_localTempPath, "TypesCache", "umbraco-types." + NetworkHelper.FileSafeMachineName); - - // ensure that the folder exists - var directory = Path.GetDirectoryName(_fileBasePath); - if (directory == null) - throw new InvalidOperationException($"Could not determine folder for path \"{_fileBasePath}\"."); - if (Directory.Exists(directory) == false) - Directory.CreateDirectory(directory); - - return _fileBasePath; + throw new InvalidOperationException($"Could not determine folder for path \"{fileBasePath}\"."); } + + if (Directory.Exists(directory) == false) + { + Directory.CreateDirectory(directory); + } + + return fileBasePath; } // internal for tests @@ -416,7 +437,10 @@ namespace Umbraco.Core.Composing writer.WriteLine(typeList.BaseType == null ? string.Empty : typeList.BaseType.FullName); writer.WriteLine(typeList.AttributeType == null ? string.Empty : typeList.AttributeType.FullName); foreach (var type in typeList.Types) + { writer.WriteLine(type.AssemblyQualifiedName); + } + writer.WriteLine(); } } @@ -434,16 +458,22 @@ namespace Umbraco.Core.Composing WriteCache(); } catch { /* bah - just don't die */ } - if (!_timing) _timer = null; + if (!_timing) + _timer = null; } } lock (_timerLock) { if (_timer == null) + { _timer = new Timer(TimerRelease, null, ListFileWriteThrottle, Timeout.Infinite); + } else + { _timer.Change(ListFileWriteThrottle, Timeout.Infinite); + } + _timing = true; } } @@ -476,7 +506,9 @@ namespace Umbraco.Core.Composing catch { if (--attempts == 0) + { throw; + } _logger.Debug("Attempted to get filestream for file {Path} failed, {NumberOfAttempts} attempts left, pausing for {PauseMilliseconds} milliseconds", path, attempts, pauseMilliseconds); Thread.Sleep(pauseMilliseconds); @@ -543,7 +575,8 @@ namespace Umbraco.Core.Composing /// attributeTypes public IEnumerable GetAssemblyAttributes(params Type[] attributeTypes) { - if (attributeTypes == null) throw new ArgumentNullException(nameof(attributeTypes)); + if (attributeTypes == null) + throw new ArgumentNullException(nameof(attributeTypes)); return AssembliesToScan.SelectMany(a => attributeTypes.SelectMany(at => a.GetCustomAttributes(at))).ToList(); } @@ -563,7 +596,9 @@ namespace Umbraco.Core.Composing public IEnumerable GetTypes(bool cache = true, IEnumerable specificAssemblies = null) { if (_logger == null) + { throw new InvalidOperationException("Cannot get types from a test/blank type loader."); + } // do not cache anything from specific assemblies cache &= specificAssemblies == null; @@ -583,7 +618,7 @@ namespace Umbraco.Core.Composing // get IDiscoverable and always cache var discovered = GetTypesInternal( - typeof (IDiscoverable), null, + typeof(IDiscoverable), null, () => TypeFinder.FindClassesOfType(AssembliesToScan), "scanning assemblies", true); @@ -594,9 +629,9 @@ namespace Umbraco.Core.Composing // filter the cached discovered types (and maybe cache the result) return GetTypesInternal( - typeof (T), null, + typeof(T), null, () => discovered - .Where(x => typeof (T).IsAssignableFrom(x)), + .Where(x => typeof(T).IsAssignableFrom(x)), "filtering IDiscoverable", cache); } @@ -614,7 +649,9 @@ namespace Umbraco.Core.Composing where TAttribute : Attribute { if (_logger == null) + { throw new InvalidOperationException("Cannot get types from a test/blank type loader."); + } // do not cache anything from specific assemblies cache &= specificAssemblies == null; @@ -633,7 +670,7 @@ namespace Umbraco.Core.Composing // get IDiscoverable and always cache var discovered = GetTypesInternal( - typeof (IDiscoverable), null, + typeof(IDiscoverable), null, () => TypeFinder.FindClassesOfType(AssembliesToScan), "scanning assemblies", true); @@ -644,7 +681,7 @@ namespace Umbraco.Core.Composing // filter the cached discovered types (and maybe cache the result) return GetTypesInternal( - typeof (T), typeof (TAttribute), + typeof(T), typeof(TAttribute), () => discovered .Where(x => typeof(T).IsAssignableFrom(x)) .Where(x => x.GetCustomAttributes(false).Any()), @@ -664,7 +701,9 @@ namespace Umbraco.Core.Composing where TAttribute : Attribute { if (_logger == null) + { throw new InvalidOperationException("Cannot get types from a test/blank type loader."); + } // do not cache anything from specific assemblies cache &= specificAssemblies == null; @@ -673,7 +712,7 @@ namespace Umbraco.Core.Composing _logger.Debug("Running a full, non-cached, scan for types / attribute {AttributeName} (slow).", typeof(TAttribute).FullName); return GetTypesInternal( - typeof (object), typeof (TAttribute), + typeof(object), typeof(TAttribute), () => TypeFinder.FindClassesWithAttribute(specificAssemblies ?? AssembliesToScan), "scanning assemblies", cache); @@ -693,12 +732,14 @@ namespace Umbraco.Core.Composing var name = GetName(baseType, attributeType); lock (_locko) - using (_logger.DebugDuration( + { + using (_logger.DebugDuration( "Getting " + name, "Got " + name)) // cannot contain typesFound.Count as it's evaluated before the find - { - // get within a lock & timer - return GetTypesInternalLocked(baseType, attributeType, finder, action, cache); + { + // get within a lock & timer + return GetTypesInternalLocked(baseType, attributeType, finder, action, cache); + } } } @@ -720,7 +761,9 @@ namespace Umbraco.Core.Composing var listKey = new CompositeTypeTypeKey(baseType ?? tobject, attributeType ?? tobject); TypeList typeList = null; if (cache) + { _types.TryGetValue(listKey, out typeList); // else null + } // if caching and found, return if (typeList != null) @@ -795,7 +838,9 @@ namespace Umbraco.Core.Composing _logger.Debug("Getting {TypeName}: " + action + ".", GetName(baseType, attributeType)); foreach (var t in finder()) + { typeList.Add(t); + } } // if we are to cache the results, do so @@ -807,7 +852,9 @@ namespace Umbraco.Core.Composing _types[listKey] = typeList; //if we are scanning then update the cache file if (scan) + { UpdateCache(); + } } _logger.Debug("Got {TypeName}, caching ({CacheType}).", GetName(baseType, attributeType), added.ToString().ToLowerInvariant()); diff --git a/src/Umbraco.Core/ConfigsExtensions.cs b/src/Umbraco.Core/ConfigsExtensions.cs index d1672c6c7f..10594fc970 100644 --- a/src/Umbraco.Core/ConfigsExtensions.cs +++ b/src/Umbraco.Core/ConfigsExtensions.cs @@ -1,10 +1,10 @@ using System.IO; using Umbraco.Core.Cache; -using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Grid; using Umbraco.Core.Configuration.HealthChecks; using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Dashboards; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; @@ -48,6 +48,8 @@ namespace Umbraco.Core configDir, factory.GetInstance(), factory.GetInstance().Debug)); + + configs.Add(() => new ContentDashboardSettings()); } } } diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index b7dce21285..c844abe75e 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -395,7 +395,6 @@ namespace Umbraco.Core.Configuration } } - /// /// An int value representing the time in milliseconds to lock the database for a write operation /// @@ -409,26 +408,34 @@ namespace Umbraco.Core.Configuration { if (_sqlWriteLockTimeOut != default) return _sqlWriteLockTimeOut; - var timeOut = 5000; // 5 seconds - var appSettingSqlWriteLockTimeOut = ConfigurationManager.AppSettings[Constants.AppSettings.SqlWriteLockTimeOut]; - if(int.TryParse(appSettingSqlWriteLockTimeOut, out var configuredTimeOut)) - { - // Only apply this setting if it's not excessively high or low - const int minimumTimeOut = 100; - const int maximumTimeOut = 20000; - if (configuredTimeOut >= minimumTimeOut && configuredTimeOut <= maximumTimeOut) // between 0.1 and 20 seconds - { - timeOut = configuredTimeOut; - } - else - { - Current.Logger.Warn($"The `{Constants.AppSettings.SqlWriteLockTimeOut}` setting in web.config is not between the minimum of {minimumTimeOut} ms and maximum of {maximumTimeOut} ms, defaulting back to {timeOut}"); - } - } + var timeOut = GetSqlWriteLockTimeoutFromConfigFile(Current.Logger); _sqlWriteLockTimeOut = timeOut; return _sqlWriteLockTimeOut; } } + + internal static int GetSqlWriteLockTimeoutFromConfigFile(ILogger logger) + { + var timeOut = 5000; // 5 seconds + var appSettingSqlWriteLockTimeOut = ConfigurationManager.AppSettings[Constants.AppSettings.SqlWriteLockTimeOut]; + if (int.TryParse(appSettingSqlWriteLockTimeOut, out var configuredTimeOut)) + { + // Only apply this setting if it's not excessively high or low + const int minimumTimeOut = 100; + const int maximumTimeOut = 20000; + if (configuredTimeOut >= minimumTimeOut && configuredTimeOut <= maximumTimeOut) // between 0.1 and 20 seconds + { + timeOut = configuredTimeOut; + } + else + { + logger.Warn( + $"The `{Constants.AppSettings.SqlWriteLockTimeOut}` setting in web.config is not between the minimum of {minimumTimeOut} ms and maximum of {maximumTimeOut} ms, defaulting back to {timeOut}"); + } + } + + return timeOut; + } } } diff --git a/src/Umbraco.Core/Constants-AppSettings.cs b/src/Umbraco.Core/Constants-AppSettings.cs index f04f0e1f5f..1f096ab9f9 100644 --- a/src/Umbraco.Core/Constants-AppSettings.cs +++ b/src/Umbraco.Core/Constants-AppSettings.cs @@ -110,6 +110,11 @@ namespace Umbraco.Core /// public const string UseHttps = "Umbraco.Core.UseHttps"; + /// + /// A true/false value indicating whether the content dashboard should be visible for all user groups. + /// + public const string AllowContentDashboardAccessToAllUsers = "Umbraco.Core.AllowContentDashboardAccessToAllUsers"; + /// /// TODO: FILL ME IN /// diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs index f900288ef5..2b6244debb 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -25,7 +25,7 @@ namespace Umbraco.Core /// /// The name of the 'unknown' user. /// - public const string UnknownUserName = "SYTEM"; + public const string UnknownUserName = "SYSTEM"; public const string AdminGroupAlias = "admin"; public const string EditorGroupAlias = "editor"; diff --git a/src/Umbraco.Core/Constants-SvgSanitizer.cs b/src/Umbraco.Core/Constants-SvgSanitizer.cs deleted file mode 100644 index c92b9f56c7..0000000000 --- a/src/Umbraco.Core/Constants-SvgSanitizer.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; - -namespace Umbraco.Core -{ - public static partial class Constants - { - /// - /// Defines the alias identifiers for Umbraco's core application sections. - /// - public static class SvgSanitizer - { - /// - /// Allowlist for SVG attributes. - /// - public static readonly IList Attributes = new [] { "accent-height", "accumulate", "additive", "alignment-baseline", "allowReorder", "alphabetic", "amplitude", "arabic-form", "ascent", "attributeName", "attributeType", "autoReverse", "azimuth", "baseFrequency", "baseline-shift", "baseProfile", "bbox", "begin", "bias", "by", "calcMode", "cap-height", "class", "clip", "clipPathUnits", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "contentScriptType", "contentStyleType", "cursor", "cx", "cy", "d", "decelerate", "descent", "diffuseConstant", "direction", "display", "divisor", "dominant-baseline", "dur", "dx", "dy", "edgeMode", "elevation", "enable-background", "end", "exponent", "externalResourcesRequired", "Section", "fill", "fill-opacity", "fill-rule", "filter", "filterRes", "filterUnits", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "format", "from", "fr", "fx", "fy", "g1", "g2", "glyph-name", "glyph-orientation-horizontal", "glyph-orientation-vertical", "glyphRef", "gradientTransform", "gradientUnits", "hanging", "height", "href", "hreflang", "horiz-adv-x", "horiz-origin-x", "ISection", "id", "ideographic", "image-rendering", "in", "in2", "intercept", "k", "k1", "k2", "k3", "k4", "kernelMatrix", "kernelUnitLength", "kerning", "keyPoints", "keySplines", "keyTimes", "lang", "lengthAdjust", "letter-spacing", "lighting-color", "limitingConeAngle", "local", "MSection", "marker-end", "marker-mid", "marker-start", "markerHeight", "markerUnits", "markerWidth", "mask", "maskContentUnits", "maskUnits", "mathematical", "max", "media", "method", "min", "mode", "NSection", "name", "numOctaves", "offset", "opacity", "operator", "order", "orient", "orientation", "origin", "overflow", "overline-position", "overline-thickness", "panose-1", "paint-order", "path", "pathLength", "patternContentUnits", "patternTransform", "patternUnits", "ping", "pointer-events", "points", "pointsAtX", "pointsAtY", "pointsAtZ", "preserveAlpha", "preserveAspectRatio", "primitiveUnits", "r", "radius", "referrerPolicy", "refX", "refY", "rel", "rendering-intent", "repeatCount", "repeatDur", "requiredExtensions", "requiredFeatures", "restart", "result", "rotate", "rx", "ry", "scale", "seed", "shape-rendering", "slope", "spacing", "specularConstant", "specularExponent", "speed", "spreadMethod", "startOffset", "stdDeviation", "stemh", "stemv", "stitchTiles", "stop-color", "stop-opacity", "strikethrough-position", "strikethrough-thickness", "string", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "style", "surfaceScale", "systemLanguage", "tabindex", "tableValues", "target", "targetX", "targetY", "text-anchor", "text-decoration", "text-rendering", "textLength", "to", "transform", "type", "u1", "u2", "underline-position", "underline-thickness", "unicode", "unicode-bidi", "unicode-range", "units-per-em", "v-alphabetic", "v-hanging", "v-ideographic", "v-mathematical", "values", "vector-effect", "version", "vert-adv-y", "vert-origin-x", "vert-origin-y", "viewBox", "viewTarget", "visibility", "width", "widths", "word-spacing", "writing-mode", "x", "x-height", "x1", "x2", "xChannelSelector", "xlink:actuate", "xlink:arcrole", "xlink:href", "xlink:role", "xlink:show", "xlink:title", "xlink:type", "xml:base", "xml:lang", "xml:space", "y", "y1", "y2", "yChannelSelector", "z", "zoomAndPan" }; - - /// - /// Allowlist for SVG tabs. - /// - public static readonly IList Tags = new [] { "a", "altGlyph", "altGlyphDef", "altGlyphItem", "animate", "animateColor", "animateMotion", "animateTransform", "circle", "clipPath", "color-profile", "cursor", "defs", "desc", "discard", "ellipse", "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence", "filter", "font", "font-face", "font-face-format", "font-face-name", "font-face-src", "font-face-uri", "foreignObject", "g", "glyph", "glyphRef", "hatch", "hatchpath", "hkern", "image", "line", "linearGradient", "marker", "mask", "mesh", "meshgradient", "meshpatch", "meshrow", "metadata", "missing-glyph", "mpath", "path", "pattern", "polygon", "polyline", "radialGradient", "rect", "set", "solidcolor", "stop", "svg", "switch", "symbol", "text", "textPath", "title", "tref", "tspan", "unknown", "use", "view", "vkern" }; - } - } -} diff --git a/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs b/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs new file mode 100644 index 0000000000..f8fb5c7b06 --- /dev/null +++ b/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs @@ -0,0 +1,24 @@ +using System.Configuration; + +namespace Umbraco.Core.Dashboards +{ + public class ContentDashboardSettings: IContentDashboardSettings + { + + /// + /// Gets a value indicating whether the content dashboard should be available to all users. + /// + /// + /// true if the dashboard is visible for all user groups; otherwise, false + /// and the default access rules for that dashboard will be in use. + /// + public bool AllowContentDashboardAccessToAllUsers + { + get + { + bool.TryParse(ConfigurationManager.AppSettings[Constants.AppSettings.AllowContentDashboardAccessToAllUsers], out var value); + return value; + } + } + } +} diff --git a/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs b/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs new file mode 100644 index 0000000000..862a28b90e --- /dev/null +++ b/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Core.Dashboards +{ + public interface IContentDashboardSettings + { + /// + /// Gets a value indicating whether the content dashboard should be available to all users. + /// + /// + /// true if the dashboard is visible for all user groups; otherwise, false + /// and the default access rules for that dashboard will be in use. + /// + bool AllowContentDashboardAccessToAllUsers { get; } + } +} diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs index 8661f73fb1..69ce50de9c 100644 --- a/src/Umbraco.Core/IO/IOHelper.cs +++ b/src/Umbraco.Core/IO/IOHelper.cs @@ -81,6 +81,7 @@ namespace Umbraco.Core.IO public static string MapPath(string path, bool useHttpContext) { if (path == null) throw new ArgumentNullException("path"); + useHttpContext = useHttpContext && IsHosted; // Check if the path is already mapped @@ -89,10 +90,8 @@ namespace Umbraco.Core.IO { return path; } - // Check that we even have an HttpContext! otherwise things will fail anyways - // http://umbraco.codeplex.com/workitem/30946 - if (useHttpContext && HttpContext.Current != null) + if (useHttpContext) { //string retval; if (String.IsNullOrEmpty(path) == false && (path.StartsWith("~") || path.StartsWith(SystemDirectories.Root))) diff --git a/src/Umbraco.Core/Mapping/UmbracoMapper.cs b/src/Umbraco.Core/Mapping/UmbracoMapper.cs index e62825101c..36e3f9eab9 100644 --- a/src/Umbraco.Core/Mapping/UmbracoMapper.cs +++ b/src/Umbraco.Core/Mapping/UmbracoMapper.cs @@ -3,7 +3,9 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Composing; using Umbraco.Core.Exceptions; +using Umbraco.Core.Scoping; namespace Umbraco.Core.Mapping { @@ -42,16 +44,29 @@ namespace Umbraco.Core.Mapping private readonly ConcurrentDictionary>> _maps = new ConcurrentDictionary>>(); + private readonly IScopeProvider _scopeProvider; + /// /// Initializes a new instance of the class. /// /// - public UmbracoMapper(MapDefinitionCollection profiles) + /// + public UmbracoMapper(MapDefinitionCollection profiles, IScopeProvider scopeProvider) { + _scopeProvider = scopeProvider; + foreach (var profile in profiles) profile.DefineMaps(this); } + /// + /// Initializes a new instance of the class. + /// + /// + [Obsolete("This constructor is no longer used and will be removed in future versions, use the other constructor instead")] + public UmbracoMapper(MapDefinitionCollection profiles) : this(profiles, Current.ScopeProvider) + {} + #region Define private static TTarget ThrowCtor(TSource source, MapperContext context) @@ -203,7 +218,10 @@ namespace Umbraco.Core.Mapping if (ctor != null && map != null) { var target = ctor(source, context); - map(source, target, context); + using (var scope = _scopeProvider.CreateScope(autoComplete: true)) + { + map(source, target, context); + } return (TTarget)target; } @@ -248,11 +266,14 @@ namespace Umbraco.Core.Mapping { var targetList = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(targetGenericArg)); - foreach (var sourceItem in source) + using (var scope = _scopeProvider.CreateScope(autoComplete: true)) { - var targetItem = ctor(sourceItem, context); - map(sourceItem, targetItem, context); - targetList.Add(targetItem); + foreach (var sourceItem in source) + { + var targetItem = ctor(sourceItem, context); + map(sourceItem, targetItem, context); + targetList.Add(targetItem); + } } object target = targetList; @@ -315,7 +336,10 @@ namespace Umbraco.Core.Mapping // if there is a direct map, map if (map != null) { - map(source, target, context); + using (var scope = _scopeProvider.CreateScope(autoComplete: true)) + { + map(source, target, context); + } return target; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs index 365e8ba481..95f699d952 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UpgradeCheckRepository.cs @@ -24,6 +24,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return result ?? new UpgradeResult("None", "", ""); } + catch (UnsupportedMediaTypeException) + { + // this occurs if the server for Our is up but doesn't return a valid result (ex. content type) + return new UpgradeResult("None", "", ""); + } catch (HttpRequestException) { // this occurs if the server for Our is down or cannot be reached diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index d930841289..d784560f2c 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -179,7 +179,14 @@ namespace Umbraco.Core.Runtime _listenTask = _mainDomLock.ListenAsync(); _listenCompleteTask = _listenTask.ContinueWith(t => { - _logger.Debug("Listening task completed with {TaskStatus}", _listenTask.Status); + if (_listenTask.Exception != null) + { + _logger.Warn("Listening task completed with {TaskStatus}, Exception: {Exception}", _listenTask.Status, _listenTask.Exception); + } + else + { + _logger.Debug("Listening task completed with {TaskStatus}", _listenTask.Status); + } OnSignal("signal"); }, TaskScheduler.Default); // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index d98d62cb20..12359c96d1 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -1,5 +1,6 @@ using NPoco; using System; +using System.Configuration; using System.Data; using System.Data.SqlClient; using System.Diagnostics; @@ -7,6 +8,7 @@ using System.Linq; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; +using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; @@ -18,6 +20,7 @@ namespace Umbraco.Core.Runtime { internal class SqlMainDomLock : IMainDomLock { + private readonly TimeSpan _lockTimeout; private string _lockId; private const string MainDomKeyPrefix = "Umbraco.Core.Runtime.SqlMainDom"; private const string UpdatedSuffix = "_updated"; @@ -40,6 +43,8 @@ namespace Umbraco.Core.Runtime Constants.System.UmbracoConnectionName, _logger, new Lazy(() => new MapperCollection(Enumerable.Empty()))); + + _lockTimeout = TimeSpan.FromMilliseconds(GlobalSettings.GetSqlWriteLockTimeoutFromConfigFile(logger)); } public async Task AcquireLockAsync(int millisecondsTimeout) @@ -81,7 +86,7 @@ namespace Umbraco.Core.Runtime // wait to get a write lock _sqlServerSyntax.WriteLock(db, TimeSpan.FromMilliseconds(millisecondsTimeout), Constants.Locks.MainDom); } - catch(SqlException ex) + catch (SqlException ex) { if (IsLockTimeoutException(ex)) { @@ -121,7 +126,7 @@ namespace Umbraco.Core.Runtime } - return await WaitForExistingAsync(tempId, millisecondsTimeout); + return await WaitForExistingAsync(tempId, millisecondsTimeout).ConfigureAwait(false); } public Task ListenAsync() @@ -134,13 +139,15 @@ namespace Umbraco.Core.Runtime // Create a long running task (dedicated thread) // to poll to check if we are still the MainDom registered in the DB - return Task.Factory.StartNew( - ListeningLoop, - _cancellationTokenSource.Token, - TaskCreationOptions.LongRunning, - // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html - TaskScheduler.Default); - + using (ExecutionContext.SuppressFlow()) + { + return Task.Factory.StartNew( + ListeningLoop, + _cancellationTokenSource.Token, + TaskCreationOptions.LongRunning, + // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html + TaskScheduler.Default); + } } /// @@ -198,7 +205,7 @@ namespace Umbraco.Core.Runtime db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + _sqlServerSyntax.ReadLock(db, _lockTimeout, Constants.Locks.MainDom); if (!IsMainDomValue(_lockId, db)) { @@ -222,11 +229,29 @@ namespace Umbraco.Core.Runtime } finally { - db?.CompleteTransaction(); - db?.Dispose(); + // Even if any of the above fail like BeginTransaction, or even a query after the + // Transaction is started, the calls below will not throw. I've tried all sorts of + // combinations to see if I can make this throw but I can't. In any case, we'll be + // extra safe and try/catch/log + try + { + db?.CompleteTransaction(); + } + catch (Exception ex) + { + _logger.Error(ex, "Unexpected error completing transaction."); + } + + try + { + db?.Dispose(); + } + catch (Exception ex) + { + _logger.Error(ex, "Unexpected error completing disposing."); + } } } - } } @@ -240,37 +265,40 @@ namespace Umbraco.Core.Runtime { var updatedTempId = tempId + UpdatedSuffix; - return Task.Run(() => + using (ExecutionContext.SuppressFlow()) { - try + return Task.Run(() => { - using var db = _dbFactory.CreateDatabase(); - - var watch = new Stopwatch(); - watch.Start(); - while (true) + try { - // poll very often, we need to take over as fast as we can - // local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO - Thread.Sleep(1000); + using var db = _dbFactory.CreateDatabase(); - var acquired = TryAcquire(db, tempId, updatedTempId); - if (acquired.HasValue) - return acquired.Value; - - if (watch.ElapsedMilliseconds >= millisecondsTimeout) + var watch = new Stopwatch(); + watch.Start(); + while (true) { - return AcquireWhenMaxWaitTimeElapsed(db); + // poll very often, we need to take over as fast as we can + // local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO + Thread.Sleep(1000); + + var acquired = TryAcquire(db, tempId, updatedTempId); + if (acquired.HasValue) + return acquired.Value; + + if (watch.ElapsedMilliseconds >= millisecondsTimeout) + { + return AcquireWhenMaxWaitTimeElapsed(db); + } } } - } - catch (Exception ex) - { - _logger.Error(ex, "An error occurred trying to acquire and waiting for existing SqlMainDomLock to shutdown"); - return false; - } + catch (Exception ex) + { + _logger.Error(ex, "An error occurred trying to acquire and waiting for existing SqlMainDomLock to shutdown"); + return false; + } - }, _cancellationTokenSource.Token); + }, _cancellationTokenSource.Token); + } } private bool? TryAcquire(IUmbracoDatabase db, string tempId, string updatedTempId) @@ -284,7 +312,7 @@ namespace Umbraco.Core.Runtime { transaction = db.GetTransaction(IsolationLevel.ReadCommitted); // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + _sqlServerSyntax.ReadLock(db, _lockTimeout, Constants.Locks.MainDom); // the row var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); @@ -296,7 +324,7 @@ namespace Umbraco.Core.Runtime // which indicates that we // can acquire it and it has shutdown. - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, _lockTimeout, Constants.Locks.MainDom); // so now we update the row with our appdomain id InsertLockRecord(_lockId, db); @@ -355,7 +383,7 @@ namespace Umbraco.Core.Runtime { transaction = db.GetTransaction(IsolationLevel.ReadCommitted); - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, _lockTimeout, Constants.Locks.MainDom); // so now we update the row with our appdomain id InsertLockRecord(_lockId, db); @@ -438,7 +466,7 @@ namespace Umbraco.Core.Runtime db.BeginTransaction(IsolationLevel.ReadCommitted); // get a write lock - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, _lockTimeout, Constants.Locks.MainDom); // When we are disposed, it means we have released the MainDom lock // and called all MainDom release callbacks, in this case diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index c4c8d08622..4c058cbdb7 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Data; +using System.Text; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Events; @@ -36,11 +37,10 @@ namespace Umbraco.Core.Scoping private IEventDispatcher _eventDispatcher; private object _dictionaryLocker; - - // ReadLocks and WriteLocks if we're the outer most scope it's those owned by the entire chain - // If we're a child scope it's those that we have requested. - internal readonly Dictionary ReadLocks; - internal readonly Dictionary WriteLocks; + private HashSet _readLocks; + private HashSet _writeLocks; + internal Dictionary> ReadLocks; + internal Dictionary> WriteLocks; // initializes a new scope private Scope(ScopeProvider scopeProvider, @@ -67,8 +67,6 @@ namespace Umbraco.Core.Scoping Detachable = detachable; _dictionaryLocker = new object(); - ReadLocks = new Dictionary(); - WriteLocks = new Dictionary(); #if DEBUG_SCOPES _scopeProvider.RegisterScope(this); @@ -345,6 +343,8 @@ namespace Umbraco.Core.Scoping if (this != _scopeProvider.AmbientScope) { + var failedMessage = $"The {nameof(Scope)} {this.InstanceId} being disposed is not the Ambient {nameof(Scope)} {(_scopeProvider.AmbientScope?.InstanceId.ToString() ?? "NULL")}. This typically indicates that a child {nameof(Scope)} was not disposed, or flowed to a child thread that was not awaited, or concurrent threads are accessing the same {nameof(Scope)} (Ambient context) which is not supported. If using Task.Run (or similar) as a fire and forget tasks or to run threads in parallel you must suppress execution context flow with ExecutionContext.SuppressFlow() and ExecutionContext.RestoreFlow()."; + #if DEBUG_SCOPES var ambient = _scopeProvider.AmbientScope; _logger.Debug("Dispose error (" + (ambient == null ? "no" : "other") + " ambient)"); @@ -356,24 +356,21 @@ namespace Umbraco.Core.Scoping + "- ambient ctor ->\r\n" + ambientInfos.CtorStack + "\r\n" + "- dispose ctor ->\r\n" + disposeInfos.CtorStack + "\r\n"); #else - throw new InvalidOperationException("Not the ambient scope."); + throw new InvalidOperationException(failedMessage); #endif } // Decrement the lock counters on the parent if any. - if (ParentScope != null) + ClearLocks(InstanceId); + if (ParentScope is null) { - lock (_dictionaryLocker) + // We're the parent scope, make sure that locks of all scopes has been cleared + // Since we're only reading we don't have to be in a lock + if (ReadLocks?.Count > 0 || WriteLocks?.Count > 0) { - foreach (var readLockPair in ReadLocks) - { - DecrementReadLock(readLockPair.Key, readLockPair.Value); - } - - foreach (var writeLockPair in WriteLocks) - { - DecrementWriteLock(writeLockPair.Key, writeLockPair.Value); - } + var exception = new InvalidOperationException($"All scopes has not been disposed from parent scope: {InstanceId}, see log for more details."); + _logger.Error(exception, GenerateUnclearedScopesLogMessage()); + throw exception; } } @@ -396,6 +393,42 @@ namespace Umbraco.Core.Scoping GC.SuppressFinalize(this); } + /// + /// Generates a log message with all scopes that hasn't cleared their locks, including how many, and what locks they have requested. + /// + /// Log message. + private string GenerateUnclearedScopesLogMessage() + { + // Dump the dicts into a message for the locks. + StringBuilder builder = new StringBuilder(); + builder.AppendLine($"Lock counters aren't empty, suggesting a scope hasn't been properly disposed, parent id: {InstanceId}"); + WriteLockDictionaryToString(ReadLocks, builder, "read locks"); + WriteLockDictionaryToString(WriteLocks, builder, "write locks"); + return builder.ToString(); + } + + /// + /// Writes a locks dictionary to a for logging purposes. + /// + /// Lock dictionary to report on. + /// String builder to write to. + /// The name to report the dictionary as. + private void WriteLockDictionaryToString(Dictionary> dict, StringBuilder builder, string dictName) + { + if (dict?.Count > 0) + { + builder.AppendLine($"Remaining {dictName}:"); + foreach (var instance in dict) + { + builder.AppendLine($"Scope {instance.Key}"); + foreach (var lockCounter in instance.Value) + { + builder.AppendLine($"\tLock ID: {lockCounter.Key} - times requested: {lockCounter.Value}"); + } + } + } + } + private void DisposeLastScope() { // figure out completed @@ -516,207 +549,157 @@ namespace Umbraco.Core.Scoping ?? (_logUncompletedScopes = Current.Configs.CoreDebug().LogUncompletedScopes)).Value; /// - /// Decrements the count of the ReadLocks with a specific lock object identifier we currently hold + /// Increment the counter of a locks dictionary, either ReadLocks or WriteLocks, + /// for a specific scope instance and lock identifier. Must be called within a lock. /// - /// Lock object identifier to decrement - /// Amount to decrement the lock count with - public void DecrementReadLock(int lockId, int amountToDecrement) + /// Lock ID to increment. + /// Instance ID of the scope requesting the lock. + /// Reference to the dictionary to increment on + private void IncrementLock(int lockId, Guid instanceId, ref Dictionary> locks) { - // If we aren't the outermost scope, pass it on to the parent. - if (ParentScope != null) - { - ParentScope.DecrementReadLock(lockId, amountToDecrement); - return; - } + // Since we've already checked that we're the parent in the WriteLockInner method, we don't need to check again. + // If it's the very first time a lock has been requested the WriteLocks dict hasn't been instantiated yet. + locks ??= new Dictionary>(); - lock (_dictionaryLocker) + // Try and get the dict associated with the scope id. + var locksDictFound = locks.TryGetValue(instanceId, out var locksDict); + if (locksDictFound) { - ReadLocks[lockId] -= amountToDecrement; + locksDict.TryGetValue(lockId, out var value); + locksDict[lockId] = value + 1; + } + else + { + // The scope hasn't requested a lock yet, so we have to create a dict for it. + locks.Add(instanceId, new Dictionary()); + locks[instanceId][lockId] = 1; } } /// - /// Decrements the count of the WriteLocks with a specific lock object identifier we currently hold. + /// Clears all lock counters for a given scope instance, signalling that the scope has been disposed. /// - /// Lock object identifier to decrement. - /// Amount to decrement the lock count with - public void DecrementWriteLock(int lockId, int amountToDecrement) + /// Instance ID of the scope to clear. + private void ClearLocks(Guid instanceId) { - // If we aren't the outermost scope, pass it on to the parent. - if (ParentScope != null) + if (ParentScope is not null) { - ParentScope.DecrementWriteLock(lockId, amountToDecrement); - return; + ParentScope.ClearLocks(instanceId); } - - lock (_dictionaryLocker) + else { - WriteLocks[lockId] -= amountToDecrement; - } - } - - /// - /// Increment the count of the read locks we've requested - /// - /// - /// This should only be done on child scopes since it's then used to decrement the count later. - /// - /// - private void IncrementRequestedReadLock(params int[] lockIds) - { - // We need to keep track of what lockIds we have requested locks for to be able to decrement them. - if (ParentScope != null) - { - foreach (var lockId in lockIds) + lock (_dictionaryLocker) { - lock (_dictionaryLocker) - { - if (ReadLocks.ContainsKey(lockId)) - { - ReadLocks[lockId] += 1; - } - else - { - ReadLocks[lockId] = 1; - } - } - } - } - } - - /// - /// Increment the count of the write locks we've requested - /// - /// - /// This should only be done on child scopes since it's then used to decrement the count later. - /// - /// - private void IncrementRequestedWriteLock(params int[] lockIds) - { - // We need to keep track of what lockIds we have requested locks for to be able to decrement them. - if (ParentScope != null) - { - foreach (var lockId in lockIds) - { - lock (_dictionaryLocker) - { - if (WriteLocks.ContainsKey(lockId)) - { - WriteLocks[lockId] += 1; - } - else - { - WriteLocks[lockId] = 1; - } - } + ReadLocks?.Remove(instanceId); + WriteLocks?.Remove(instanceId); } } } /// - public void ReadLock(params int[] lockIds) - { - IncrementRequestedReadLock(lockIds); - ReadLockInner(null, lockIds); - } + public void ReadLock(params int[] lockIds) => ReadLockInner(InstanceId, null, lockIds); /// - public void ReadLock(TimeSpan timeout, int lockId) - { - IncrementRequestedReadLock(lockId); - ReadLockInner(timeout, lockId); - } + public void ReadLock(TimeSpan timeout, int lockId) => ReadLockInner(InstanceId, timeout, lockId); /// - public void WriteLock(params int[] lockIds) - { - IncrementRequestedWriteLock(lockIds); - WriteLockInner(null, lockIds); - } + public void WriteLock(params int[] lockIds) => WriteLockInner(InstanceId, null, lockIds); /// - public void WriteLock(TimeSpan timeout, int lockId) - { - IncrementRequestedWriteLock(lockId); - WriteLockInner(timeout, lockId); - } + public void WriteLock(TimeSpan timeout, int lockId) => WriteLockInner(InstanceId, timeout, lockId); /// /// Handles acquiring a read lock, will delegate it to the parent if there are any. /// + /// Instance ID of the requesting scope. /// Optional database timeout in milliseconds. /// Array of lock object identifiers. - internal void ReadLockInner(TimeSpan? timeout = null, params int[] lockIds) + private void ReadLockInner(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds) { - if (ParentScope != null) + if (ParentScope is not null) { - // Delegate acquiring the lock to the parent if any. - ParentScope.ReadLockInner(timeout, lockIds); - return; + // If we have a parent we delegate lock creation to parent. + ParentScope.ReadLockInner(instanceId, timeout, lockIds); } - - // If we are the parent, then handle the lock request. - foreach (var lockId in lockIds) + else { - lock (_dictionaryLocker) - { - // Only acquire the lock if we haven't done so yet. - if (!ReadLocks.ContainsKey(lockId)) - { - if (timeout is null) - { - // We want a lock with a custom timeout - ObtainReadLock(lockId); - } - else - { - // We just want an ordinary lock. - ObtainTimoutReadLock(lockId, timeout.Value); - } - // Add the lockId as a key to the dict. - ReadLocks[lockId] = 0; - } - - ReadLocks[lockId] += 1; - } + // We are the outermost scope, handle the lock request. + LockInner(instanceId, ref ReadLocks, ref _readLocks, ObtainReadLock, ObtainTimeoutReadLock, timeout, lockIds); } } /// /// Handles acquiring a write lock with a specified timeout, will delegate it to the parent if there are any. /// + /// Instance ID of the requesting scope. /// Optional database timeout in milliseconds. /// Array of lock object identifiers. - internal void WriteLockInner(TimeSpan? timeout = null, params int[] lockIds) + private void WriteLockInner(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds) { - if (ParentScope != null) + if (ParentScope is not null) { // If we have a parent we delegate lock creation to parent. - ParentScope.WriteLockInner(timeout, lockIds); - return; + ParentScope.WriteLockInner(instanceId, timeout, lockIds); } - - foreach (var lockId in lockIds) + else { - lock (_dictionaryLocker) - { - // Only acquire lock if we haven't yet (WriteLocks not containing the key) - if (!WriteLocks.ContainsKey(lockId)) - { - if (timeout is null) - { - ObtainWriteLock(lockId); - } - else - { - ObtainTimeoutWriteLock(lockId, timeout.Value); - } - // Add the lockId as a key to the dict. - WriteLocks[lockId] = 0; - } + // We are the outermost scope, handle the lock request. + LockInner(instanceId, ref WriteLocks, ref _writeLocks, ObtainWriteLock, ObtainTimeoutWriteLock, timeout, lockIds); + } + } - // Increment count of the lock by 1. - WriteLocks[lockId] += 1; + /// + /// Handles acquiring a lock, this should only be called from the outermost scope. + /// + /// Instance ID of the scope requesting the lock. + /// Reference to the applicable locks dictionary (ReadLocks or WriteLocks). + /// Reference to the applicable locks hashset (_readLocks or _writeLocks). + /// Delegate used to request the lock from the database without a timeout. + /// Delegate used to request the lock from the database with a timeout. + /// Optional timeout parameter to specify a timeout. + /// Lock identifiers to lock on. + private void LockInner(Guid instanceId, ref Dictionary> locks, ref HashSet locksSet, + Action obtainLock, Action obtainLockTimeout, TimeSpan? timeout = null, + params int[] lockIds) + { + lock (_dictionaryLocker) + { + locksSet ??= new HashSet(); + foreach (var lockId in lockIds) + { + // Only acquire the lock if we haven't done so yet. + if (!locksSet.Contains(lockId)) + { + IncrementLock(lockId, instanceId, ref locks); + locksSet.Add(lockId); + try + { + if (timeout is null) + { + // We just want an ordinary lock. + obtainLock(lockId); + } + else + { + // We want a lock with a custom timeout + obtainLockTimeout(lockId, timeout.Value); + } + } + catch + { + // Something went wrong and we didn't get the lock + // Since we at this point have determined that we haven't got any lock with an ID of LockID, it's safe to completely remove it instead of decrementing. + locks[instanceId].Remove(lockId); + // It needs to be removed from the HashSet as well, because that's how we determine to acquire a lock. + locksSet.Remove(lockId); + throw; + } + } + else + { + // We already have a lock, but need to update the dictionary for debugging purposes. + IncrementLock(lockId, instanceId, ref locks); + } } } } @@ -735,10 +718,10 @@ namespace Umbraco.Core.Scoping /// /// Lock object identifier to lock. /// TimeSpan specifying the timout period. - private void ObtainTimoutReadLock(int lockId, TimeSpan timeout) + private void ObtainTimeoutReadLock(int lockId, TimeSpan timeout) { var syntax2 = Database.SqlContext.SqlSyntax as ISqlSyntaxProvider2; - if (syntax2 == null) + if (syntax2 is null) { throw new InvalidOperationException($"{Database.SqlContext.SqlSyntax.GetType()} is not of type {typeof(ISqlSyntaxProvider2)}"); } @@ -763,7 +746,7 @@ namespace Umbraco.Core.Scoping private void ObtainTimeoutWriteLock(int lockId, TimeSpan timeout) { var syntax2 = Database.SqlContext.SqlSyntax as ISqlSyntaxProvider2; - if (syntax2 == null) + if (syntax2 is null) { throw new InvalidOperationException($"{Database.SqlContext.SqlSyntax.GetType()} is not of type {typeof(ISqlSyntaxProvider2)}"); } diff --git a/src/Umbraco.Core/Scoping/ScopeProvider.cs b/src/Umbraco.Core/Scoping/ScopeProvider.cs index bf4e27bdb6..a1cc128181 100644 --- a/src/Umbraco.Core/Scoping/ScopeProvider.cs +++ b/src/Umbraco.Core/Scoping/ScopeProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Data; using System.Runtime.Remoting.Messaging; using System.Web; @@ -240,6 +241,9 @@ namespace Umbraco.Core.Scoping var value = GetHttpContextObject(ContextItemKey, false); return value ?? GetCallContextObject(ContextItemKey); } + + [Obsolete("This setter is not used and will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] set { // clear both diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index e5363d0e2b..d8e99663ea 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1415,7 +1415,7 @@ namespace Umbraco.Core.Services.Implement var result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs.Value, d.WriterId); if (result.Success == false) - Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); results.Add(result); } @@ -2201,7 +2201,7 @@ namespace Umbraco.Core.Services.Implement while (page * pageSize < total) { var descendants = GetPagedDescendants(content.Id, page++, pageSize, out total); - foreach (var descendant in descendants) + foreach (var descendant in descendants.OrderBy(x => x.Level).ThenBy(y => y.SortOrder)) { // if parent has not been copied, skip, else gets its copy id if (idmap.TryGetValue(descendant.ParentId, out parentId) == false) continue; @@ -2420,7 +2420,7 @@ namespace Umbraco.Core.Services.Implement if (report.FixedIssues.Count > 0) { //The event args needs a content item so we'll make a fake one with enough properties to not cause a null ref - var root = new Content("root", -1, new ContentType(-1)) {Id = -1, Key = Guid.Empty}; + var root = new Content("root", -1, new ContentType(-1)) { Id = -1, Key = Guid.Empty }; scope.Events.Dispatch(TreeChanged, this, new TreeChange.EventArgs(new TreeChange(root, TreeChangeTypes.RefreshAll))); } @@ -3169,7 +3169,7 @@ namespace Umbraco.Core.Services.Implement if (rollbackSaveResult.Success == false) { //Log the error/warning - Logger.Error("User '{UserId}' was unable to rollback content '{ContentId}' to version '{VersionId}'", userId, id, versionId); + Logger.Error("User '{UserId}' was unable to rollback content '{ContentId}' to version '{VersionId}'", userId, id, versionId); } else { @@ -3178,7 +3178,7 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(RolledBack, this, rollbackEventArgs); //Logging & Audit message - Logger.Info("User '{UserId}' rolled back content '{ContentId}' to version '{VersionId}'", userId, id, versionId); + Logger.Info("User '{UserId}' rolled back content '{ContentId}' to version '{VersionId}'", userId, id, versionId); Audit(AuditType.RollBack, userId, id, $"Content '{content.Name}' was rolled back to version '{versionId}'"); } diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 80ef81f36d..0e16c2c852 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -22,7 +22,7 @@ namespace Umbraco.Core ///
    public static class StringExtensions { - + private const char DefaultEscapedStringEscapeChar = '\\'; private static readonly char[] ToCSharpHexDigitLower = "0123456789abcdef".ToCharArray(); private static readonly char[] ToCSharpEscapeChars; @@ -1490,5 +1490,44 @@ namespace Umbraco.Core ///
    public static string NullOrWhiteSpaceAsNull(this string text) => string.IsNullOrWhiteSpace(text) ? null : text; + + /// + /// Splits a string with an escape character that allows for the split character to exist in a string + /// + /// The string to split + /// The character to split on + /// The character which can be used to escape the character to split on + /// The string split into substrings delimited by the split character + public static IEnumerable EscapedSplit(this string value, char splitChar, char escapeChar = DefaultEscapedStringEscapeChar) + { + if (value == null) yield break; + + var sb = new StringBuilder(value.Length); + var escaped = false; + + foreach (var chr in value.ToCharArray()) + { + if (escaped) + { + escaped = false; + sb.Append(chr); + } + else if (chr == splitChar) + { + yield return sb.ToString(); + sb.Clear(); + } + else if (chr == escapeChar) + { + escaped = true; + } + else + { + sb.Append(chr); + } + } + + yield return sb.ToString(); + } } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index f0ba7f66d8..1160881304 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -133,6 +133,8 @@ + + @@ -270,7 +272,6 @@ - diff --git a/src/Umbraco.Examine/UmbracoExamineIndex.cs b/src/Umbraco.Examine/UmbracoExamineIndex.cs index cc97178e5c..511d78db92 100644 --- a/src/Umbraco.Examine/UmbracoExamineIndex.cs +++ b/src/Umbraco.Examine/UmbracoExamineIndex.cs @@ -24,8 +24,8 @@ namespace Umbraco.Examine // note // wrapping all operations that end up calling base.SafelyProcessQueueItems in a safe call // context because they will fork a thread/task/whatever which should *not* capture our - // call context (and the database it can contain)! ideally we should be able to override - // SafelyProcessQueueItems but that's not possible in the current version of Examine. + // call context (and the database it can contain)! + // TODO: FIX Examine to not flow the ExecutionContext so callers don't need to worry about this! /// /// Used to store the path of a content object @@ -99,6 +99,9 @@ namespace Umbraco.Examine { if (CanInitialize()) { + // Use SafeCallContext to prevent the current CallContext flow to child + // tasks executed in the base class so we don't leak Scopes. + // TODO: See notes at the top of this class using (new SafeCallContext()) { base.PerformDeleteFromIndex(itemIds, onComplete); @@ -106,6 +109,20 @@ namespace Umbraco.Examine } } + protected override void PerformIndexItems(IEnumerable values, Action onComplete) + { + if (CanInitialize()) + { + // Use SafeCallContext to prevent the current CallContext flow to child + // tasks executed in the base class so we don't leak Scopes. + // TODO: See notes at the top of this class + using (new SafeCallContext()) + { + base.PerformIndexItems(values, onComplete); + } + } + } + /// /// Returns true if the Umbraco application is in a state that we can initialize the examine indexes /// diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs index 45c4de5d2a..01010cca66 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Reflection; using Umbraco.Core; using Umbraco.Core.Logging; @@ -20,14 +21,11 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose { public void Compose(Composition composition) { - var isLegacyModelsBuilderInstalled = IsLegacyModelsBuilderInstalled(); - - composition.Configs.Add(() => new ModelsBuilderConfig()); - if (isLegacyModelsBuilderInstalled) + if (IsExternalModelsBuilderInstalled() == true) { - ComposeForLegacyModelsBuilder(composition); + ComposeForExternalModelsBuilder(composition); return; } @@ -45,22 +43,35 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose ComposeForDefaultModelsFactory(composition); } - private static bool IsLegacyModelsBuilderInstalled() + private static bool IsExternalModelsBuilderInstalled() { - Assembly legacyMbAssembly = null; + var assemblyNames = new[] + { + "Umbraco.ModelsBuider", + "ModelsBuilder.Umbraco" + }; + try { - legacyMbAssembly = Assembly.Load("Umbraco.ModelsBuilder"); + foreach (var name in assemblyNames) + { + var assembly = Assembly.Load(name); + + if (assembly != null) + { + return true; + } + } } - catch (System.Exception) + catch (Exception) { //swallow exception, DLL must not be there } - return legacyMbAssembly != null; + return false; } - private void ComposeForLegacyModelsBuilder(Composition composition) + private void ComposeForExternalModelsBuilder(Composition composition) { composition.Logger.Info("ModelsBuilder.Embedded is disabled, the external ModelsBuilder was detected."); composition.Components().Append(); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts index 1a40e8451f..0cec374c5d 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts @@ -1,5 +1,13 @@ /// -import { DocumentTypeBuilder, ContentBuilder, AliasHelper } from 'umbraco-cypress-testhelpers'; +import { + DocumentTypeBuilder, + ContentBuilder, + AliasHelper, + GridDataTypeBuilder, + PartialViewMacroBuilder, + MacroBuilder +} from 'umbraco-cypress-testhelpers'; + context('Content', () => { beforeEach(() => { @@ -14,6 +22,23 @@ context('Content', () => { cy.get('.umb-tree-item__inner').should('exist', {timeout: 10000}); } + function createSimpleMacro(name){ + const insertMacro = new PartialViewMacroBuilder() + .withName(name) + .withContent(`@inherits Umbraco.Web.Macros.PartialViewMacroPage +

    Acceptance test

    `) + .build(); + + const macroWithPartial = new MacroBuilder() + .withName(name) + .withPartialViewMacro(insertMacro) + .withRenderInEditor() + .withUseInEditor() + .build(); + + cy.saveMacroWithPartial(macroWithPartial); + } + it('Copy content', () => { const rootDocTypeName = "Test document type"; const childDocTypeName = "Child test document type"; @@ -596,4 +621,181 @@ context('Content', () => { cy.umbracoEnsureTemplateNameNotExists(pickerDocTypeName); cy.umbracoEnsureDocumentTypeNameNotExists(pickedDocTypeName); }); + + it('Content with macro in RTE', () => { + const viewMacroName = 'Content with macro in RTE'; + const partialFileName = viewMacroName + '.cshtml'; + + cy.umbracoEnsureMacroNameNotExists(viewMacroName); + cy.umbracoEnsurePartialViewMacroFileNameNotExists(partialFileName); + cy.umbracoEnsureDocumentTypeNameNotExists(viewMacroName); + cy.umbracoEnsureTemplateNameNotExists(viewMacroName); + cy.deleteAllContent(); + + // First thing first we got to create the macro we will be inserting + createSimpleMacro(viewMacroName); + + // Now we need to create a document type with a rich text editor where we can insert the macro + // The document type must have a template as well in order to ensure that the content is displayed correctly + const alias = AliasHelper.toAlias(viewMacroName); + const docType = new DocumentTypeBuilder() + .withName(viewMacroName) + .withAlias(alias) + .withAllowAsRoot(true) + .withDefaultTemplate(alias) + .addGroup() + .addRichTextProperty() + .withAlias('text') + .done() + .done() + .build(); + + cy.saveDocumentType(docType).then((generatedDocType) => { + // Might as wel initally create the content here, the less GUI work during the test the better + const contentNode = new ContentBuilder() + .withContentTypeAlias(generatedDocType["alias"]) + .withAction('saveNew') + .addVariant() + .withName(viewMacroName) + .withSave(true) + .done() + .build(); + + cy.saveContent(contentNode); + }); + + // Edit the macro template in order to have something to verify on when rendered. + cy.editTemplate(viewMacroName, `@inherits Umbraco.Web.Mvc.UmbracoViewPage +@using ContentModels = Umbraco.Web.PublishedModels; +@{ + Layout = null; +} +@{ + if (Model.HasValue("text")){ + @(Model.Value("text")) + } +} `); + + // Enter content + refreshContentTree(); + cy.umbracoTreeItem("content", [viewMacroName]).click(); + + // Insert macro + cy.get('#mceu_13-button').click(); + cy.get('.umb-card-grid-item').contains(viewMacroName).click(); + + // Save and publish + cy.umbracoButtonByLabelKey('buttons_saveAndPublish').click(); + cy.umbracoSuccessNotification().should('be.visible'); + + // Ensure that the view gets rendered correctly + const expected = `

    Acceptance test

     

    `; + cy.umbracoVerifyRenderedViewContent('/', expected, true).should('be.true'); + + // Cleanup + cy.umbracoEnsureMacroNameNotExists(viewMacroName); + cy.umbracoEnsurePartialViewMacroFileNameNotExists(partialFileName); + cy.umbracoEnsureDocumentTypeNameNotExists(viewMacroName); + cy.umbracoEnsureTemplateNameNotExists(viewMacroName); + }); + + it('Content with macro in grid', () => { + const name = 'Content with macro in grid'; + const macroName = 'Grid macro'; + const macroFileName = macroName + '.cshtml'; + + cy.umbracoEnsureDataTypeNameNotExists(name); + cy.umbracoEnsureDocumentTypeNameNotExists(name); + cy.umbracoEnsureTemplateNameNotExists(name); + cy.umbracoEnsureMacroNameNotExists(macroName); + cy.umbracoEnsurePartialViewMacroFileNameNotExists(macroFileName); + cy.deleteAllContent(); + + createSimpleMacro(macroName); + + const grid = new GridDataTypeBuilder() + .withName(name) + .withDefaultGrid() + .build(); + + const alias = AliasHelper.toAlias(name); + // Save grid and get the ID + cy.saveDataType(grid).then((dataType) => { + // Create a document type using the data type + const docType = new DocumentTypeBuilder() + .withName(name) + .withAlias(alias) + .withAllowAsRoot(true) + .withDefaultTemplate(alias) + .addGroup() + .addCustomProperty(dataType['id']) + .withAlias('grid') + .done() + .done() + .build(); + + cy.saveDocumentType(docType).then((generatedDocType) => { + const contentNode = new ContentBuilder() + .withContentTypeAlias(generatedDocType["alias"]) + .addVariant() + .withName(name) + .withSave(true) + .done() + .build(); + + cy.saveContent(contentNode); + }); + }); + + // Edit the template to allow us to verify the rendered view + cy.editTemplate(name, `@inherits Umbraco.Web.Mvc.UmbracoViewPage +@using ContentModels = Umbraco.Web.PublishedModels; +@{ + Layout = null; +} +@Html.GetGridHtml(Model, "grid")`); + + // Act + // Enter content + refreshContentTree(); + cy.umbracoTreeItem("content", [name]).click(); + // Click add + cy.get(':nth-child(2) > .preview-row > .preview-col > .preview-cell').click(); // Choose 1 column layout. + cy.get('.umb-column > .templates-preview > :nth-child(2) > small').click(); // Choose headline + cy.get('.umb-cell-placeholder').click(); + // Click macro + cy.get(':nth-child(4) > .umb-card-grid-item > :nth-child(1)').click(); + // Select the macro + cy.get('.umb-card-grid-item').contains(macroName).click(); + + // Save and publish + cy.umbracoButtonByLabelKey('buttons_saveAndPublish').click(); + cy.umbracoSuccessNotification().should('be.visible'); + + const expected = ` +
    +
    +
    +
    +
    +
    +
    +

    Acceptance test

    +
    +
    +
    +
    +
    +
    +
    ` + + cy.umbracoVerifyRenderedViewContent('/', expected, true).should('be.true'); + + // Clean + cy.umbracoEnsureDataTypeNameNotExists(name); + cy.umbracoEnsureDocumentTypeNameNotExists(name); + cy.umbracoEnsureTemplateNameNotExists(name); + cy.umbracoEnsureMacroNameNotExists(macroName); + cy.umbracoEnsurePartialViewMacroFileNameNotExists(macroFileName); + }); }); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts index 49bcf94943..336e5793d9 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts @@ -6,7 +6,10 @@ context('Languages', () => { }); it('Add language', () => { - const name = "Kyrgyz (Kyrgyzstan)"; // Must be an option in the select box + // For some reason the languages to chose fom seems to be translated differently than normal, as an example: + // My system is set to EN (US), but most languages are translated into Danish for some reason + // Aghem seems untranslated though? + const name = "Aghem"; // Must be an option in the select box cy.umbracoEnsureLanguageNameNotExists(name); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts index c586384af7..65d03e5a78 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts @@ -23,14 +23,18 @@ context('Templates', () => { cy.umbracoEnsureTemplateNameNotExists(name); createTemplate(); + // We have to wait for the ace editor to load, because when the editor is loading it will "steal" the focus briefly, + // which causes the save event to fire if we've added something to the header field, causing errors. + cy.wait(500); + //Type name cy.umbracoEditorHeaderName(name); // Save // We must drop focus for the auto save event to occur. cy.get('.btn-success').focus(); // And then wait for the auto save event to finish by finding the page in the tree view. - // This is a bit of a roundabout way to find items in a treev view since we dont use umbracoTreeItem - // but we must be able to wait for the save evnent to finish, and we can't do that with umbracoTreeItem + // This is a bit of a roundabout way to find items in a tree view since we dont use umbracoTreeItem + // but we must be able to wait for the save event to finish, and we can't do that with umbracoTreeItem cy.get('[data-element="tree-item-templates"] > :nth-child(2) > .umb-animated > .umb-tree-item__inner > .umb-tree-item__label') .contains(name).should('be.visible', { timeout: 10000 }); // Now that the auto save event has finished we can save diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts index 9bc1fff488..d3950d7d19 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts @@ -49,7 +49,7 @@ function resetTourData() { { "alias": "umbIntroIntroduction", "completed": false, - "disabled": false + "disabled": true }; cy.getCookie('UMB-XSRF-TOKEN', { log: false }).then((token) => { diff --git a/src/Umbraco.Tests.AcceptanceTest/package.json b/src/Umbraco.Tests.AcceptanceTest/package.json index 378fe719fc..caf75638e6 100644 --- a/src/Umbraco.Tests.AcceptanceTest/package.json +++ b/src/Umbraco.Tests.AcceptanceTest/package.json @@ -7,10 +7,10 @@ }, "devDependencies": { "cross-env": "^7.0.2", - "cypress": "^6.0.1", + "cypress": "^6.8.0", "ncp": "^2.0.0", "prompt": "^1.0.0", - "umbraco-cypress-testhelpers": "^1.0.0-beta-52" + "umbraco-cypress-testhelpers": "^1.0.0-beta-53" }, "dependencies": { "typescript": "^3.9.2" diff --git a/src/Umbraco.Tests/Mapping/MappingTests.cs b/src/Umbraco.Tests/Mapping/MappingTests.cs index e6a382692c..35f64cac62 100644 --- a/src/Umbraco.Tests/Mapping/MappingTests.cs +++ b/src/Umbraco.Tests/Mapping/MappingTests.cs @@ -1,17 +1,40 @@ using System; using System.Collections.Generic; +using System.Data; using System.Linq; using System.Threading; +using Moq; using NUnit.Framework; +using Umbraco.Core.Events; using Umbraco.Core.Mapping; using Umbraco.Core.Models; +using Umbraco.Core.Scoping; using Umbraco.Web.Models.ContentEditing; +using PropertyCollection = Umbraco.Core.Models.PropertyCollection; namespace Umbraco.Tests.Mapping { [TestFixture] public class MappingTests { + private IScopeProvider _scopeProvider; + + [SetUp] + public void MockScopeProvider() + { + var scopeMock = new Mock(); + scopeMock.Setup(x => x.CreateScope( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Mock.Of); + + _scopeProvider = scopeMock.Object; + } + [Test] public void SimpleMap() { @@ -19,7 +42,7 @@ namespace Umbraco.Tests.Mapping { new MapperDefinition1(), }); - var mapper = new UmbracoMapper(definitions); + var mapper = new UmbracoMapper(definitions, _scopeProvider); var thing1 = new Thing1 { Value = "value" }; var thing2 = mapper.Map(thing1); @@ -44,7 +67,7 @@ namespace Umbraco.Tests.Mapping { new MapperDefinition1(), }); - var mapper = new UmbracoMapper(definitions); + var mapper = new UmbracoMapper(definitions, _scopeProvider); var thing1A = new Thing1 { Value = "valueA" }; var thing1B = new Thing1 { Value = "valueB" }; @@ -78,7 +101,7 @@ namespace Umbraco.Tests.Mapping { new MapperDefinition1(), }); - var mapper = new UmbracoMapper(definitions); + var mapper = new UmbracoMapper(definitions, _scopeProvider); var thing3 = new Thing3 { Value = "value" }; var thing2 = mapper.Map(thing3); @@ -103,7 +126,7 @@ namespace Umbraco.Tests.Mapping { new MapperDefinition2(), }); - var mapper = new UmbracoMapper(definitions); + var mapper = new UmbracoMapper(definitions, _scopeProvider); // can map a PropertyCollection var source = new PropertyCollection(); @@ -119,7 +142,7 @@ namespace Umbraco.Tests.Mapping new MapperDefinition1(), new MapperDefinition3(), }); - var mapper = new UmbracoMapper(definitions); + var mapper = new UmbracoMapper(definitions, _scopeProvider); // the mapper currently has a map from Thing1 to Thing2 // because Thing3 inherits from Thing1, it will map a Thing3 instance, @@ -179,7 +202,7 @@ namespace Umbraco.Tests.Mapping { new MapperDefinition4(), }); - var mapper = new UmbracoMapper(definitions); + var mapper = new UmbracoMapper(definitions, _scopeProvider); var thing5 = new Thing5() { @@ -203,7 +226,7 @@ namespace Umbraco.Tests.Mapping { new MapperDefinition5(), }); - var mapper = new UmbracoMapper(definitions); + var mapper = new UmbracoMapper(definitions, _scopeProvider); var thing7 = new Thing7(); diff --git a/src/Umbraco.Tests/Scoping/ScopeTests.cs b/src/Umbraco.Tests/Scoping/ScopeTests.cs index 6c5e9a74b5..7d8984baad 100644 --- a/src/Umbraco.Tests/Scoping/ScopeTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopeTests.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Runtime.Remoting.Messaging; using System.Threading; +using System.Threading.Tasks; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Persistence; @@ -24,6 +25,119 @@ namespace Umbraco.Tests.Scoping Assert.IsNull(ScopeProvider.AmbientScope); // gone } + [Test] + public void GivenUncompletedScopeOnChildThread_WhenTheParentCompletes_TheTransactionIsRolledBack() + { + ScopeProvider scopeProvider = ScopeProvider; + + Assert.IsNull(ScopeProvider.AmbientScope); + IScope mainScope = scopeProvider.CreateScope(); + + var t = Task.Run(() => + { + IScope nested = scopeProvider.CreateScope(); + Thread.Sleep(2000); + nested.Dispose(); + }); + + Thread.Sleep(1000); // mimic some long running operation that is shorter than the other thread + mainScope.Complete(); + Assert.Throws(() => mainScope.Dispose()); + + Task.WaitAll(t); + } + + [Test] + public void GivenNonDisposedChildScope_WhenTheParentDisposes_ThenInvalidOperationExceptionThrows() + { + // this all runs in the same execution context so the AmbientScope reference isn't a copy + + ScopeProvider scopeProvider = ScopeProvider; + + Assert.IsNull(ScopeProvider.AmbientScope); + IScope mainScope = scopeProvider.CreateScope(); + + IScope nested = scopeProvider.CreateScope(); // not disposing + + InvalidOperationException ex = Assert.Throws(() => mainScope.Dispose()); + Console.WriteLine(ex); + } + + [Test] + public void GivenChildThread_WhenParentDisposedBeforeChild_ParentScopeThrows() + { + // The ambient context is NOT thread safe, even though it has locks, etc... + // This all just goes to show that concurrent threads with scopes is a no-go. + var childWait = new ManualResetEventSlim(false); + var parentWait = new ManualResetEventSlim(false); + + ScopeProvider scopeProvider = ScopeProvider; + + Assert.IsNull(ScopeProvider.AmbientScope); + IScope mainScope = scopeProvider.CreateScope(); + + var t = Task.Run(() => + { + Console.WriteLine("Child Task start: " + scopeProvider.AmbientScope.InstanceId); + // This will evict the parent from the ScopeProvider.StaticCallContextObjects + // and replace it with the child + IScope nested = scopeProvider.CreateScope(); + childWait.Set(); + Console.WriteLine("Child Task scope created: " + scopeProvider.AmbientScope.InstanceId); + parentWait.Wait(); // wait for the parent thread + Console.WriteLine("Child Task before dispose: " + scopeProvider.AmbientScope.InstanceId); + // This will evict the child from the ScopeProvider.StaticCallContextObjects + // and replace it with the parent + nested.Dispose(); + Console.WriteLine("Child Task after dispose: " + scopeProvider.AmbientScope.InstanceId); + }); + + childWait.Wait(); // wait for the child to start and create the scope + // This is a confusing thing (this is not the case in netcore), this is NULL because the + // parent thread's scope ID was evicted from the ScopeProvider.StaticCallContextObjects + // so now the ambient context is null because the GUID in the CallContext doesn't match + // the GUID in the ScopeProvider.StaticCallContextObjects. + Assert.IsNull(scopeProvider.AmbientScope); + // now dispose the main without waiting for the child thread to join + // This will throw because at this stage a child scope has been created which means + // it is the Ambient (top) scope but here we're trying to dispose the non top scope. + Assert.Throws(() => mainScope.Dispose()); + parentWait.Set(); // tell child thread to proceed + Task.WaitAll(t); // wait for the child to dispose + mainScope.Dispose(); // now it's ok + Console.WriteLine("Parent Task disposed: " + scopeProvider.AmbientScope?.InstanceId); + } + + [Test] + public void GivenChildThread_WhenChildDisposedBeforeParent_OK() + { + ScopeProvider scopeProvider = ScopeProvider; + + Assert.IsNull(ScopeProvider.AmbientScope); + IScope mainScope = scopeProvider.CreateScope(); + + // Task.Run will flow the execution context unless ExecutionContext.SuppressFlow() is explicitly called. + // This is what occurs in normal async behavior since it is expected to await (and join) the main thread, + // but if Task.Run is used as a fire and forget thread without being done correctly then the Scope will + // flow to that thread. + var t = Task.Run(() => + { + Console.WriteLine("Child Task start: " + scopeProvider.AmbientScope.InstanceId); + IScope nested = scopeProvider.CreateScope(); + Console.WriteLine("Child Task before dispose: " + scopeProvider.AmbientScope.InstanceId); + nested.Dispose(); + Console.WriteLine("Child Task after disposed: " + scopeProvider.AmbientScope.InstanceId); + }); + + Console.WriteLine("Parent Task waiting: " + scopeProvider.AmbientScope?.InstanceId); + Task.WaitAll(t); + Console.WriteLine("Parent Task disposing: " + scopeProvider.AmbientScope.InstanceId); + mainScope.Dispose(); + Console.WriteLine("Parent Task disposed: " + scopeProvider.AmbientScope?.InstanceId); + + Assert.Pass(); + } + [Test] public void SimpleCreateScope() { diff --git a/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs b/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs index 32bd7e2afe..038376f71c 100644 --- a/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopeUnitTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Moq; using NPoco; using NUnit.Framework; @@ -9,6 +10,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Scoping; +using Umbraco.Tests.TestHelpers; namespace Umbraco.Tests.Scoping { @@ -72,6 +74,30 @@ namespace Umbraco.Tests.Scoping syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), Constants.Locks.Languages), Times.Once); } + [Test] + public void WriteLock_Acquired_Only_Once_When_InnerScope_Disposed() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + + using (var outerScope = scopeProvider.CreateScope()) + { + outerScope.WriteLock(Constants.Locks.Languages); + + using (var innerScope = scopeProvider.CreateScope()) + { + innerScope.WriteLock(Constants.Locks.Languages); + innerScope.WriteLock(Constants.Locks.ContentTree); + innerScope.Complete(); + } + + outerScope.WriteLock(Constants.Locks.ContentTree); + outerScope.Complete(); + } + + syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), Constants.Locks.Languages), Times.Once); + syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), Constants.Locks.ContentTree), Times.Once); + } + [Test] public void WriteLock_With_Timeout_Acquired_Only_Once_Per_Key(){ var scopeProvider = GetScopeProvider(out var syntaxProviderMock); @@ -176,31 +202,58 @@ namespace Umbraco.Tests.Scoping syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), timeOut, Constants.Locks.Languages), Times.Once); } + [Test] + public void ReadLock_Acquired_Only_Once_When_InnerScope_Disposed() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + + using (var outerScope = scopeProvider.CreateScope()) + { + outerScope.ReadLock(Constants.Locks.Languages); + + using (var innerScope = scopeProvider.CreateScope()) + { + innerScope.ReadLock(Constants.Locks.Languages); + innerScope.ReadLock(Constants.Locks.ContentTree); + innerScope.Complete(); + } + + outerScope.ReadLock(Constants.Locks.ContentTree); + outerScope.Complete(); + } + + syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), Constants.Locks.Languages), Times.Once); + syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), Constants.Locks.ContentTree), Times.Once); + } + [Test] public void WriteLocks_Count_correctly_If_Lock_Requested_Twice_In_Scope() { var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + Guid innerscopeId; using (var outerscope = scopeProvider.CreateScope()) { var realOuterScope = (Scope) outerscope; outerscope.WriteLock(Constants.Locks.ContentTree); outerscope.WriteLock(Constants.Locks.ContentTree); - Assert.AreEqual(2, realOuterScope.WriteLocks[Constants.Locks.ContentTree]); + Assert.AreEqual(2, realOuterScope.WriteLocks[outerscope.InstanceId][Constants.Locks.ContentTree]); using (var innerScope = scopeProvider.CreateScope()) { + innerscopeId = innerScope.InstanceId; innerScope.WriteLock(Constants.Locks.ContentTree); innerScope.WriteLock(Constants.Locks.ContentTree); - Assert.AreEqual(4, realOuterScope.WriteLocks[Constants.Locks.ContentTree]); + Assert.AreEqual(2, realOuterScope.WriteLocks[outerscope.InstanceId][Constants.Locks.ContentTree]); + Assert.AreEqual(2, realOuterScope.WriteLocks[innerscopeId][Constants.Locks.ContentTree]); innerScope.WriteLock(Constants.Locks.Languages); innerScope.WriteLock(Constants.Locks.Languages); - Assert.AreEqual(2, realOuterScope.WriteLocks[Constants.Locks.Languages]); + Assert.AreEqual(2, realOuterScope.WriteLocks[innerScope.InstanceId][Constants.Locks.Languages]); innerScope.Complete(); } - Assert.AreEqual(0, realOuterScope.WriteLocks[Constants.Locks.Languages]); - Assert.AreEqual(2, realOuterScope.WriteLocks[Constants.Locks.ContentTree]); + Assert.AreEqual(2, realOuterScope.WriteLocks[realOuterScope.InstanceId][Constants.Locks.ContentTree]); + Assert.IsFalse(realOuterScope.WriteLocks.ContainsKey(innerscopeId)); outerscope.Complete(); } } @@ -209,27 +262,32 @@ namespace Umbraco.Tests.Scoping public void ReadLocks_Count_correctly_If_Lock_Requested_Twice_In_Scope() { var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + Guid innerscopeId; using (var outerscope = scopeProvider.CreateScope()) { var realOuterScope = (Scope) outerscope; outerscope.ReadLock(Constants.Locks.ContentTree); outerscope.ReadLock(Constants.Locks.ContentTree); - Assert.AreEqual(2, realOuterScope.ReadLocks[Constants.Locks.ContentTree]); + Assert.AreEqual(2, realOuterScope.ReadLocks[outerscope.InstanceId][Constants.Locks.ContentTree]); using (var innerScope = scopeProvider.CreateScope()) { + innerscopeId = innerScope.InstanceId; innerScope.ReadLock(Constants.Locks.ContentTree); innerScope.ReadLock(Constants.Locks.ContentTree); - Assert.AreEqual(4, realOuterScope.ReadLocks[Constants.Locks.ContentTree]); + Assert.AreEqual(2, realOuterScope.ReadLocks[outerscope.InstanceId][Constants.Locks.ContentTree]); + Assert.AreEqual(2, realOuterScope.ReadLocks[innerScope.InstanceId][Constants.Locks.ContentTree]); innerScope.ReadLock(Constants.Locks.Languages); innerScope.ReadLock(Constants.Locks.Languages); - Assert.AreEqual(2, realOuterScope.ReadLocks[Constants.Locks.Languages]); + Assert.AreEqual(2, realOuterScope.ReadLocks[innerScope.InstanceId][Constants.Locks.Languages]); innerScope.Complete(); } - Assert.AreEqual(0, realOuterScope.ReadLocks[Constants.Locks.Languages]); - Assert.AreEqual(2, realOuterScope.ReadLocks[Constants.Locks.ContentTree]); + Assert.AreEqual(2, realOuterScope.ReadLocks[outerscope.InstanceId][Constants.Locks.ContentTree]); + Assert.IsFalse(realOuterScope.ReadLocks.ContainsKey(innerscopeId)); + + outerscope.Complete(); } } @@ -238,51 +296,61 @@ namespace Umbraco.Tests.Scoping public void Nested_Scopes_WriteLocks_Count_Correctly() { var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + Guid innerScope1Id, innerScope2Id; - using (var outerScope = scopeProvider.CreateScope()) + using (var parentScope = scopeProvider.CreateScope()) { - var parentScope = (Scope) outerScope; - outerScope.WriteLock(Constants.Locks.ContentTree); - outerScope.WriteLock(Constants.Locks.ContentTypes); + var realParentScope = (Scope) parentScope; + parentScope.WriteLock(Constants.Locks.ContentTree); + parentScope.WriteLock(Constants.Locks.ContentTypes); - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTree], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); using (var innerScope1 = scopeProvider.CreateScope()) { + innerScope1Id = innerScope1.InstanceId; innerScope1.WriteLock(Constants.Locks.ContentTree); innerScope1.WriteLock(Constants.Locks.ContentTypes); innerScope1.WriteLock(Constants.Locks.Languages); - Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTree], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.Languages], $"childScope1 after locks acquired: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope1, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.Languages], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.Languages)}"); using (var innerScope2 = scopeProvider.CreateScope()) { + innerScope2Id = innerScope2.InstanceId; innerScope2.WriteLock(Constants.Locks.ContentTree); innerScope2.WriteLock(Constants.Locks.MediaTypes); - Assert.AreEqual(3, parentScope.WriteLocks[Constants.Locks.ContentTree], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.Languages], $"childScope2 after locks acquired: {nameof(Constants.Locks.Languages)}"); - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.MediaTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.MediaTypes)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope2, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope2, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.Languages], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope2.InstanceId][Constants.Locks.ContentTree], $"innerScope2, innerScope2 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope2.InstanceId][Constants.Locks.MediaTypes], $"innerScope2, innerScope2 instance, after locks acquired: {nameof(Constants.Locks.MediaTypes)}"); innerScope2.Complete(); } - Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTree], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(2, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.Languages], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.Languages)}"); - Assert.AreEqual(0, parentScope.WriteLocks[Constants.Locks.MediaTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.MediaTypes)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope1, parent instance, after innserScope2 disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, parent instance, after innserScope2 disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope1, innerScope1 instance, after innserScope2 disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, innerScope1 instance, after innserScope2 disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[innerScope1.InstanceId][Constants.Locks.Languages], $"innerScope1, innerScope1 instance, after innserScope2 disposed: {nameof(Constants.Locks.Languages)}"); + Assert.IsFalse(realParentScope.WriteLocks.ContainsKey(innerScope2Id)); innerScope1.Complete(); } - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTree], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, parentScope.WriteLocks[Constants.Locks.ContentTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(0, parentScope.WriteLocks[Constants.Locks.Languages], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.Languages)}"); - Assert.AreEqual(0, parentScope.WriteLocks[Constants.Locks.MediaTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.MediaTypes)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.WriteLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.IsFalse(realParentScope.WriteLocks.ContainsKey(innerScope2Id)); + Assert.IsFalse(realParentScope.WriteLocks.ContainsKey(innerScope1Id)); - outerScope.Complete(); + parentScope.Complete(); } } @@ -290,48 +358,166 @@ namespace Umbraco.Tests.Scoping public void Nested_Scopes_ReadLocks_Count_Correctly() { var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + Guid innerScope1Id, innerScope2Id; - using (var outerScope = scopeProvider.CreateScope()) + using (var parentScope = scopeProvider.CreateScope()) { - var parentScope = (Scope) outerScope; - outerScope.ReadLock(Constants.Locks.ContentTree); - outerScope.ReadLock(Constants.Locks.ContentTypes); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTree], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + var realParentScope = (Scope) parentScope; + parentScope.ReadLock(Constants.Locks.ContentTree); + parentScope.ReadLock(Constants.Locks.ContentTypes); + Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"parentScope after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); using (var innserScope1 = scopeProvider.CreateScope()) { + innerScope1Id = innserScope1.InstanceId; innserScope1.ReadLock(Constants.Locks.ContentTree); innserScope1.ReadLock(Constants.Locks.ContentTypes); innserScope1.ReadLock(Constants.Locks.Languages); - Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTree], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"childScope1 after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.Languages], $"childScope1 after locks acquired: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope1, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.Languages], $"innerScope1, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.Languages)}"); using (var innerScope2 = scopeProvider.CreateScope()) { + innerScope2Id = innerScope2.InstanceId; innerScope2.ReadLock(Constants.Locks.ContentTree); innerScope2.ReadLock(Constants.Locks.MediaTypes); - Assert.AreEqual(3, parentScope.ReadLocks[Constants.Locks.ContentTree], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.Languages], $"childScope2 after locks acquired: {nameof(Constants.Locks.Languages)}"); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.MediaTypes], $"childScope2 after locks acquired: {nameof(Constants.Locks.MediaTypes)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope2, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope2, parent instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.Languages], $"innerScope2, innerScope1 instance, after locks acquired: {nameof(Constants.Locks.Languages)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innerScope2.InstanceId][Constants.Locks.ContentTree], $"innerScope2, innerScope2 instance, after locks acquired: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innerScope2.InstanceId][Constants.Locks.MediaTypes], $"innerScope2, innerScope2 instance, after locks acquired: {nameof(Constants.Locks.MediaTypes)}"); innerScope2.Complete(); } - Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTree], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(2, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.Languages], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.Languages)}"); - Assert.AreEqual(0, parentScope.ReadLocks[Constants.Locks.MediaTypes], $"childScope1 after inner scope disposed: {nameof(Constants.Locks.MediaTypes)}"); + + Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"innerScope1, parent instance, after innerScope2 disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, parent instance, after innerScope2 disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.ContentTree], $"innerScope1, innerScope1 instance, after innerScope2 disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.ContentTypes], $"innerScope1, innerScope1 instance, after innerScope2 disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[innserScope1.InstanceId][Constants.Locks.Languages], $"innerScope1, innerScope1 instance, after innerScope2 disposed: {nameof(Constants.Locks.Languages)}"); + Assert.IsFalse(realParentScope.ReadLocks.ContainsKey(innerScope2Id)); innserScope1.Complete(); } - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTree], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTree)}"); - Assert.AreEqual(1, parentScope.ReadLocks[Constants.Locks.ContentTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.ContentTypes)}"); - Assert.AreEqual(0, parentScope.ReadLocks[Constants.Locks.Languages], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.Languages)}"); - Assert.AreEqual(0, parentScope.ReadLocks[Constants.Locks.MediaTypes], $"parentScope after inner scopes disposed: {nameof(Constants.Locks.MediaTypes)}"); - outerScope.Complete(); + Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTree], $"parentScope after innerScope1 disposed: {nameof(Constants.Locks.ContentTree)}"); + Assert.AreEqual(1, realParentScope.ReadLocks[realParentScope.InstanceId][Constants.Locks.ContentTypes], $"parentScope after innerScope1 disposed: {nameof(Constants.Locks.ContentTypes)}"); + Assert.IsFalse(realParentScope.ReadLocks.ContainsKey(innerScope2Id)); + Assert.IsFalse(realParentScope.ReadLocks.ContainsKey(innerScope1Id)); + + parentScope.Complete(); + } + } + + [Test] + public void WriteLock_Doesnt_Increment_On_Error() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + syntaxProviderMock.Setup(x => x.WriteLock(It.IsAny(), It.IsAny())).Throws(new Exception("Boom")); + + using (var scope = scopeProvider.CreateScope()) + { + var realScope = (Scope) scope; + + Assert.Throws(() => scope.WriteLock(Constants.Locks.Languages)); + Assert.IsFalse(realScope.WriteLocks[scope.InstanceId].ContainsKey(Constants.Locks.Languages)); + scope.Complete(); + } + } + + [Test] + public void ReadLock_Doesnt_Increment_On_Error() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + syntaxProviderMock.Setup(x => x.ReadLock(It.IsAny(), It.IsAny())).Throws(new Exception("Boom")); + + using (var scope = scopeProvider.CreateScope()) + { + var realScope = (Scope) scope; + + Assert.Throws(() => scope.ReadLock(Constants.Locks.Languages)); + Assert.IsFalse(realScope.ReadLocks[scope.InstanceId].ContainsKey(Constants.Locks.Languages)); + scope.Complete(); + } + } + + [Test] + public void Scope_Throws_If_ReadLocks_Not_Cleared() + { + var scopeprovider = GetScopeProvider(out var syntaxProviderMock); + var scope = (Scope) scopeprovider.CreateScope(); + + try + { + // Request a lock to create the ReadLocks dict. + scope.ReadLock(Constants.Locks.Domains); + + var readDict = new Dictionary(); + readDict[Constants.Locks.Languages] = 1; + scope.ReadLocks[Guid.NewGuid()] = readDict; + + Assert.Throws(() => scope.Dispose()); + } + finally + { + // We have to clear so we can properly dispose the scope, otherwise it'll mess with other tests. + scope.ReadLocks?.Clear(); + scope.Dispose(); + } + } + + [Test] + public void Scope_Throws_If_WriteLocks_Not_Cleared() + { + var scopeprovider = GetScopeProvider(out var syntaxProviderMock); + var scope = (Scope) scopeprovider.CreateScope(); + + try + { + // Request a lock to create the WriteLocks dict. + scope.WriteLock(Constants.Locks.Domains); + + var writeDict = new Dictionary(); + writeDict[Constants.Locks.Languages] = 1; + scope.WriteLocks[Guid.NewGuid()] = writeDict; + + Assert.Throws(() => scope.Dispose()); + } + finally + { + // We have to clear so we can properly dispose the scope, otherwise it'll mess with other tests. + scope.WriteLocks?.Clear(); + scope.Dispose(); + } + } + + [Test] + public void WriteLocks_Not_Created_Until_First_Lock() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + + using (var scope = scopeProvider.CreateScope()) + { + var realScope = (Scope) scope; + Assert.IsNull(realScope.WriteLocks); + } + } + + [Test] + public void ReadLocks_Not_Created_Until_First_Lock() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + + using (var scope = scopeProvider.CreateScope()) + { + var realScope = (Scope) scope; + Assert.IsNull(realScope.ReadLocks); } } } diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 008c24fcbf..0faa4af316 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -2082,6 +2082,32 @@ namespace Umbraco.Tests.Services Assert.AreEqual("world", copiedTags[1].Text); } + [Test] + public void Copy_Recursive_Preserves_Sort_Order() + { + // Arrange + var contentService = ServiceContext.ContentService; + var temp = contentService.GetById(NodeDto.NodeIdSeed + 2); + Assert.AreEqual("Home", temp.Name); + Assert.AreEqual(3, contentService.CountChildren(temp.Id)); + var reversedChildren = contentService.GetPagedChildren(temp.Id, 0, 10, out var total1).Reverse().ToArray(); + contentService.Sort(reversedChildren); + + // Act + var copy = contentService.Copy(temp, temp.ParentId, false, true, Constants.Security.SuperUserId); + var content = contentService.GetById(NodeDto.NodeIdSeed + 2); + + // Assert + Assert.That(copy, Is.Not.Null); + Assert.That(copy.Id, Is.Not.EqualTo(content.Id)); + Assert.AreNotSame(content, copy); + Assert.AreEqual(3, contentService.CountChildren(copy.Id)); + + var copiedChildren = contentService.GetPagedChildren(copy.Id, 0, 10, out var total2).OrderBy(c => c.SortOrder).ToArray(); + Assert.AreEqual(reversedChildren.First().Name, copiedChildren.First().Name); + Assert.AreEqual(reversedChildren.Last().Name, copiedChildren.Last().Name); + } + [Test] public void Can_Rollback_Version_On_Content() { diff --git a/src/Umbraco.Tests/Services/Importing/StandardMvc-Package.xml b/src/Umbraco.Tests/Services/Importing/StandardMvc-Package.xml index ee6f7cea4a..daeb74cc75 100644 --- a/src/Umbraco.Tests/Services/Importing/StandardMvc-Package.xml +++ b/src/Umbraco.Tests/Services/Importing/StandardMvc-Package.xml @@ -210,7 +210,7 @@ Google Maps - A map macro that you can use within Rich Text Areas Built by Creative Founds

    Web ApplicationsCreative Founds design and build first class software solutions that deliver big results. We provide ASP.NET web and mobile applications, Umbraco development service & technical consultancy.

    -

    www.creativefounds.co.uk

    ]]> +

    www.creativefounds.co.uk

    ]]>
    Umbraco Development @@ -218,7 +218,7 @@ Google Maps - A map macro that you can use within Rich Text Areas Contact Us -

    Contact Us on TwitterWe'd love to hear how this package has helped you and how it can be improved. Get in touch on the project website or via twitter

    ]]> +

    Contact Us on TwitterWe'd love to hear how this package has helped you and how it can be improved. Get in touch on the project website or via twitter

    ]]>
    Standard Website MVC, Company Address, Glasgow, Postcode
    @@ -418,7 +418,7 @@ Google Maps - A map macro that you can use within Rich Text Areas Standard Website MVC

    Well hello! This website package demonstrates all the standard functionality of Umbraco. It's a great starting point for starting point for further development or as a prototype.

    Creative Founds

    -

    This package was developed by Chris Koiak & Creative Founds

    ]]> +

    This package was developed by Chris Koiak & Creative Founds

    ]]> 1 diff --git a/src/Umbraco.Tests/Services/PerformanceTests.cs b/src/Umbraco.Tests/Services/PerformanceTests.cs index 9cf38e1789..718f99ce2f 100644 --- a/src/Umbraco.Tests/Services/PerformanceTests.cs +++ b/src/Umbraco.Tests/Services/PerformanceTests.cs @@ -297,11 +297,11 @@ namespace Umbraco.Tests.Services Built by Creative Founds

    Web ApplicationsCreative Founds design and build first class software solutions that deliver big results. We provide ASP.NET web and mobile applications, Umbraco development service & technical consultancy.

    -

    www.creativefounds.co.uk

    ]]>
    +

    www.creativefounds.co.uk

    ]]> Umbraco Development

    UmbracoUmbraco the the leading ASP.NET open source CMS, under pinning over 150,000 websites. Our Certified Developers are experts in developing high performance and feature rich websites.

    ]]>
    Contact Us -

    Contact Us on TwitterWe'd love to hear how this package has helped you and how it can be improved. Get in touch on the project website or via twitter

    ]]>
    +

    Contact Us on TwitterWe'd love to hear how this package has helped you and how it can be improved. Get in touch on the project website or via twitter

    ]]>
    Standard Website MVC, Company Address, Glasgow, Postcode
    Copyright &copy; 2012 Your Company diff --git a/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs b/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs index 33ee2f737a..5c58b35b6d 100644 --- a/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs +++ b/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs @@ -104,7 +104,7 @@ namespace Umbraco.Tests.Services var threads = new List(); var exceptions = new List(); - Debug.WriteLine("Starting..."); + Console.WriteLine("Starting..."); var done = TraceLocks(); @@ -114,12 +114,12 @@ namespace Umbraco.Tests.Services { try { - Debug.WriteLine("[{0}] Running...", Thread.CurrentThread.ManagedThreadId); + Console.WriteLine("[{0}] Running...", Thread.CurrentThread.ManagedThreadId); var name1 = "test-" + Guid.NewGuid(); var content1 = contentService.Create(name1, -1, "umbTextpage"); - Debug.WriteLine("[{0}] Saving content #1.", Thread.CurrentThread.ManagedThreadId); + Console.WriteLine("[{0}] Saving content #1.", Thread.CurrentThread.ManagedThreadId); Save(contentService, content1); Thread.Sleep(100); //quick pause for maximum overlap! @@ -127,7 +127,7 @@ namespace Umbraco.Tests.Services var name2 = "test-" + Guid.NewGuid(); var content2 = contentService.Create(name2, -1, "umbTextpage"); - Debug.WriteLine("[{0}] Saving content #2.", Thread.CurrentThread.ManagedThreadId); + Console.WriteLine("[{0}] Saving content #2.", Thread.CurrentThread.ManagedThreadId); Save(contentService, content2); } catch (Exception e) @@ -139,16 +139,16 @@ namespace Umbraco.Tests.Services } // start all threads - Debug.WriteLine("Starting threads"); + Console.WriteLine("Starting threads"); threads.ForEach(x => x.Start()); // wait for all to complete - Debug.WriteLine("Joining threads"); + Console.WriteLine("Joining threads"); threads.ForEach(x => x.Join()); done.Set(); - Debug.WriteLine("Checking exceptions"); + Console.WriteLine("Checking exceptions"); if (exceptions.Count == 0) { //now look up all items, there should be 40! @@ -172,7 +172,7 @@ namespace Umbraco.Tests.Services var threads = new List(); var exceptions = new List(); - Debug.WriteLine("Starting..."); + Console.WriteLine("Starting..."); var done = TraceLocks(); @@ -182,18 +182,18 @@ namespace Umbraco.Tests.Services { try { - Debug.WriteLine("[{0}] Running...", Thread.CurrentThread.ManagedThreadId); + Console.WriteLine("[{0}] Running...", Thread.CurrentThread.ManagedThreadId); var name1 = "test-" + Guid.NewGuid(); var media1 = mediaService.CreateMedia(name1, -1, Constants.Conventions.MediaTypes.Folder); - Debug.WriteLine("[{0}] Saving media #1.", Thread.CurrentThread.ManagedThreadId); + Console.WriteLine("[{0}] Saving media #1.", Thread.CurrentThread.ManagedThreadId); Save(mediaService, media1); Thread.Sleep(100); //quick pause for maximum overlap! var name2 = "test-" + Guid.NewGuid(); var media2 = mediaService.CreateMedia(name2, -1, Constants.Conventions.MediaTypes.Folder); - Debug.WriteLine("[{0}] Saving media #2.", Thread.CurrentThread.ManagedThreadId); + Console.WriteLine("[{0}] Saving media #2.", Thread.CurrentThread.ManagedThreadId); Save(mediaService, media2); } catch (Exception e) diff --git a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs index f53b0bfff0..7eacccc8d5 100644 --- a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs +++ b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs @@ -1,4 +1,5 @@ using System; +using System.Data; using System.Globalization; using System.Linq; using System.Web.Security; @@ -9,11 +10,13 @@ using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Dictionary; +using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Stubs; @@ -98,10 +101,21 @@ namespace Umbraco.Tests.Testing.TestingTests { var umbracoContext = TestObjects.GetUmbracoContextMock(); + var scopeProvider = new Mock(); + scopeProvider + .Setup(x => x.CreateScope( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Mock.Of); + var membershipHelper = new MembershipHelper(umbracoContext.HttpContext, Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of()); var umbracoHelper = new UmbracoHelper(Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), membershipHelper); - var umbracoMapper = new UmbracoMapper(new MapDefinitionCollection(new[] { Mock.Of() })); - + var umbracoMapper = new UmbracoMapper(new MapDefinitionCollection(new[] { Mock.Of() }), scopeProvider.Object); + // ReSharper disable once UnusedVariable var umbracoApiController = new FakeUmbracoApiController(Mock.Of(), Mock.Of(), Mock.Of(), ServiceContext.CreatePartial(), AppCaches.NoCache, Mock.Of(), Mock.Of(), umbracoHelper, umbracoMapper); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js index 36eeb173d6..ea8b5e8173 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js @@ -16,7 +16,7 @@ function UmbLoginController($scope, $location, currentUserResource, formHelper, mediaHelper, umbRequestHelper, Upload, localizationService, userService, externalLoginInfo, externalLoginInfoService, - resetPasswordCodeInfo, $timeout, authResource, $q, $route) { + resetPasswordCodeInfo, authResource, $q) { const vm = this; @@ -72,6 +72,7 @@ vm.loginSubmit = loginSubmit; vm.requestPasswordResetSubmit = requestPasswordResetSubmit; vm.setPasswordSubmit = setPasswordSubmit; + vm.newPasswordKeyUp = newPasswordKeyUp; vm.labels = {}; localizationService.localizeMany([ vm.usernameIsEmail ? "general_email" : "general_username", @@ -362,6 +363,9 @@ }); } + function newPasswordKeyUp(event) { + vm.passwordVal = event.target.value; + } //// diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbpasswordtip.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbpasswordtip.directive.js new file mode 100644 index 0000000000..86e1d3d32f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbpasswordtip.directive.js @@ -0,0 +1,71 @@ +(function () { + 'use strict'; + + angular + .module('umbraco.directives') + .component('umbPasswordTip', { + controller: UmbPasswordTipController, + controllerAs: 'vm', + template: + '{{vm.passwordTip}}', + bindings: { + passwordVal: "<", + minPwdLength: "<", + minPwdNonAlphaNum: "<" + } + }); + + function UmbPasswordTipController(localizationService) { + + let defaultMinPwdLength = Umbraco.Sys.ServerVariables.umbracoSettings.minimumPasswordLength; + let defaultMinPwdNonAlphaNum = Umbraco.Sys.ServerVariables.umbracoSettings.minimumPasswordNonAlphaNum; + + var vm = this; + vm.$onInit = onInit; + vm.$onChanges = onChanges; + + function onInit() { + if (vm.minPwdLength === undefined) { + vm.minPwdLength = defaultMinPwdLength; + } + + if (vm.minPwdNonAlphaNum === undefined) { + vm.minPwdNonAlphaNum = defaultMinPwdNonAlphaNum; + } + + if (vm.minPwdNonAlphaNum > 0) { + localizationService.localize('user_newPasswordFormatNonAlphaTip', [vm.minPwdNonAlphaNum]).then(data => { + vm.passwordNonAlphaTip = data; + updatePasswordTip(0); + }); + } else { + vm.passwordNonAlphaTip = ''; + updatePasswordTip(0); + } + } + + function onChanges(simpleChanges) { + if (simpleChanges.passwordVal) { + if (simpleChanges.passwordVal.currentValue) { + updatePasswordTip(simpleChanges.passwordVal.currentValue.length); + } else { + updatePasswordTip(0); + } + } + } + + const updatePasswordTip = passwordLength => { + const remainingLength = vm.minPwdLength - passwordLength; + if (remainingLength > 0) { + localizationService.localize('user_newPasswordFormatLengthTip', [remainingLength]).then(data => { + vm.passwordTip = data; + if (vm.passwordNonAlphaTip) { + vm.passwordTip += `
    ${vm.passwordNonAlphaTip}`; + } + }); + } else { + vm.passwordTip = vm.passwordNonAlphaTip; + } + } + } +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js index 3e227bfcb3..71bf151b89 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js @@ -202,6 +202,9 @@ splitViewChanged(); unbindSplitViewRequest(); } + + // if split view was never closed, the listener is not disposed when changing nodes - this unbinds it + $scope.$on('$destroy', () => unbindSplitViewRequest()); /** * Changes the currently selected variant diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/prevententersubmit.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/prevententersubmit.directive.js index 355b02216f..62334387cb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/prevententersubmit.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/prevententersubmit.directive.js @@ -17,7 +17,7 @@ angular.module("umbraco.directives") } $(element).on("keypress", function (event) { - if (event.which === 13) { + if (event.which === 13 && enabled === true) { event.preventDefault(); } }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 569f49b88a..f7cd32217e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -29,10 +29,6 @@ var defaultFocusedElement = getAutoFocusElement(focusableElements); var firstFocusableElement = focusableElements[0]; var lastFocusableElement = focusableElements[focusableElements.length -1]; - - // We need to add the tabbing-active class in order to highlight the focused button since the default style is - // outline: none; set in the stylesheet specifically - bodyElement.classList.add('tabbing-active'); // If there is no default focused element put focus on the first focusable element in the nodelist if(defaultFocusedElement === null ){ diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbsearchfilter.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbsearchfilter.directive.js index efbc384cb4..2e9f15913c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbsearchfilter.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbsearchfilter.directive.js @@ -24,7 +24,7 @@ @param {boolean} model Set to true or false to set the checkbox to checked or unchecked. @param {string} inputId Set the id of the checkbox. @param {string} text Set the text for the checkbox label. -@param {string} labelKey Set a dictinary/localization string for the checkbox label +@param {string} labelKey Set a dictionary/localization string for the checkbox label @param {callback} onChange Callback when the value of the checkbox change by interaction. @param {boolean} autoFocus Add autofocus to the input field @param {boolean} preventSubmitOnEnter Set the enter prevent directive or not @@ -42,13 +42,15 @@ vm.change = change; function onInit() { - vm.inputId = vm.inputId || "umb-check_" + String.CreateGuid(); + vm.inputId = vm.inputId || "umb-search-filter_" + String.CreateGuid(); + vm.autoFocus = Object.toBoolean(vm.autoFocus) === true; + vm.preventSubmitOnEnter = Object.toBoolean(vm.preventSubmitOnEnter) === true; // If a labelKey is passed let's update the returned text if it's does not contain an opening square bracket [ if (vm.labelKey) { localizationService.localize(vm.labelKey).then(function (data) { - if(data.indexOf('[') === -1){ - vm.text = data; + if (data.indexOf('[') === -1){ + vm.text = data; } }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabsnav.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabsnav.directive.js index b1c8608124..7a10ff51b5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabsnav.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabsnav.directive.js @@ -11,24 +11,27 @@ Use this directive to render a tabs navigation.
         
    - - + + - -
    -
    Content of tab 1
    -
    -
    -
    Content of tab 2
    -
    -
    + + + +
    +
    Content of tab 1
    +
    +
    +
    Content of tab 2
    +
    +
    +
    @@ -37,7 +40,7 @@ Use this directive to render a tabs navigation. (function () { "use strict"; - function Controller() { + function Controller(eventsService) { var vm = this; @@ -62,7 +65,7 @@ Use this directive to render a tabs navigation. selectedTab.active = true; }; - eventsService.on("tab.tabChange", function(name, args){ + eventsService.on("app.tabChange", function(name, args){ console.log("args", args); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js index 3e2e7e362e..2ae17fdc6b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js @@ -15,6 +15,7 @@ function treeSearchBox(localizationService, searchService, $q) { datatypeKey: "@", hideSearchCallback: "=", searchCallback: "=", + inputId: "@", autoFocus: "=" }, restrict: "E", // restrict to an element diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js index 8cbdabbf75..a9961a7579 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js @@ -11,6 +11,7 @@ vm.cancelChange = cancelChange; vm.showOldPass = showOldPass; vm.showCancelBtn = showCancelBtn; + vm.newPasswordKeyUp = newPasswordKeyUp; var unsubscribe = []; @@ -55,6 +56,11 @@ vm.config.minPasswordLength = 0; } + // Check non-alpha pwd settings for tooltip display + if (vm.config.minNonAlphaNumericChars === undefined) { + vm.config.minNonAlphaNumericChars = 0; + } + //set the model defaults if (!Utilities.isObject(vm.passwordValues)) { //if it's not an object then just create a new one @@ -152,6 +158,9 @@ return vm.config.disableToggle !== true && vm.config.hasPassword; }; + function newPasswordKeyUp(event) { + vm.passwordVal = event.target.value; + } } var component = { diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js index ea7f3a6d4c..9a05e3cd7f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js @@ -193,7 +193,7 @@ angular.module('umbraco.mocks'). "defaultdialogs_recycleBinDeleting": "The items in the recycle bin are now being deleted. Please do not close this window while this operation takes place", "defaultdialogs_recycleBinIsEmpty": "The recycle bin is now empty", "defaultdialogs_recycleBinWarning": "When items are deleted from the recycle bin, they will be gone forever", - "defaultdialogs_regexSearchError": "regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.", + "defaultdialogs_regexSearchError": "regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.", "defaultdialogs_regexSearchHelp": "Search for a regular expression to add validation to a form field. Exemple: 'email, 'zip-code' 'url'", "defaultdialogs_removeMacro": "Remove Macro", "defaultdialogs_requiredField": "Required Field", @@ -355,7 +355,7 @@ angular.module('umbraco.mocks'). "installer_databaseHeader": "Database configuration", "installer_databaseInstall": " Press the install button to install the Umbraco %0% database ", "installer_databaseInstallDone": "Umbraco %0% has now been copied to your database. Press Next to proceed.", - "installer_databaseNotFound": "

    Database not found! Please check that the information in the 'connection string' of the \"web.config\" file is correct.

    To proceed, please edit the 'web.config' file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named 'UmbracoDbDSN' and save the file.

    Click the retry button when done.
    More information on editing web.config here.

    ", + "installer_databaseNotFound": "

    Database not found! Please check that the information in the 'connection string' of the \"web.config\" file is correct.

    To proceed, please edit the 'web.config' file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named 'UmbracoDbDSN' and save the file.

    Click the retry button when done.
    More information on editing web.config here.

    ", "installer_databaseText": "To complete this step, you must know some information regarding your database server ('connection string').
    Please contact your ISP if necessary. If you're installing on a local machine or server you might need information from your system administrator.", "installer_databaseUpgrade": "

    Press the upgrade button to upgrade your database to Umbraco %0%

    Don't worry - no content will be deleted and everything will continue working afterwards!

    ", "installer_databaseUpgradeDone": "Your database has been upgraded to the final version %0%.
    Press Next to proceed. ", @@ -420,7 +420,7 @@ angular.module('umbraco.mocks'). "login_greeting6": "Happy friendly Friday", "login_greeting7": "Happy shiny Saturday", "login_instruction": "Log in below:", - "login_bottomText": "

    © 2001 - %0%
    Umbraco.org

    ", + "login_bottomText": "

    © 2001 - %0%
    Umbraco.org

    ", "main_dashboard": "Dashboard", "main_sections": "Sections", "main_tree": "Content", diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js index 6acf702546..a3a5b1946d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js @@ -432,13 +432,15 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter, loca throw "args.id cannot be null"; } + var promise = localizationService.localize("contentType_moveFailed"); + return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostMove"), { parentId: args.parentId, id: args.id }, { responseType: 'text' }), - 'Failed to move content'); + promise); }, /** @@ -475,13 +477,15 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter, loca throw "args.id cannot be null"; } + var promise = localizationService.localize("contentType_copyFailed"); + return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostCopy"), { parentId: args.parentId, id: args.id }, { responseType: 'text' }), - 'Failed to copy content'); + promise); }, /** diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js index d194ae2c73..e3fab86067 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js @@ -208,7 +208,7 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter, locali throw "args.id cannot be null"; } - var promise = localizationService.localize("media_moveFailed"); + var promise = localizationService.localize("mediaType_moveFailed"); return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("mediaTypeApiBaseUrl", "PostMove"), @@ -230,7 +230,7 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter, locali throw "args.id cannot be null"; } - var promise = localizationService.localize("media_copyFailed"); + var promise = localizationService.localize("mediaType_copyFailed"); return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("mediaTypeApiBaseUrl", "PostCopy"), diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js index 2314fa6d6c..bf02d9618e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js @@ -3,7 +3,7 @@ * @name umbraco.resources.memberTypeResource * @description Loads in data for member types **/ -function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { +function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter, localizationService) { return { @@ -102,8 +102,29 @@ function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("memberTypeApiBaseUrl", "PostSave"), saveModel), 'Failed to save data for member type id ' + contentType.id); - } + }, + copy: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } + + var promise = localizationService.localize("memberType_copyFailed"); + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("memberTypeApiBaseUrl", "PostCopy"), + { + parentId: args.parentId, + id: args.id + }, { responseType: 'text' }), + promise); + } }; } angular.module('umbraco.resources').factory('memberTypeResource', memberTypeResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js index 14643dc9cd..f9ebba00ea 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js @@ -572,13 +572,15 @@ * Method for opening an item in a list view for editing. * * @param {Object} item The item to edit + * @param {Object} scope The scope with options */ function editItem(item, scope) { + if (!item.editPath) { return; } - if (scope.options.useInfiniteEditor) + if (scope && scope.options && scope.options.useInfiniteEditor) { var editorModel = { id: item.id, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js index 0a4009264d..2b5447cdf6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js @@ -252,10 +252,10 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe //each item has a property alias and the file object, we'll ensure that the alias is suffixed to the key // so we know which property it belongs to on the server side var file = args.files[f]; - var fileKey = "file_" + file.alias + "_" + (file.culture ? file.culture : "") + "_" + (file.segment ? file.segment : ""); + var fileKey = "file_" + (file.alias || '').replace(/_/g, '\\_') + "_" + (file.culture ? file.culture.replace(/_/g, '\\_') : "") + "_" + (file.segment ? file.segment.replace(/_/g, '\\_') : ""); if (Utilities.isArray(file.metaData) && file.metaData.length > 0) { - fileKey += ("_" + file.metaData.join("_")); + fileKey += ("_" + _.map(file.metaData, x => ('' + x).replace(/_/g, '\\_')).join("_")); } formData.append(fileKey, file.file); } diff --git a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js index 74858d652e..d3ab9e519c 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js +++ b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js @@ -19,8 +19,8 @@ angular.module("umbraco.install").factory('installerService', function ($rootSco "Over 500 000 websites are currently powered by Umbraco", "At least 2 people have named their cat 'Umbraco'", "On an average day more than 1000 people download Umbraco", - "umbraco.tv is the premier source of Umbraco video tutorials to get you started", - "You can find the world's friendliest CMS community at our.umbraco.com", + "umbraco.tv is the premier source of Umbraco video tutorials to get you started", + "You can find the world's friendliest CMS community at our.umbraco.com", "You can become a certified Umbraco developer by attending one of the official courses", "Umbraco works really well on tablets", "You have 100% control over your markup and design when crafting a website in Umbraco", @@ -30,7 +30,7 @@ angular.module("umbraco.install").factory('installerService', function ($rootSco "At least 4 people have the Umbraco logo tattooed on them", "'Umbraco' is the Danish name for an allen key", "Umbraco has been around since 2005, that's a looong time in IT", - "More than 700 people from all over the world meet each year in Denmark in May for our annual conference CodeGarden", + "More than 700 people from all over the world meet each year in Denmark in May for our annual conference CodeGarden", "While you are installing Umbraco someone else on the other side of the planet is probably doing it too", "You can extend Umbraco without modifying the source code using either JavaScript or C#", "Umbraco has been installed in more than 198 countries" diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/upgrade.html b/src/Umbraco.Web.UI.Client/src/installer/steps/upgrade.html index 472ceb7135..8ea69b3ee4 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/upgrade.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/upgrade.html @@ -10,7 +10,7 @@ To compare versions and read a report of changes between versions, use the View Report button below.

    - View Report + View Report

    Simply click continue below to be guided through the rest of the upgrade. diff --git a/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less b/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less index 939366d5ac..1f1c2c0e72 100644 --- a/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less +++ b/src/Umbraco.Web.UI.Client/src/less/application/umb-outline.less @@ -15,6 +15,7 @@ right: 0; border-radius: 3px; box-shadow: 0 0 2px 0px @ui-outline, inset 0 0 2px 2px @ui-outline; + pointer-events: none; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/buttons.less b/src/Umbraco.Web.UI.Client/src/less/buttons.less index c446a02424..d1a426f818 100644 --- a/src/Umbraco.Web.UI.Client/src/less/buttons.less +++ b/src/Umbraco.Web.UI.Client/src/less/buttons.less @@ -65,15 +65,22 @@ // -------------------------------------------------- .btn-reset { - padding: 0; - margin: 0; - border: none; + padding: 0; + margin: 0; + border: none; background: none; - color: currentColor; + color: currentColor; font-family: @baseFontFamily; font-size: @baseFontSize; line-height: @baseLineHeight; - cursor: pointer; + cursor: pointer; + + // Disabled state + &.disabled, + &[disabled], + &:disabled:hover { + cursor: default; + } } // Button Sizes diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less index 9d2782f184..ce9286e5f5 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less @@ -69,7 +69,6 @@ button.umb-variant-switcher__toggle { .umb-variant-switcher__expand { color: @ui-action-discreet-type; - margin-top: 3px; margin-left: 5px; margin-right: -5px; transition: color 0.2s ease-in-out; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-search-filter.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-search-filter.less index b96d3e8569..b38f5937c7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-search-filter.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-search-filter.less @@ -21,11 +21,12 @@ html .umb-search-filter { // "icon-search" class it kept for backward compatibility .umb-icon, .icon-search { - color: #d8d7d9; + color: @gray-8; position: absolute; top: 0; bottom: 0; left: 10px; margin: auto 0; + pointer-events: none; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index f38ba8f806..3782fca695 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -308,7 +308,14 @@ select[size] { input[type="file"], input[type="radio"], input[type="checkbox"] { - .umb-outline(); + &:focus { + border-color: @inputBorderFocus; + outline: 0; + + .tabbing-active & { + outline: 2px solid @ui-outline; + } + } } @@ -582,19 +589,21 @@ table.domains .help-inline { } } .add-on { - display: inline-block; + display: inline-flex; + align-items: center; + justify-content: center; width: auto; height: 22px; min-width: 18px; - padding: 5px 6px 3px 6px; + padding: 4px 6px; font-size: @baseFontSize; font-weight: normal; line-height: @baseLineHeight; text-align: center; - //text-shadow: 0 1px 0 @white; background-color: @white; border: 1px solid @inputBorder; color: @ui-option-type; + &:hover { border-color:@inputBorderFocus; color: @ui-option-type-hover; diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 31bb8484c4..66afbfd73f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -402,6 +402,10 @@ table thead button:focus{ /* UI interactions */ +.ui-sortable-handle { + cursor: move; +} + .umb-table tbody.ui-sortable tr { cursor:pointer; diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index a036267c85..cc87a0edf5 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -349,6 +349,7 @@ .umb-panel-header-icon { cursor: pointer; + font-size: 2rem; margin-right: 5px; margin-top: -6px; height: 50px; diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index 0d8f270f1b..f5e652aa3d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -838,22 +838,25 @@ // // Date/time picker // -------------------------------------------------- -.bootstrap-datetimepicker-widget .btn{padding: 0;} -.bootstrap-datetimepicker-widget .picker-switch .btn{ background: none; border: none;} -.umb-datepicker .input-append .add-on{cursor: pointer;} -.umb-datepicker .input-append .on-top { - border: 0 none; +.bootstrap-datetimepicker-widget .btn {padding: 0;} +.bootstrap-datetimepicker-widget .picker-switch .btn { background: none; border: none;} +.umb-datepicker .input-append .btn-clear { + border: none; position: absolute; margin-left: -31px; margin-top: 1px; - display: inline-block; - padding: 5px 6px 3px 6px; + display: inline-flex; + align-items: center; + justify-content: center; + height: 30px; + padding: 4px 6px; font-size: @baseFontSize; font-weight: normal; line-height: @baseLineHeight; text-align: center; background-color: @white; color: @ui-option-type; + &:hover { color: @ui-option-type-hover; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html index 093e69b5ed..0a2a8223e9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html @@ -16,7 +16,7 @@

    - + + ng-disabled="model.target.id || model.target.udi" + id="urlLinkPicker"/> - + + ng-model="model.target.anchor" + id="anchor"/> @@ -41,19 +43,21 @@
    - + + ng-model="model.target.name" + id="nodeNameLinkPicker"/> - + + text="{{vm.labels.openInNewWindow}}" + input-id="openInNewWindow"> @@ -61,36 +65,35 @@
    Link to page
    - + +
    - - +
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.controller.js index 40338f2dca..1701553efc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.controller.js @@ -1,12 +1,13 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEditorHelper, macroService, formHelper, localizationService) { $scope.macros = []; + $scope.a11yInfo = ""; $scope.model.selectedMacro = null; $scope.model.macroParams = []; - + $scope.displayA11YMessageForFilter = displayA11YMessageForFilter; $scope.wizardStep = "macroSelect"; $scope.noMacroParams = false; - + $scope.model.searchTerm = ""; function onInit() { if (!$scope.model.title) { localizationService.localize("defaultdialogs_selectMacro").then(function (value) { @@ -49,6 +50,7 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi $scope.model.submit($scope.model); } else { $scope.wizardStep = 'macroSelect'; + displayA11yMessages($scope.macros); } } else { @@ -95,6 +97,28 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi }); } + function displayA11yMessages(macros) { + if ($scope.noMacroParams || !macros || macros.length === 0) + localizationService.localize("general_searchNoResult").then(function (value) { + $scope.a11yInfo = value; + }); + else if (macros) { + if (macros.length === 1) { + localizationService.localize("treeSearch_searchResult").then(function(value) { + $scope.a11yInfo = "1 " + value; + }); + } else { + localizationService.localize("treeSearch_searchResults").then(function (value) { + $scope.a11yInfo = macros.length + " " + value; + }); + } + } + } + + function displayA11YMessageForFilter() { + var macros = _.filter($scope.macros, v => v.name.toLowerCase().includes($scope.model.searchTerm.toLowerCase())); + displayA11yMessages(macros); + } //here we check to see if we've been passed a selected macro and if so we'll set the //editor to start with parameter editing if ($scope.model.dialogData && $scope.model.dialogData.macroData) { @@ -141,10 +165,11 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi //we don't have a pre-selected macro so ensure the correct step is set $scope.wizardStep = 'macroSelect'; } + displayA11yMessages($scope.macros); }); onInit(); - + } angular.module("umbraco").controller("Umbraco.Overlays.MacroPickerController", MacroPickerController); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html index 33d7a471a5..8bda49b328 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html @@ -16,18 +16,18 @@
    - - + + - +

      -
    • +
    + position="center"> There are no macros available to insert @@ -53,7 +53,7 @@
  • - + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.controller.js index a7021b2867..33d526c3cf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/userpicker/userpicker.controller.js @@ -1,8 +1,8 @@ (function () { "use strict"; - function UserPickerController($scope, usersResource, localizationService, eventsService) { - + function UserPickerController($scope, entityResource, localizationService, eventsService) { + var vm = this; vm.users = []; @@ -102,17 +102,9 @@ vm.loading = true; // Get users - usersResource.getPagedResults(vm.usersOptions).then(function (users) { - - vm.users = users.items; - - vm.usersOptions.pageNumber = users.pageNumber; - vm.usersOptions.pageSize = users.pageSize; - vm.usersOptions.totalItems = users.totalItems; - vm.usersOptions.totalPages = users.totalPages; - + entityResource.getAll("User").then(function (data) { + vm.users = data; preSelect($scope.model.selection, vm.users); - vm.loading = false; }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html index 1e570b4af6..c4b6a4a2ed 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html @@ -226,7 +226,8 @@
    - + +
    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 6978672e99..6e33633512 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 @@ -96,6 +96,7 @@
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-search-filter.html b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-search-filter.html index d6fde29090..ab21654f91 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-search-filter.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-search-filter.html @@ -2,27 +2,14 @@
    - - - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-box.html b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-box.html index 77498cd007..054472e4b6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-box.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-box.html @@ -2,6 +2,7 @@ - - Sorry, we can not find what you are looking for. - - -
      -
    • -
        -
      • -
        - -
        + +

        Sorry, we can not find what you are looking for.

        +
        +

        1 item returned

        +

        {{results.length}} items returned

        +
          +
        • +
            +
          • +
            + +
            +
          • +
        • -
        -
      • -
      +
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html index 974f8d6b4e..273f56d256 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html @@ -43,12 +43,14 @@ required val-server-field="password" ng-minlength="{{vm.config.minPasswordLength}}" - no-dirty-check /> + no-dirty-check + ng-keyup="vm.newPasswordKeyUp($event)"/> Required Minimum {{vm.config.minPasswordLength}} characters {{changePasswordForm.password.errorMsg}} + diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/media/mediadashboardvideos.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/media/mediadashboardvideos.html index 96d6a3f40a..670fae2f6e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/media/mediadashboardvideos.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/media/mediadashboardvideos.html @@ -1,5 +1,5 @@

    Hours of Umbraco training videos are only a click away

    -

    Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

    +

    Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

    - + {{key}} - {{val}} + {{values | umbCmsJoinArray:', '}} diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html index 8b81462ad5..824527be34 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html @@ -20,7 +20,7 @@
    {{source.name}} was copied underneath {{target.name}}
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/export.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/export.html index 0b9feb3fb6..b9aa4d9d3a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/export.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/export.html @@ -1,8 +1,8 @@
    diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html index fe0bde7f1f..b623b6131d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html @@ -20,7 +20,7 @@
    {{source.name}} was moved underneath {{target.name}}
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.html index 9c21f623b5..58968c9dfa 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.html @@ -20,7 +20,7 @@
    {{source.name}} was copied underneath {{target.name}}
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html index 5225a41a0d..6bb1b6fa10 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html @@ -20,7 +20,7 @@
    {{source.name}} was moved underneath {{target.name}}
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/membertypes/copy.controller.js b/src/Umbraco.Web.UI.Client/src/views/membertypes/copy.controller.js new file mode 100644 index 0000000000..aa94b4bd04 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/membertypes/copy.controller.js @@ -0,0 +1,61 @@ +angular.module("umbraco") + .controller("Umbraco.Editors.MemberTypes.CopyController", + function ($scope, memberTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { + + $scope.dialogTreeApi = {}; + $scope.source = _.clone($scope.currentNode); + + function nodeSelectHandler(args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + + $scope.target = args.node; + $scope.target.selected = true; + } + + $scope.copy = function () { + + $scope.busy = true; + $scope.error = false; + + memberTypeResource.copy({ parentId: $scope.target.id, id: $scope.source.id }) + .then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + + //get the currently edited node (if any) + var activeNode = appState.getTreeState("selectedNode"); + + //we need to do a double sync here: first sync to the copied content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was copied!!) + + navigationService.syncTree({ tree: "memberTypes", path: path, forceReload: true, activate: false }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ tree: "memberTypes", path: activeNodePath, forceReload: false, activate: true }); + } + }); + + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + }); + }; + + $scope.onTreeInit = function () { + $scope.dialogTreeApi.callbacks.treeNodeSelect(nodeSelectHandler); + }; + + $scope.close = function() { + navigationService.hideDialog(); + }; + + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/membertypes/copy.html b/src/Umbraco.Web.UI.Client/src/views/membertypes/copy.html new file mode 100644 index 0000000000..fb7c6b5584 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/membertypes/copy.html @@ -0,0 +1,53 @@ +
    + +
    +
    + +

    + Select the folder to copy {{source.name}} to in the tree structure below +

    + + + +
    +
    +
    {{error.errorMsg}}
    +
    {{error.data.message}}
    +
    +
    + +
    +
    + {{source.name}} was copied underneath {{target.name}} +
    + +
    + +
    + +
    + + +
    + +
    +
    +
    + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js index ab7f5c66e0..e80aad64f9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js @@ -36,6 +36,9 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.ChangePasswordCont if (!$scope.model.config || $scope.model.config.minPasswordLength === undefined) { $scope.model.config.minPasswordLength = 0; } + if (!$scope.model.config || $scope.model.config.minNonAlphaNumericChars === undefined) { + $scope.model.config.minNonAlphaNumericChars = 0; + } //set the model defaults if (!Utilities.isObject($scope.model.value)) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html index 9501a6631b..f5ac69b9b8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html @@ -19,12 +19,12 @@ ng-required="model.validation.mandatory" val-server="value" class="datepickerinput" /> - -
    @@ -32,7 +32,7 @@
    -
    +

    {{mandatoryMessage}}

    {{datePickerForm.datepicker.errorMsg}}

    Invalid date

    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html index 26ec22df8d..3ae03a2d7b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html @@ -1,6 +1,7 @@
    - -
    +

    {{mandatoryMessage}}

    Invalid email

    {{emailFieldForm.textbox.errorMsg}}

    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index 716ca405c1..94ea4b8604 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js @@ -1,9 +1,9 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.Grid.MediaController", - function ($scope, userService, editorService, localizationService) { - - $scope.thumbnailUrl = getThumbnailUrl(); - + function ($scope, userService, editorService, localizationService) { + + $scope.thumbnailUrl = getThumbnailUrl(); + if (!$scope.model.config.startNodeId) { if ($scope.model.config.ignoreUserStartNodes === true) { $scope.model.config.startNodeId = -1; @@ -29,16 +29,16 @@ angular.module("umbraco") onlyImages: true, dataTypeKey: $scope.model.dataTypeKey, submit: model => { - updateControlValue(model.selection[0]); + updateControlValue(model.selection[0]); editorService.close(); }, - close: () => editorService.close() + close: () => editorService.close() }; editorService.mediaPicker(mediaPicker); }; - $scope.editImage = function() { + $scope.editImage = function() { const mediaCropDetailsConfig = { size: 'small', @@ -47,17 +47,17 @@ angular.module("umbraco") updateControlValue(model.target); editorService.close(); }, - close: () => editorService.close() + close: () => editorService.close() }; localizationService.localize('defaultdialogs_editSelectedMedia').then(value => { mediaCropDetailsConfig.title = value; editorService.mediaCropDetails(mediaCropDetailsConfig); - }); + }); } - + /** - * + * */ function getThumbnailUrl() { @@ -94,19 +94,15 @@ angular.module("umbraco") return url; } - + return null; } /** - * - * @param {object} selectedImage + * + * @param {object} selectedImage */ function updateControlValue(selectedImage) { - - const doGetThumbnail = $scope.control.value.focalPoint !== selectedImage.focalPoint - || $scope.control.value.image !== selectedImage.image; - // we could apply selectedImage directly to $scope.control.value, // but this allows excluding fields in future if needed $scope.control.value = { @@ -118,10 +114,6 @@ angular.module("umbraco") caption: selectedImage.caption, altText: selectedImage.altText }; - - - if (doGetThumbnail) { - $scope.thumbnailUrl = getThumbnailUrl(); - } - } + $scope.thumbnailUrl = getThumbnailUrl(); + } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index 50146a4c36..15f5ceaa88 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -141,9 +141,9 @@ angular.module("umbraco") over: function (event, ui) { var area = event.target.getScope_HackForSortable().area; - var allowedEditors = area.allowed; + var allowedEditors = area.$allowedEditors.map(e => e.alias); - if (($.inArray(ui.item[0].getScope_HackForSortable().control.editor.alias, allowedEditors) < 0 && allowedEditors) || + if (($.inArray(ui.item[0].getScope_HackForSortable().control.editor.alias, allowedEditors) < 0) || (startingArea != area && area.maxItems != '' && area.maxItems > 0 && area.maxItems < area.controls.length + 1)) { $scope.$apply(function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/integer/integer.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/integer/integer.html index 5c10790400..24a8c33696 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/integer/integer.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/integer/integer.html @@ -9,10 +9,15 @@ aria-required="{{model.validation.mandatory}}" id="{{model.alias}}" val-server="value" - fix-number min="{{model.config.min}}" max="{{model.config.max}}" step="{{model.config.step}}" /> + min="{{model.config.min}}" + max="{{model.config.max}}" + step="{{model.config.step}}" + ng-step="model.config.step" + fix-number /> - + Not a number + Not a valid numeric step size {{integerFieldForm.integerField.errorMsg}} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html index 42597f0c82..7d863f6730 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html index 4527458d16..295345a827 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html @@ -6,20 +6,20 @@
    - +
    - -
    - -
    -
    @@ -32,12 +32,12 @@
    -
    +
    (blank) -

    Be a part of the community

    The Umbraco community is the best of its kind, be sure to visit, and if you have any questions, we're sure that you can get your answers from the community.

    - our.Umbraco → + our.Umbraco →
    diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index 03f462fb9e..ae141e5408 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -37,6 +37,7 @@ + diff --git a/src/Umbraco.Web/Dashboards/ContentDashboard.cs b/src/Umbraco.Web/Dashboards/ContentDashboard.cs index 0cd96f738c..260eb8baf9 100644 --- a/src/Umbraco.Web/Dashboards/ContentDashboard.cs +++ b/src/Umbraco.Web/Dashboards/ContentDashboard.cs @@ -1,15 +1,21 @@ -using Umbraco.Core; +using System.Collections.Generic; +using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Dashboards; +using Umbraco.Core.Services; namespace Umbraco.Web.Dashboards { [Weight(10)] public class ContentDashboard : IDashboard { + private readonly IContentDashboardSettings _dashboardSettings; + private readonly IUserService _userService; + private IAccessRule[] _accessRulesFromConfig; + public string Alias => "contentIntro"; - public string[] Sections => new [] { "content" }; + public string[] Sections => new[] { "content" }; public string View => "views/dashboard/default/startupdashboardintro.html"; @@ -17,13 +23,54 @@ namespace Umbraco.Web.Dashboards { get { - var rules = new IAccessRule[] + var rules = AccessRulesFromConfig; + + if (rules.Length == 0) { - new AccessRule {Type = AccessRuleType.Deny, Value = Constants.Security.TranslatorGroupAlias}, - new AccessRule {Type = AccessRuleType.Grant, Value = Constants.Security.AdminGroupAlias} - }; + rules = new IAccessRule[] + { + new AccessRule {Type = AccessRuleType.Deny, Value = Constants.Security.TranslatorGroupAlias}, + new AccessRule {Type = AccessRuleType.Grant, Value = Constants.Security.AdminGroupAlias} + }; + } + return rules; } } + + private IAccessRule[] AccessRulesFromConfig + { + get + { + if (_accessRulesFromConfig is null) + { + var rules = new List(); + + if (_dashboardSettings.AllowContentDashboardAccessToAllUsers) + { + var allUserGroups = _userService.GetAllUserGroups(); + + foreach (var userGroup in allUserGroups) + { + rules.Add(new AccessRule + { + Type = AccessRuleType.Grant, + Value = userGroup.Alias + }); + } + } + + _accessRulesFromConfig = rules.ToArray(); + } + + return _accessRulesFromConfig; + } + } + + public ContentDashboard(IContentDashboardSettings dashboardSettings, IUserService userService) + { + _dashboardSettings = dashboardSettings; + _userService = userService; + } } } diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index 42b5186c03..6ec9ac4f90 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -57,7 +57,7 @@ namespace Umbraco.Web.Editors var keepOnlyKeys = new Dictionary { {"umbracoUrls", new[] {"authenticationApiBaseUrl", "serverVarsJs", "externalLoginsUrl", "currentUserApiBaseUrl", "iconApiBaseUrl"}}, - {"umbracoSettings", new[] {"allowPasswordReset", "imageFileTypes", "maxFileSize", "loginBackgroundImage", "loginLogoImage", "canSendRequiredEmail", "usernameIsEmail"}}, + {"umbracoSettings", new[] {"allowPasswordReset", "imageFileTypes", "maxFileSize", "loginBackgroundImage", "loginLogoImage", "canSendRequiredEmail", "usernameIsEmail", "minimumPasswordLength", "minimumPasswordNonAlphaNum"}}, {"application", new[] {"applicationPath", "cacheBuster"}}, {"isDebuggingEnabled", new string[] { }}, {"features", new [] {"disabledFeatures"}} @@ -100,6 +100,8 @@ namespace Umbraco.Web.Editors /// internal Dictionary GetServerVariables() { + var userMembershipProvider = Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider(); + var defaultVals = new Dictionary { { @@ -357,6 +359,8 @@ namespace Umbraco.Web.Editors {"showUserInvite", EmailSender.CanSendRequiredEmail}, {"canSendRequiredEmail", EmailSender.CanSendRequiredEmail}, {"showAllowSegmentationForDocumentTypes", false}, + {"minimumPasswordLength", userMembershipProvider.MinRequiredPasswordLength}, + {"minimumPasswordNonAlphaNum", userMembershipProvider.MinRequiredNonAlphanumericCharacters}, } }, { diff --git a/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs b/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs index e0d39b5f65..75060d059a 100644 --- a/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs +++ b/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs @@ -1,5 +1,9 @@ -using System.Net; +using System.Collections.Generic; +using System.Linq; +using System.Net; using System.Net.Http; +using System.Text; +using System.Text.RegularExpressions; using System.Web.Http; using System.Web.Http.Controllers; using Umbraco.Core; @@ -17,6 +21,8 @@ namespace Umbraco.Web.Editors.Binders ///
    internal static class ContentModelBinderHelper { + private const char _escapeChar = '\\'; + public static TModelSave BindModelFromMultipartRequest(HttpActionContext actionContext, ModelBindingContext bindingContext) where TModelSave : IHaveUploadedFiles { @@ -30,6 +36,7 @@ namespace Umbraco.Web.Editors.Binders //The name that has been assigned in JS has 2 or more parts. The second part indicates the property id // for which the file belongs, the remaining parts are just metadata that can be used by the property editor. var parts = file.Headers.ContentDisposition.Name.Trim(Constants.CharArrays.DoubleQuote).Split(Constants.CharArrays.Underscore); + if (parts.Length < 2) { var response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest); diff --git a/src/Umbraco.Web/Editors/CodeFileController.cs b/src/Umbraco.Web/Editors/CodeFileController.cs index 409cded781..a6d142a6ea 100644 --- a/src/Umbraco.Web/Editors/CodeFileController.cs +++ b/src/Umbraco.Web/Editors/CodeFileController.cs @@ -638,7 +638,10 @@ namespace Umbraco.Web.Editors { var path = IOHelper.MapPath(systemDirectory + "/" + virtualPath); var dirInfo = new DirectoryInfo(path); - return dirInfo.Attributes == FileAttributes.Directory; + + // If you turn off indexing in Windows this will have the attribute: + // `FileAttributes.Directory | FileAttributes.NotContentIndexed` + return (dirInfo.Attributes & FileAttributes.Directory) != 0; } // this is an internal class for passing stylesheet data from the client to the controller while editing diff --git a/src/Umbraco.Web/Editors/DashboardController.cs b/src/Umbraco.Web/Editors/DashboardController.cs index da620eb5ac..97db8818f2 100644 --- a/src/Umbraco.Web/Editors/DashboardController.cs +++ b/src/Umbraco.Web/Editors/DashboardController.cs @@ -17,6 +17,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Core.Dashboards; +using Umbraco.Core.Models; using Umbraco.Web.Services; namespace Umbraco.Web.Editors @@ -52,8 +53,9 @@ namespace Umbraco.Web.Editors var allowedSections = string.Join(",", user.AllowedSections); var language = user.Language; var version = UmbracoVersion.SemanticVersion.ToSemanticString(); + var isAdmin = user.IsAdmin(); - var url = string.Format(baseUrl + "{0}?section={0}&allowed={1}&lang={2}&version={3}", section, allowedSections, language, version); + var url = string.Format(baseUrl + "{0}?section={0}&allowed={1}&lang={2}&version={3}&admin={4}", section, allowedSections, language, version, isAdmin); var key = "umbraco-dynamic-dashboard-" + language + allowedSections.Replace(",", "-") + section; var content = AppCaches.RuntimeCache.GetCacheItem(key); diff --git a/src/Umbraco.Web/Editors/ExamineManagementController.cs b/src/Umbraco.Web/Editors/ExamineManagementController.cs index 49599bc8b9..132cc25404 100644 --- a/src/Umbraco.Web/Editors/ExamineManagementController.cs +++ b/src/Umbraco.Web/Editors/ExamineManagementController.cs @@ -25,7 +25,6 @@ namespace Umbraco.Web.Editors private readonly IAppPolicyCache _runtimeCache; private readonly IndexRebuilder _indexRebuilder; - public ExamineManagementController(IExamineManager examineManager, ILogger logger, AppCaches appCaches, IndexRebuilder indexRebuilder) @@ -79,14 +78,11 @@ namespace Umbraco.Web.Editors { Id = x.Id, Score = x.Score, - //order the values by key - Values = new Dictionary(x.Values.OrderBy(y => y.Key).ToDictionary(y => y.Key, y => y.Value)) + Values = x.AllValues.OrderBy(y => y.Key).ToDictionary(y => y.Key, y => y.Value) }) }; } - - /// /// Check if the index has been rebuilt /// @@ -113,7 +109,6 @@ namespace Umbraco.Web.Editors return found != null ? null : CreateModel(index); - } /// @@ -167,8 +162,6 @@ namespace Umbraco.Web.Editors } } - - private ExamineIndexModel CreateModel(IIndex index) { var indexName = index.Name; @@ -182,11 +175,13 @@ namespace Umbraco.Web.Editors } var isHealth = indexDiag.IsHealthy(); + var properties = new Dictionary { [nameof(IIndexDiagnostics.DocumentCount)] = indexDiag.DocumentCount, [nameof(IIndexDiagnostics.FieldCount)] = indexDiag.FieldCount, }; + foreach (var p in indexDiag.Metadata) properties[p.Key] = p.Value; @@ -198,7 +193,6 @@ namespace Umbraco.Web.Editors CanRebuild = _indexRebuilder.CanRebuild(index) }; - return indexerModel; } @@ -211,7 +205,6 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } - //if we didn't find anything try to find it by an explicitly declared searcher if (_examineManager.TryGetSearcher(searcherName, out searcher)) return Request.CreateResponse(HttpStatusCode.OK); diff --git a/src/Umbraco.Web/Editors/KeepAliveController.cs b/src/Umbraco.Web/Editors/KeepAliveController.cs index 23815e1bbe..f29ee6c60a 100644 --- a/src/Umbraco.Web/Editors/KeepAliveController.cs +++ b/src/Umbraco.Web/Editors/KeepAliveController.cs @@ -1,14 +1,12 @@ using System.Runtime.Serialization; using System.Web.Http; -using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.Editors { public class KeepAliveController : UmbracoApiController { - [OnlyLocalRequests] + [HttpHead] [HttpGet] public KeepAlivePingResult Ping() { diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index 4bfea76eda..2e665350e7 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -237,6 +237,18 @@ namespace Umbraco.Web.Editors return display; } + /// + /// Copy the member type + /// + /// + /// + public HttpResponseMessage PostCopy(MoveOrCopy copy) + { + return PerformCopy( + copy, + getContentType: i => Services.MemberTypeService.Get(i), + doCopy: (type, i) => Services.MemberTypeService.Copy(type, i)); + } } } diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs index fd76b9d486..33cf89b2e4 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs @@ -49,7 +49,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Security { var message = string.Empty; var success = false; - var url = _runtime.ApplicationUrl; + var url = _runtime.ApplicationUrl.GetLeftPart(UriPartial.Authority); // Access the site home page and check for the headers var request = WebRequest.Create(url); @@ -69,7 +69,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Security } catch (Exception ex) { - message = _textService.Localize("healthcheck/httpsCheckInvalidUrl", new[] { url.ToString(), ex.Message }); + message = _textService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url.ToString(), ex.Message }); } var actions = new List(); diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs index 98f8a83c1d..83fafb79f8 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Net; using System.Security.Cryptography.X509Certificates; -using System.Web; using Umbraco.Core; using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Services; using Umbraco.Web.HealthCheck.Checks.Config; @@ -21,14 +21,16 @@ namespace Umbraco.Web.HealthCheck.Checks.Security private readonly ILocalizedTextService _textService; private readonly IRuntimeState _runtime; private readonly IGlobalSettings _globalSettings; + private readonly IContentSection _contentSection; private const string FixHttpsSettingAction = "fixHttpsSetting"; - public HttpsCheck(ILocalizedTextService textService, IRuntimeState runtime, IGlobalSettings globalSettings) + public HttpsCheck(ILocalizedTextService textService, IRuntimeState runtime, IGlobalSettings globalSettings, IContentSection contentSection) { _textService = textService; _runtime = runtime; _globalSettings = globalSettings; + _contentSection = contentSection; } /// @@ -65,12 +67,25 @@ namespace Umbraco.Web.HealthCheck.Checks.Security // Attempt to access the site over HTTPS to see if it HTTPS is supported // and a valid certificate has been configured var url = _runtime.ApplicationUrl.ToString().Replace("http:", "https:"); + var request = (HttpWebRequest) WebRequest.Create(url); - request.Method = "HEAD"; + request.AllowAutoRedirect = false; try { + var response = (HttpWebResponse)request.GetResponse(); + + // Check for 301/302 as a external login provider such as UmbracoID might be in use + if (response.StatusCode == HttpStatusCode.Moved || response.StatusCode == HttpStatusCode.Redirect) + { + // Reset request to use the static login background image + var absoluteLoginBackgroundImage = $"{url}/{_contentSection.LoginBackgroundImage}"; + + request = (HttpWebRequest)WebRequest.Create(absoluteLoginBackgroundImage); + response = (HttpWebResponse)request.GetResponse(); + } + if (response.StatusCode == HttpStatusCode.OK) { // Got a valid response, check now for if certificate expiring within 14 days diff --git a/src/Umbraco.Web/Models/ContentEditing/SearchResult.cs b/src/Umbraco.Web/Models/ContentEditing/SearchResult.cs index 1cdd539165..d33bc3530e 100644 --- a/src/Umbraco.Web/Models/ContentEditing/SearchResult.cs +++ b/src/Umbraco.Web/Models/ContentEditing/SearchResult.cs @@ -16,6 +16,6 @@ namespace Umbraco.Web.Models.ContentEditing public int FieldCount => Values?.Count ?? 0; [DataMember(Name = "values")] - public IReadOnlyDictionary Values { get; set; } + public IReadOnlyDictionary> Values { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index 862837381a..f9eacd9e73 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -202,8 +202,8 @@ namespace Umbraco.Web.PropertyEditors _richTextPropertyValueEditor.GetReferences(x.Value))) yield return umbracoEntityReference; - foreach (var umbracoEntityReference in mediaValues.SelectMany(x => - _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) + foreach (var umbracoEntityReference in mediaValues.Where(x => x.Value.HasValues) + .SelectMany(x => _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) yield return umbracoEntityReference; } } diff --git a/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs b/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs index de538793a5..2ea7b9e44e 100644 --- a/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs @@ -28,7 +28,7 @@ namespace Umbraco.Web.PropertyEditors Layouts = new[] { new Layout { Name = "List", Icon = "icon-list", IsSystem = 1, Selected = true, Path = "views/propertyeditors/listview/layouts/list/list.html" }, - new Layout { Name = "grid", Icon = "icon-thumbnails-small", IsSystem = 1, Selected = true, Path = "views/propertyeditors/listview/layouts/grid/grid.html" } + new Layout { Name = "Grid", Icon = "icon-thumbnails-small", IsSystem = 1, Selected = true, Path = "views/propertyeditors/listview/layouts/grid/grid.html" } }; IncludeProperties = new [] diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 8aa3b69fb4..c851894149 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -173,8 +173,8 @@ namespace Umbraco.Web return value; // else... if we have a property, at least let the converter return its own - // vision of 'no value' (could be an empty enumerable) - otherwise, defaultValue - return property == null ? defaultValue : property.Value(culture, segment, defaultValue: defaultValue); + // vision of 'no value' (could be an empty enumerable) - otherwise, default + return property == null ? default : property.Value(culture, segment, fallback, defaultValue); } #endregion @@ -814,6 +814,64 @@ namespace Umbraco.Web #endregion + #region Axes: breadcrumbs + + /// + /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified . + /// + /// The content. + /// Indicates whether the specified content should be included. + /// + /// The breadcrumbs (ancestors and self, top to bottom) for the specified . + /// + public static IEnumerable Breadcrumbs(this IPublishedContent content, bool andSelf = true) + { + return content.AncestorsOrSelf(andSelf, null).Reverse(); + } + + /// + /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher or equal to . + /// + /// The content. + /// The minimum level. + /// Indicates whether the specified content should be included. + /// + /// The breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher or equal to . + /// + public static IEnumerable Breadcrumbs(this IPublishedContent content, int minLevel, bool andSelf = true) + { + return content.AncestorsOrSelf(andSelf, n => n.Level >= minLevel).Reverse(); + } + + /// + /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher or equal to the specified root content type . + /// + /// The root content type. + /// The content. + /// Indicates whether the specified content should be included. + /// + /// The breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher or equal to the specified root content type . + /// + public static IEnumerable Breadcrumbs(this IPublishedContent content, bool andSelf = true) + where T : class, IPublishedContent + { + static IEnumerable TakeUntil(IEnumerable source, Func predicate) + { + foreach (var item in source) + { + yield return item; + if (predicate(item)) + { + yield break; + } + } + } + + return TakeUntil(content.AncestorsOrSelf(andSelf, null), n => n is T).Reverse(); + } + + #endregion + #region Axes: descendants, descendants-or-self /// @@ -1271,15 +1329,37 @@ namespace Umbraco.Web #region Axes: custom /// - /// Gets the root content for this content. + /// Gets the root content (ancestor or self at level 1) for the specified . /// /// The content. - /// The 'site' content ie AncestorOrSelf(1). + /// + /// The root content (ancestor or self at level 1) for the specified . + /// + /// + /// This is the same as calling with maxLevel set to 1. + /// public static IPublishedContent Root(this IPublishedContent content) { return content.AncestorOrSelf(1); } + /// + /// Gets the root content (ancestor or self at level 1) for the specified if it's of the specified content type . + /// + /// The content type. + /// The content. + /// + /// The root content (ancestor or self at level 1) for the specified of content type . + /// + /// + /// This is the same as calling with maxLevel set to 1. + /// + public static T Root(this IPublishedContent content) + where T : class, IPublishedContent + { + return content.AncestorOrSelf(1); + } + #endregion #region PropertyAliasesAndNames diff --git a/src/Umbraco.Web/PublishedPropertyExtension.cs b/src/Umbraco.Web/PublishedPropertyExtension.cs index 0c3aa57cc2..6e8647db47 100644 --- a/src/Umbraco.Web/PublishedPropertyExtension.cs +++ b/src/Umbraco.Web/PublishedPropertyExtension.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Models.PublishedContent; @@ -37,13 +36,20 @@ namespace Umbraco.Web // we have a value // try to cast or convert it var value = property.GetValue(culture, segment); - if (value is T valueAsT) return valueAsT; - var valueConverted = value.TryConvertTo(); - if (valueConverted) return valueConverted.Result; + if (value is T valueAsT) + { + return valueAsT; + } - // cannot cast nor convert the value, nothing we can return but 'defaultValue' + var valueConverted = value.TryConvertTo(); + if (valueConverted) + { + return valueConverted.Result; + } + + // cannot cast nor convert the value, nothing we can return but 'default' // note: we don't want to fallback in that case - would make little sense - return defaultValue; + return default; } // we don't have a value, try fallback @@ -57,15 +63,22 @@ namespace Umbraco.Web var noValue = property.GetValue(culture, segment); if (noValue == null) { - return defaultValue; + return default; + } + + if (noValue is T noValueAsT) + { + return noValueAsT; } - if (noValue is T noValueAsT) return noValueAsT; var noValueConverted = noValue.TryConvertTo(); - if (noValueConverted) return noValueConverted.Result; + if (noValueConverted) + { + return noValueConverted.Result; + } - // cannot cast noValue nor convert it, nothing we can return but 'defaultValue' - return defaultValue; + // cannot cast noValue nor convert it, nothing we can return but 'default' + return default; } #endregion diff --git a/src/Umbraco.Web/Routing/PublishedRouter.cs b/src/Umbraco.Web/Routing/PublishedRouter.cs index a02fd5872a..ebf935dcf8 100644 --- a/src/Umbraco.Web/Routing/PublishedRouter.cs +++ b/src/Umbraco.Web/Routing/PublishedRouter.cs @@ -427,7 +427,7 @@ namespace Umbraco.Web.Routing return finder.TryFindContent(request); }); - _profilingLogger.Debug( + _logger.Debug( "Found? {Found} Content: {PublishedContentId}, Template: {TemplateAlias}, Domain: {Domain}, Culture: {Culture}, Is404: {Is404}, StatusCode: {StatusCode}", found, request.HasPublishedContent ? request.PublishedContent.Id : "NULL", @@ -516,55 +516,47 @@ namespace Umbraco.Web.Routing // don't try to find a redirect if the property doesn't exist if (request.PublishedContent.HasProperty(Constants.Conventions.Content.InternalRedirectId) == false) + { return false; + } + + var internalRedirectId = request.PublishedContent.Value(Constants.Conventions.Content.InternalRedirectId)?.ToString(); + + if (internalRedirectId == null) + { + // no value stored, just return, no need to log + return false; + } + + if (int.TryParse(internalRedirectId, out var internalRedirectIdAsInt) && internalRedirectIdAsInt == request.PublishedContent.Id) + { + // redirect to self + _logger.Debug("FollowInternalRedirects: Redirecting to self, ignore"); + return false; + } - var redirect = false; - var valid = false; IPublishedContent internalRedirectNode = null; - var internalRedirectId = request.PublishedContent.Value(Constants.Conventions.Content.InternalRedirectId, defaultValue: -1); - - if (internalRedirectId > 0) + if (internalRedirectIdAsInt > 0) { // try and get the redirect node from a legacy integer ID - valid = true; - internalRedirectNode = request.UmbracoContext.Content.GetById(internalRedirectId); + internalRedirectNode = request.UmbracoContext.Content.GetById(internalRedirectIdAsInt); } - else + else if (GuidUdi.TryParse(internalRedirectId, out var internalRedirectIdAsUdi)) { - var udiInternalRedirectId = request.PublishedContent.Value(Constants.Conventions.Content.InternalRedirectId); - if (udiInternalRedirectId != null) - { - // try and get the redirect node from a UDI Guid - valid = true; - internalRedirectNode = request.UmbracoContext.Content.GetById(udiInternalRedirectId.Guid); - } - } - - if (valid == false) - { - // bad redirect - log and display the current page (legacy behavior) - _logger.Debug("FollowInternalRedirects: Failed to redirect to id={InternalRedirectId}: value is not an int nor a GuidUdi.", - request.PublishedContent.GetProperty(Constants.Conventions.Content.InternalRedirectId).GetSourceValue()); + // try and get the redirect node from a UDI Guid + internalRedirectNode = request.UmbracoContext.Content.GetById(internalRedirectIdAsUdi.Guid); } if (internalRedirectNode == null) { _logger.Debug("FollowInternalRedirects: Failed to redirect to id={InternalRedirectId}: no such published document.", request.PublishedContent.GetProperty(Constants.Conventions.Content.InternalRedirectId).GetSourceValue()); - } - else if (internalRedirectId == request.PublishedContent.Id) - { - // redirect to self - _logger.Debug("FollowInternalRedirects: Redirecting to self, ignore"); - } - else - { - request.SetInternalRedirectPublishedContent(internalRedirectNode); // don't use .PublishedContent here - redirect = true; - _logger.Debug("FollowInternalRedirects: Redirecting to id={InternalRedirectId}", internalRedirectId); + return false; } - return redirect; + request.SetInternalRedirectPublishedContent(internalRedirectNode); // don't use .PublishedContent here + _logger.Debug("FollowInternalRedirects: Redirecting to id={InternalRedirectId}", internalRedirectIdAsInt); + return true; } /// diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index b15641b503..5d97bfe4a2 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -40,7 +40,6 @@ using Current = Umbraco.Web.Composing.Current; using Umbraco.Web.PropertyEditors; using Umbraco.Core.Models; using Umbraco.Web.Models; -using Ganss.XSS; namespace Umbraco.Web.Runtime { @@ -140,15 +139,6 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); - composition.Register(_ => - { - var sanitizer = new HtmlSanitizer(); - sanitizer.AllowedAttributes.UnionWith(Umbraco.Core.Constants.SvgSanitizer.Attributes); - sanitizer.AllowedCssProperties.UnionWith(Umbraco.Core.Constants.SvgSanitizer.Attributes); - sanitizer.AllowedTags.UnionWith(Umbraco.Core.Constants.SvgSanitizer.Tags); - return sanitizer; - },Lifetime.Singleton); - composition.RegisterUnique(factory => ExamineManager.Instance); // configure the container for web diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index a126592ffc..e8cb592536 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -319,7 +319,10 @@ namespace Umbraco.Web.Scheduling // create a new token source since this is a new process _shutdownTokenSource = new CancellationTokenSource(); _shutdownToken = _shutdownTokenSource.Token; - _runningTask = Task.Run(async () => await Pump().ConfigureAwait(false), _shutdownToken); + using (ExecutionContext.SuppressFlow()) + { + _runningTask = Task.Run(async () => await Pump().ConfigureAwait(false), _shutdownToken); + } _logger.Debug("{LogPrefix} Starting", _logPrefix); } @@ -544,10 +547,14 @@ namespace Umbraco.Web.Scheduling try { if (bgTask.IsAsync) + { // configure await = false since we don't care about the context, we're on a background thread. await bgTask.RunAsync(token).ConfigureAwait(false); + } else + { bgTask.Run(); + } } finally // ensure we disposed - unless latched again ie wants to re-run { @@ -710,14 +717,20 @@ namespace Umbraco.Web.Scheduling // with a single aspnet thread during shutdown and we don't want to delay other calls to IRegisteredObject.Stop. if (!immediate) { - return Task.Run(StopInitial, CancellationToken.None); + using (ExecutionContext.SuppressFlow()) + { + return Task.Run(StopInitial, CancellationToken.None); + } } else { lock (_locker) { if (_terminated) return Task.CompletedTask; - return Task.Run(StopImmediate, CancellationToken.None); + using (ExecutionContext.SuppressFlow()) + { + return Task.Run(StopImmediate, CancellationToken.None); + } } } } diff --git a/src/Umbraco.Web/Services/IconService.cs b/src/Umbraco.Web/Services/IconService.cs index 15e673e6ba..fad53103c0 100644 --- a/src/Umbraco.Web/Services/IconService.cs +++ b/src/Umbraco.Web/Services/IconService.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Ganss.XSS; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; @@ -15,13 +14,11 @@ namespace Umbraco.Web.Services public class IconService : IIconService { private readonly IGlobalSettings _globalSettings; - private readonly IHtmlSanitizer _htmlSanitizer; private readonly IAppPolicyCache _cache; - public IconService(IGlobalSettings globalSettings, IHtmlSanitizer htmlSanitizer, AppCaches appCaches) + public IconService(IGlobalSettings globalSettings, AppCaches appCaches) { _globalSettings = globalSettings; - _htmlSanitizer = htmlSanitizer; _cache = appCaches.RuntimeCache; } @@ -78,12 +75,11 @@ namespace Umbraco.Web.Services try { var svgContent = System.IO.File.ReadAllText(iconPath); - var sanitizedString = _htmlSanitizer.Sanitize(svgContent); var svg = new IconModel { Name = iconName, - SvgString = sanitizedString + SvgString = svgContent }; return svg; diff --git a/src/Umbraco.Web/Trees/MemberTreeController.cs b/src/Umbraco.Web/Trees/MemberTreeController.cs index c0a9d15cfa..37496d1bff 100644 --- a/src/Umbraco.Web/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTreeController.cs @@ -129,10 +129,12 @@ namespace Umbraco.Web.Trees if (_isUmbracoProvider) { - nodes.AddRange(Services.MemberTypeService.GetAll() - .Select(memberType => - CreateTreeNode(memberType.Alias, id, queryStrings, memberType.Name, memberType.Icon.IfNullOrWhiteSpace(Constants.Icons.Member), true, - queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/list/" + memberType.Alias))); + nodes.AddRange( + Services.MemberTypeService.GetAll() + .OrderBy(x => x.Name) + .Select(memberType => + CreateTreeNode(memberType.Alias, id, queryStrings, memberType.Name, memberType.Icon.IfNullOrWhiteSpace(Constants.Icons.Member), true, + queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/list/" + memberType.Alias))); } } diff --git a/src/Umbraco.Web/Trees/MemberTypeAndGroupTreeControllerBase.cs b/src/Umbraco.Web/Trees/MemberTypeAndGroupTreeControllerBase.cs index 5e71266bca..61b9b3e063 100644 --- a/src/Umbraco.Web/Trees/MemberTypeAndGroupTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/MemberTypeAndGroupTreeControllerBase.cs @@ -13,6 +13,10 @@ namespace Umbraco.Web.Trees protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) { var nodes = new TreeNodeCollection(); + + // if the request is for folders only then just return + if (queryStrings["foldersonly"].IsNullOrWhiteSpace() == false && queryStrings["foldersonly"] == "1") return nodes; + nodes.AddRange(GetTreeNodesFromService(id, queryStrings)); return nodes; } @@ -30,7 +34,13 @@ namespace Umbraco.Web.Trees } else { - //delete member type/group + var memberType = Services.MemberTypeService.Get(int.Parse(id)); + if (memberType != null) + { + menu.Items.Add(Services.TextService, opensDialog: true); + } + + // delete member type/group menu.Items.Add(Services.TextService, opensDialog: true); } diff --git a/src/Umbraco.Web/Trees/MemberTypeTreeController.cs b/src/Umbraco.Web/Trees/MemberTypeTreeController.cs index 5db9088f20..85f61d5fed 100644 --- a/src/Umbraco.Web/Trees/MemberTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTypeTreeController.cs @@ -29,6 +29,7 @@ namespace Umbraco.Web.Trees root.HasChildren = Services.MemberTypeService.GetAll().Any(); return root; } + protected override IEnumerable GetTreeNodesFromService(string id, FormDataCollection queryStrings) { return Services.MemberTypeService.GetAll() diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index a6096692b4..a0e074c9c1 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -65,9 +65,6 @@ - - 5.0.376 - 2.7.0.100 From 7704d02883306d2b71c82b24f4d424c0ffd062f9 Mon Sep 17 00:00:00 2001 From: Chad Date: Wed, 21 Apr 2021 11:05:37 +1200 Subject: [PATCH 107/289] Merge in #9859 to v8/feature/nucache-perf (#10152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * load only once * Bump version to 8.6.8 * Initial rework of Lock dictionaries * [Issue 5277-146] accessibility - Close 'X' icon next to language drop… (#9264) * [Issue 5277-146] accessibility - Close 'X' icon next to language drop down is identified as "link" - screen reader * add new loacalization key * Fix issue with SqlMainDomLock that cannot use implicit lock timeouts … (#9973) * Fix issue with SqlMainDomLock that cannot use implicit lock timeouts … (#9973) (cherry picked from commit da5351dfcf23daad69fcd73eb74811456ffc34c0) * Adjust unit tests and apply fixes to scope * Add more unit tests, showing current issue * Counting Umbraco.ModelsBuilder and ModelsBuilder.Umbraco namespaces as external providers * Fix dead lock with TypeLoader * Fix errors shown in unit tests * Throw error if all scopes hasn't been disposed * Clean * Fixes and Updates for DB Scope and Ambient Context leaks (#9953) * Adds some scope tests (ported back from netcore) and provides a much better error message, ensure execution context is not flowed to child tasks that shouldn't leak any current ambient context * updates comment * Ensure SqlMainDomLock suppresses execution context too * Since we're awaiting a task in a library method, ConfigureAwait(false) * missing null check Co-authored-by: Elitsa Marinovska * Adds additional error checking and reporting to MainDom/SqlMainDomLock (#9954) Co-authored-by: Elitsa Marinovska * Add copy logic to Media Picker (#9957) * Add copy logic to Media Picker * Add action for copy all * Fix for selectable media item * Wrap calls to map in scopes * Autocomplete scopes * Remove unnecessary aria-hidden attribute from * Remove scope from method that calls another method that has a scope * Fixes #9993 - Cannot save empty image in Grid * Clean * Revert "The Value() method for IPublishedContent was not working with the defaultValue parameter" (#9989) * Use a hashset to keep track of acquired locks This simplifies disposing/checking for locks greatly. * Add images in grid - fixes 9982 (#9987) Co-authored-by: Sebastiaan Janssen * Only create the dicts and hashset when a lock is requested * Clean * Adds a config for configuring the access rules on the content dashboard - by default it granted for all user groups * Adds additional params indicating whether user is admin * Add images in grid - fixes 9982 (#9987) Co-authored-by: Sebastiaan Janssen (cherry picked from commit e2019777fbfc1f9221d040cb9f0b82c57f8552b9) * Bump version to 8.12.2 * #9964 Removed unneeded check for HttpContext * Fix for #9950 - HttpsCheck will now retry using the login background image if inital request returns 301/302. Excessvie Headers check will now check the root url instead of the backoffice * Merge pull request #9994 from umbraco/v8/bugfix/9993 Fixes #9993 - Cannot save empty image in Grid (cherry picked from commit 0ecc933921f2dea9a2a16d6f395b44a039663ec6) * Apply suggestions from review * Fixes #9983 - Getting kicked, if document type has a Umbraco.UserPicker property (#10002) * Fixes #9983 Temporary fix for this issue. using the entityservice like before. * Needed to remove the call to usersResource here as well for displaying the picked items * Don't need usersResource for now * Fixes #9983 - Getting kicked, if document type has a Umbraco.UserPicker property (#10002) * Fixes #9983 Temporary fix for this issue. using the entityservice like before. * Needed to remove the call to usersResource here as well for displaying the picked items * Don't need usersResource for now (cherry picked from commit 45de0a101eaa2b8f16e21a765f32928c7cb968be) * 8539: Allow alias in image cropper (#9266) Co-authored-by: Owain Williams * Wrap dumping dictionaries in a method. * Create method for generating log message And remove forgotten comments. * Fix swedish translation for somethingElse. * Copy member type (#10020) * Add copy dialog for member type * Implement copy action for member type * Create specific localization for content type, media type and member type * Handle "foldersonly" querystring * Add button type attribute * Add a few missing changes of anchor to button element * Null check on scope and options to ensure backward compatibility * Improve performance, readability and handling of FollowInternalRedirects (#9889) * Improve performance, readability and handling of FollowInternalRedirects * Logger didn't like string param Passing string param to _logger.Debug wasn't happy. Changed to pass existing internalRedirectAsInt variable. Co-authored-by: Nathan Woulfe * Update casing of listview layout name * 9097 add contextual password helper (#9256) * update back-office forms * Display tip on reset password page as well * add directive for password tip * integrate directove in login screen * forgot the ng-keyup :-) * adapt tooltip directive to potential different Members and Users password settings * remove watcher Co-authored-by: Nathan Woulfe * Unbind listener Listening for splitViewRequest was only unbound if the split view editor was opened. Not cleaning up the listener caused a memory leak when changing between nodes as the spit view editor was detached but not garbage-collected * Replace icon in date picker with umb-icon component (#10040) * Replace icon in date picker with component * Adjust height of clear button * Update cypress and fix tests * Listview config icons (#10036) * Update icons to use component * Simplify markup and use disabled button * Use move cursor style on sortable handle * Add class for action column * Update setting auto focus * Increase font size of umb-panel-header-icon * Anchor noopener (#10009) * Set rel="noopener" for anchors with target="_blank" * Reverted unwanted changes to Default.cshtml * Align 'Add language' test to netcore * Add new cypress tests * Add indentation * Getting rid of the config file and implementing an appSetting instead * Implementation for IContentDashboardSettings * Cleanup * bool.Try * Taking AllowContentDashboardAccessToAllUsers prop from GlobalSettings to ContentDashboardSettings and saving AccessRulesFromConfig into a backing field * fix support for non run states * Handling multiple values per field in Examine Management * Add Root and Breadcrumbs extension methods for IPublishedContent (#9033) * Fix usage of obsolete CreatorName and WriterName properties * Add generic Root extension method * Add Breadcrumbs extension methods * Orders member type grouping of members alphabetically, matching the listing of member types. * Revert updating deprecated WriterName/CreatorName refs Changing the properties to use the extensions is a good thing (given the props are deprecated), but causes issues deep in tests. I'm reverting that change to fix the tests, and all refs to the deprecated properties should be updated in one sweep, to deal with any other test issues that might crop up. * Handle Invalid format for Upgrade check * Fixes tabbing-mode remains active after closing modal #9790 (#10074) * Allow to pass in boolean to preventEnterSubmit directive (#8639) * Pass in value to preventEnterSubmit directive * Set enabled similar to preventDefault and preventEnterSubmit directives * Update prevent enter submit value * Init value from controller * Use a different default input id prefix for umb-search-filter * Fix typo * Check for truthly value * Revert "Set enabled similar to preventDefault and preventEnterSubmit directives" This reverts commit 536ce855c4545ead82cea77b4013bf9010a8687b. * None pointer events when clicking icon * Use color variable * Fixes tabbing-mode remains active after closing modal #9790 (#10074) (cherry picked from commit c881fa9e7d08c11954e18489827f70cdafceb947) * Null check on scope and options to ensure backward compatibility (cherry picked from commit fe8cd239d2f4c528c1a8a3cf4c50e90bb43cacfc) * Fix validation of step size in integer/numeric field * 9962: Use $allowedEditors instead of allowed (#10086) * 9962: Use $allowedEditors instead of allowed * 9962: Remove redundant statement * fixes #10021 adds ng-form and val-form-manager to the documentation * Improved accessibility of link picker (#10099) * Added support for screeen reader alerts on the embed so that assitive technology knows when a url retrieve has been succesfull. Added labels for the controls Preview reload only triggered if the values for height and width change * Added control ids for the link picker * Add French translation * Accessibility: Alerts the user how many results have been returned on a tree search (#10100) * Added support for screeen reader alerts on the embed so that assitive technology knows when a url retrieve has been succesfull. Added labels for the controls Preview reload only triggered if the values for height and width change * Tree search details the number of search items returned * Add French translations * Updated LightInject to v6.4.0 * Remove HtmlSanitizer once more - see #9803 * Also make sure NuGet installs the correct version of the CodePages dependency * Bump version to 8.13 RC * Fixed copy preserving sort order (#10091) * Revert "Updated LightInject to v6.4.0" This reverts commit fc77252ec756cf90bb74e7fbbe6dd6d75cbdacfc. * Revert "Add copy logic to Media Picker (#9957)" This reverts commit f7c032af65cac83182782c758a3ab79c86b92e70. * Reintroduce old constructor to make non-breaking * Update cypress test to make macros in the grid work again * Attributes could be multiple items, test specifically if `Directory` is an attribute * Accessibility: Adding label fors and control ids for the macro picker (#10101) * Added support for screeen reader alerts on the embed so that assitive technology knows when a url retrieve has been succesfull. Added labels for the controls Preview reload only triggered if the values for height and width change * Added support for label fors for the macro picker and also gave the ,acro search box a title * Now displays a count of the matching macros returned. Please note the language file amends shared with #10100 * Removed src-only class for the display of the count of messages * Updating typo * Removed top-margin from switcher icon * Allow KeepAlive controller Ping method to be requested by non local requests (#10126) * Allow KeepAlive controller Ping method to be requested by non local requests and accept head requests * removed unused references * fix csproj * fix merge * btree serializer optimizations * array pool and nametable optimizations Co-authored-by: Mole Co-authored-by: Sebastiaan Janssen Co-authored-by: Justin Shearer Co-authored-by: Bjarke Berg Co-authored-by: Callum Whyte Co-authored-by: Shannon Co-authored-by: Elitsa Marinovska Co-authored-by: patrickdemooij9 Co-authored-by: Bjarne Fyrstenborg Co-authored-by: Michael Latouche Co-authored-by: Nathan Woulfe Co-authored-by: Markus Johansson Co-authored-by: Jeavon Leopold Co-authored-by: Benjamin Carleski Co-authored-by: Owain Williams Co-authored-by: Jesper Löfgren Co-authored-by: Martin Bentancour Co-authored-by: Ronald Barendse Co-authored-by: Andy Butland Co-authored-by: BeardinaSuit Co-authored-by: Mads Rasmussen Co-authored-by: Rachel Breeze Co-authored-by: Dave de Moel Co-authored-by: ric <60885685+ricbrady@users.noreply.github.com> Co-authored-by: Carole Rennie Logan Co-authored-by: Dennis Öhman --- .../Sync/DatabaseServerMessenger.cs | 90 ++++++++++++------- .../Sync/ISyncBootStateAccessor.cs | 20 +++++ .../Sync/NonRuntimeLevelBootStateAccessor.cs | 19 ++++ src/Umbraco.Core/Sync/SyncBootState.cs | 24 +++++ src/Umbraco.Core/Umbraco.Core.csproj | 3 + .../PublishedContent/NuCacheChildrenTests.cs | 2 + .../PublishedContent/NuCacheTests.cs | 2 + .../Scoping/ScopedNuCacheTests.cs | 1 + .../ContentTypeServiceVariantsTests.cs | 2 + .../TestHelpers/TestSyncBootStateAccessor.cs | 23 +++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + ...aseServerRegistrarAndMessengerComponent.cs | 1 + ...Tree.DictionaryOfPropertyDataSerializer.cs | 8 +- .../JsonContentNestedDataSerializer.cs | 53 ++++++++++- .../PublishedCache/NuCache/NuCacheComposer.cs | 4 + .../NuCache/PublishedSnapshotService.cs | 19 +++- 16 files changed, 233 insertions(+), 39 deletions(-) create mode 100644 src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs create mode 100644 src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs create mode 100644 src/Umbraco.Core/Sync/SyncBootState.cs create mode 100644 src/Umbraco.Tests/TestHelpers/TestSyncBootStateAccessor.cs mode change 100755 => 100644 src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index 49b0d23862..ebc77dbdca 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -28,7 +28,7 @@ namespace Umbraco.Core.Sync // but only processes instructions coming from remote servers, // thus ensuring that instructions run only once // - public class DatabaseServerMessenger : ServerMessengerBase + public class DatabaseServerMessenger : ServerMessengerBase, ISyncBootStateAccessor { private readonly IRuntimeState _runtime; private readonly ManualResetEvent _syncIdle; @@ -172,35 +172,7 @@ namespace Umbraco.Core.Sync lock (_locko) { if (_released) return; - - var coldboot = false; - if (_lastId < 0) // never synced before - { - // we haven't synced - in this case we aren't going to sync the whole thing, we will assume this is a new - // server and it will need to rebuild it's own caches, eg Lucene or the xml cache file. - Logger.Warn("No last synced Id found, this generally means this is a new server/install." - + " The server will build its caches and indexes, and then adjust its last synced Id to the latest found in" - + " the database and maintain cache updates based on that Id."); - - coldboot = true; - } - else - { - //check for how many instructions there are to process, each row contains a count of the number of instructions contained in each - //row so we will sum these numbers to get the actual count. - var count = database.ExecuteScalar("SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new {lastId = _lastId}); - if (count > Options.MaxProcessingInstructionCount) - { - //too many instructions, proceed to cold boot - Logger.Warn( - "The instruction count ({InstructionCount}) exceeds the specified MaxProcessingInstructionCount ({MaxProcessingInstructionCount})." - + " The server will skip existing instructions, rebuild its caches and indexes entirely, adjust its last synced Id" - + " to the latest found in the database and maintain cache updates based on that Id.", - count, Options.MaxProcessingInstructionCount); - - coldboot = true; - } - } + var coldboot = IsColdBoot(database); if (coldboot) { @@ -223,6 +195,40 @@ namespace Umbraco.Core.Sync } } + private bool IsColdBoot(IUmbracoDatabase database) + { + var coldboot = false; + if (_lastId < 0) // never synced before + { + // we haven't synced - in this case we aren't going to sync the whole thing, we will assume this is a new + // server and it will need to rebuild it's own caches, eg Lucene or the xml cache file. + Logger.Warn("No last synced Id found, this generally means this is a new server/install." + + " The server will build its caches and indexes, and then adjust its last synced Id to the latest found in" + + " the database and maintain cache updates based on that Id."); + + coldboot = true; + } + else + { + //check for how many instructions there are to process, each row contains a count of the number of instructions contained in each + //row so we will sum these numbers to get the actual count. + var count = database.ExecuteScalar("SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new {lastId = _lastId}); + if (count > Options.MaxProcessingInstructionCount) + { + //too many instructions, proceed to cold boot + Logger.Warn( + "The instruction count ({InstructionCount}) exceeds the specified MaxProcessingInstructionCount ({MaxProcessingInstructionCount})." + + " The server will skip existing instructions, rebuild its caches and indexes entirely, adjust its last synced Id" + + " to the latest found in the database and maintain cache updates based on that Id.", + count, Options.MaxProcessingInstructionCount); + + coldboot = true; + } + } + + return coldboot; + } + /// /// Synchronize the server (throttled). /// @@ -548,6 +554,30 @@ namespace Umbraco.Core.Sync #endregion + public SyncBootState GetSyncBootState() + { + try + { + ReadLastSynced(); // get _lastId + using (var scope = ScopeProvider.CreateScope()) + { + EnsureInstructions(scope.Database); + bool isColdBoot = IsColdBoot(scope.Database); + + if (isColdBoot) + { + return SyncBootState.ColdBoot; + } + return SyncBootState.HasSyncState; + } + } + catch(Exception ex) + { + Logger.Warn("Error determining Sync Boot State", ex); + return SyncBootState.Unknown; + } + } + #region Notify refreshers private static ICacheRefresher GetRefresher(Guid id) diff --git a/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs new file mode 100644 index 0000000000..4b8500f2d9 --- /dev/null +++ b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Sync +{ + /// + /// Retrieve the state of the sync service + /// + public interface ISyncBootStateAccessor + { + /// + /// Get the boot state + /// + /// + SyncBootState GetSyncBootState(); + } +} diff --git a/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs b/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs new file mode 100644 index 0000000000..70cec6cc96 --- /dev/null +++ b/src/Umbraco.Core/Sync/NonRuntimeLevelBootStateAccessor.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Sync +{ + /// + /// Boot state implementation for when umbraco is not in the run state + /// + public class NonRuntimeLevelBootStateAccessor : ISyncBootStateAccessor + { + public SyncBootState GetSyncBootState() + { + return SyncBootState.Unknown; + } + } +} diff --git a/src/Umbraco.Core/Sync/SyncBootState.cs b/src/Umbraco.Core/Sync/SyncBootState.cs new file mode 100644 index 0000000000..4abc53abba --- /dev/null +++ b/src/Umbraco.Core/Sync/SyncBootState.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Sync +{ + public enum SyncBootState + { + /// + /// Unknown state. Treat as HasSyncState + /// + Unknown = 0, + /// + /// Cold boot. No Sync state + /// + ColdBoot = 1, + /// + /// Warm boot. Sync state present + /// + HasSyncState = 2 + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 1160881304..6b4725c48c 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -190,6 +190,9 @@ + + + diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index afba2dcc4f..75a20ade6f 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -17,6 +17,7 @@ using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; using Umbraco.Core.Strings; +using Umbraco.Core.Sync; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing.Objects; using Umbraco.Tests.Testing.Objects.Accessors; @@ -158,6 +159,7 @@ namespace Umbraco.Tests.PublishedContent Mock.Of(), Mock.Of(), new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), + new TestSyncBootStateAccessor(SyncBootState.HasSyncState), _contentNestedDataSerializerFactory); // invariant is the current default diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs index eee3500495..9feb0d703b 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs @@ -17,6 +17,7 @@ using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; using Umbraco.Core.Strings; +using Umbraco.Core.Sync; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing.Objects; using Umbraco.Tests.Testing.Objects.Accessors; @@ -204,6 +205,7 @@ namespace Umbraco.Tests.PublishedContent Mock.Of(), Mock.Of(), new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), + new TestSyncBootStateAccessor(SyncBootState.HasSyncState), _contentNestedDataSerializerFactory); // invariant is the current default diff --git a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs index be10db3a9d..ad372c00b9 100644 --- a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs @@ -101,6 +101,7 @@ namespace Umbraco.Tests.Scoping Factory.GetInstance(), Mock.Of(), new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), + new TestSyncBootStateAccessor(SyncBootState.HasSyncState), nestedContentDataSerializerFactory); } diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index aaad60f7e9..b252738fee 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -17,6 +17,7 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Core.Sync; +using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; using Umbraco.Web.PublishedCache; @@ -73,6 +74,7 @@ namespace Umbraco.Tests.Services Factory.GetInstance(), Mock.Of(), new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), + new TestSyncBootStateAccessor(SyncBootState.HasSyncState), nestedContentDataSerializerFactory); } diff --git a/src/Umbraco.Tests/TestHelpers/TestSyncBootStateAccessor.cs b/src/Umbraco.Tests/TestHelpers/TestSyncBootStateAccessor.cs new file mode 100644 index 0000000000..e5f6989381 --- /dev/null +++ b/src/Umbraco.Tests/TestHelpers/TestSyncBootStateAccessor.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Sync; + +namespace Umbraco.Tests.TestHelpers +{ + class TestSyncBootStateAccessor : ISyncBootStateAccessor + { + private readonly SyncBootState _syncBootState; + + public TestSyncBootStateAccessor(SyncBootState syncBootState) + { + _syncBootState = syncBootState; + } + public SyncBootState GetSyncBootState() + { + return _syncBootState; + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 46d2216e82..4920bcda2a 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -184,6 +184,7 @@ + diff --git a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs index 2fa9d80779..26ba0db324 100644 --- a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs +++ b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs @@ -72,6 +72,7 @@ namespace Umbraco.Web.Compose composition.SetDatabaseServerMessengerOptions(GetDefaultOptions); composition.SetServerMessenger(); + composition.Register(factory=> factory.GetInstance() as BatchedDatabaseServerMessenger, Lifetime.Singleton); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs index 0b15c0ba4b..1b96538dd0 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs @@ -13,11 +13,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { public IDictionary ReadFrom(Stream stream) { - var dict = new Dictionary(StringComparer.InvariantCultureIgnoreCase); // read properties count var pcount = PrimitiveSerializer.Int32.ReadFrom(stream); + var dict = new Dictionary(pcount,StringComparer.InvariantCultureIgnoreCase); // read each property for (var i = 0; i < pcount; i++) { @@ -28,13 +28,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource var vcount = PrimitiveSerializer.Int32.ReadFrom(stream); // create pdata and add to the dictionary - var pdatas = new List(); + var pdatas = new PropertyData[vcount]; // for each value, read and add to pdata for (var j = 0; j < vcount; j++) { var pdata = new PropertyData(); - pdatas.Add(pdata); + pdatas[j] =pdata; // everything that can be null is read/written as object // even though - culture and segment should never be null here, as 'null' represents @@ -46,7 +46,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource pdata.Value = ReadObject(stream); } - dict[key] = pdatas.ToArray(); + dict[key] = pdatas; } return dict; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs index c4d40f721f..21cd0bf763 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs @@ -1,6 +1,8 @@ using Newtonsoft.Json; using System; +using System.Buffers; using System.Collections.Generic; +using System.IO; using Umbraco.Core.Models; using Umbraco.Core.Serialization; @@ -21,13 +23,20 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource DateTimeZoneHandling = DateTimeZoneHandling.Utc, DateFormatString = "o" }; - + private readonly JsonNameTable _propertyNameTable = new DefaultJsonNameTable(); public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData) { if (stringData == null && byteData != null) throw new NotSupportedException($"{typeof(JsonContentNestedDataSerializer)} does not support byte[] serialization"); - return JsonConvert.DeserializeObject(stringData, _jsonSerializerSettings); + JsonSerializer serializer = JsonSerializer.Create(_jsonSerializerSettings); + using (JsonTextReader reader = new JsonTextReader(new StringReader(stringData))) + { + // reader will get buffer from array pool + reader.ArrayPool = JsonArrayPool.Instance; + reader.PropertyNameTable = _propertyNameTable; + return serializer.Deserialize(reader); + } } public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model) @@ -39,4 +48,44 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return new ContentCacheDataSerializationResult(json, null); } } + public class JsonArrayPool : IArrayPool + { + public static readonly JsonArrayPool Instance = new JsonArrayPool(); + + public char[] Rent(int minimumLength) + { + // get char array from System.Buffers shared pool + return ArrayPool.Shared.Rent(minimumLength); + } + + public void Return(char[] array) + { + // return char array to System.Buffers shared pool + ArrayPool.Shared.Return(array); + } + } + public class AutomaticJsonNameTable : DefaultJsonNameTable + { + int nAutoAdded = 0; + int maxToAutoAdd; + + public AutomaticJsonNameTable(int maxToAdd) + { + this.maxToAutoAdd = maxToAdd; + } + + public override string Get(char[] key, int start, int length) + { + var s = base.Get(key, start, length); + + if (s == null && nAutoAdded < maxToAutoAdd) + { + s = new string(key, start, length); + Add(s); + nAutoAdded++; + } + + return s; + } + } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs index 98d8b91386..6dac3b9afb 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs @@ -1,6 +1,7 @@ using System.Configuration; using Umbraco.Core; using Umbraco.Core.Composing; +using Umbraco.Core.Sync; using Umbraco.Core.PropertyEditors; using Umbraco.Web.PublishedCache.NuCache.DataSource; @@ -27,6 +28,9 @@ namespace Umbraco.Web.PublishedCache.NuCache composition.RegisterUnique(factory => new ContentDataSerializer(new DictionaryOfPropertyDataSerializer())); + //Overriden on Run state in DatabaseServerRegistrarAndMessengerComposer + composition.Register(Lifetime.Singleton); + // register the NuCache database data source composition.RegisterUnique(); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs old mode 100755 new mode 100644 index 3a055223a5..5b3980ad06 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -21,6 +21,7 @@ using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; +using Umbraco.Core.Sync; using Umbraco.Web.Cache; using Umbraco.Web.Install; using Umbraco.Web.PublishedCache.NuCache.DataSource; @@ -62,6 +63,8 @@ namespace Umbraco.Web.PublishedCache.NuCache private bool _localContentDbExists; private bool _localMediaDbExists; + private readonly ISyncBootStateAccessor _syncBootStateAccessor; + // define constant - determines whether to use cache when previewing // to store eg routes, property converted values, anything - caching // means faster execution, but uses memory - not sure if we want it @@ -80,7 +83,10 @@ namespace Umbraco.Web.PublishedCache.NuCache IDataSource dataSource, IGlobalSettings globalSettings, IEntityXmlSerializer entitySerializer, IPublishedModelFactory publishedModelFactory, - UrlSegmentProviderCollection urlSegmentProviders, IContentCacheDataSerializerFactory contentCacheDataSerializerFactory, ContentDataSerializer contentDataSerializer = null) + UrlSegmentProviderCollection urlSegmentProviders, + ISyncBootStateAccessor syncBootStateAccessor, + IContentCacheDataSerializerFactory contentCacheDataSerializerFactory, + ContentDataSerializer contentDataSerializer = null) : base(publishedSnapshotAccessor, variationContextAccessor) { //if (Interlocked.Increment(ref _singletonCheck) > 1) @@ -100,6 +106,8 @@ namespace Umbraco.Web.PublishedCache.NuCache _contentCacheDataSerializerFactory = contentCacheDataSerializerFactory; _contentDataSerializer = contentDataSerializer; + _syncBootStateAccessor = syncBootStateAccessor; + // we need an Xml serializer here so that the member cache can support XPath, // for members this is done by navigating the serialized-to-xml member _entitySerializer = entitySerializer; @@ -218,7 +226,12 @@ namespace Umbraco.Web.PublishedCache.NuCache { var okContent = false; var okMedia = false; - + if (_syncBootStateAccessor.GetSyncBootState() == SyncBootState.ColdBoot) + { + _logger.Warn("Sync Service is in a Cold Boot state. Skip LoadCachesOnStartup as the Sync Service will trigger a full reload"); + _isReady = true; + return; + } try { if (_localContentDbExists) @@ -234,7 +247,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (!okMedia) _logger.Warn("Loading media from local db raised warnings, will reload from database."); } - + if (!okContent) LockAndLoadContent(scope => LoadContentFromDatabaseLocked(scope, true)); From 1a5b88525b59af3594a7f21a3e029b557f5b3944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 22 Apr 2021 10:28:53 +0200 Subject: [PATCH 108/289] Media Picker v3 (#9461) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * set input file accept * Use PreValue file extensions for limiting the files to be chosen in file input * Current state for Warren to review * This should fix up what you need Niels * update csproj * use empty string if fileExtensions is undefined * public interface * initial work * local crops * translations * translation correction * fix misspeling * some progress * filter media picker * align media card grid items correctly * responsive media cropper * always be able to scale 3 times smallest scale * making image cropper property editor responsive * scroll to scale * adjust slider look * rearrange parts of mediaentryeditor * test helper * styling * move controls inside umb-image-crop * seperate umg-cropper-gravity styling * corrected layout * more ui refinement * keep the idea of mandatory out for now. * remove double ; * removed testing code * JSON Property Value Convertor now has an array of property editors to exclude * Property Value Convertor for Media Picker 3 aka Media Picker with Local Crops * Experimenting on best approach to retrieve local crop in razor view when iterating over picked media items * Update ValueConvertor to use ImageCropperValue as part of the model for views as alot of existing CropUrls can then use it * Update extension methods to take an ImageCropperValue model (localCropData) * Forgot to update CSProj for new ValueConvertor * New GetCropUrl @Url.GetCropUrl(crop.Alias, media.LocalCrops) as oppposed to @Url.GetCropUrl(media.LocalCrops, cropAlias:crop.Alias, useCropDimensions: true) * Remove dupe item in CSProj * Use a contains as an opposed to Array.IndexOf * various corrections, SingleMode based on max 1, remove double checkerBackground, enforce validation for Crops, changed error indication * mediapicker v3 * correct version * fixing file ext label text color * clipboard features for MediaPicker v3 * highlight not allowed types * highlight trashed as an error * Media Types Video, Sound, Document and Vector Image * Rename to Audio and VectorGraphics * Add (SVG) in the name for Vector Graphics * adding CSV to Documents * remove this commented code. * remove this commented code * number range should not go below 0, at-least as default until we make that configurable. * use min not ng-min * description for local crops * Error/Limits highlighting reactive * visual adjustments * Enabling opening filtered folders + corrected select hover states * Varous fixes to resolve issues with unit tests. * Refactor MediaType Documents to only contain Article file type * mark as build-in * predefined MediaPicker3 DataTypes, renaming v2 to "old" * set scale bar current value after min and max has been set * added missing } * update when focal point is dragged * adjusted styling for Image Cropper property editor * correcting comment * remove todo - message for trashed media items works * Changed parameter ordering * Introduced new extension method on MediaWithCrops to get croppings urls in with full path * Reintroducing Single Item Mode * use Multiple instead of SingleMode * renaming and adding multiple to preconfigured datatypes * Change existing media picker to use the Clipboard type MEDIA, enabling shared functionality. * clean up unused clipboard parts * adjusted to new amount * correcting test * Fix unit test * Move MediaWithCrops to separate file and move to Core.Models * parseContentForPaste * clean up * ensure crops is an array. * actively enable focal points, so we dont set focal points that aren't used. * only accept files that matches file extensions from Umbraco Settings * Cleanup * Add references from MediaPicker3 to media * corrections from various feedback * remove comment * correct wording * use windowResizeListener Co-authored-by: Warren Buckley Co-authored-by: Niels Lyngsø Co-authored-by: Mads Rasmussen Co-authored-by: Andy Butland Co-authored-by: Bjarke Berg Co-authored-by: Sebastiaan Janssen Co-authored-by: Elitsa Marinovska --- src/Umbraco.Core/Constants-Conventions.cs | 20 + src/Umbraco.Core/Constants-DataTypes.cs | 93 +++- src/Umbraco.Core/Constants-Icons.cs | 22 +- src/Umbraco.Core/Constants-PropertyEditors.cs | 7 +- .../Constants-PropertyTypeGroups.cs | 24 +- .../Migrations/Install/DatabaseDataCreator.cs | 113 ++++- src/Umbraco.Core/Models/DataTypeExtensions.cs | 4 + src/Umbraco.Core/Models/MediaWithCrops.cs | 15 + .../Persistence/Dtos/ContentTypeDto.cs | 2 +- .../Persistence/Dtos/PropertyTypeDto.cs | 2 +- .../ValueConverters/JsonValueConverter.cs | 8 +- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../Composing/TypeLoaderTests.cs | 2 +- .../DataTypeDefinitionRepositoryTest.cs | 2 +- .../Repositories/MediaTypeRepositoryTest.cs | 16 +- .../PublishedContent/PublishedMediaTests.cs | 29 +- .../Services/MediaServiceTests.cs | 4 +- .../Entities/MockedContentTypes.cs | 8 +- .../components/forms/validwhen.directive.js | 12 + .../imaging/umbimagecrop.directive.js | 287 +++++++----- .../imaging/umbimagegravity.directive.js | 129 +++--- .../components/umbmediagrid.directive.js | 27 +- .../upload/umbpropertyfileupload.directive.js | 7 +- .../upload/umbsinglefileupload.directive.js | 15 +- .../validation/valservermatch.directive.js | 4 +- .../src/common/services/clipboard.service.js | 6 + .../common/services/cropperhelper.service.js | 46 +- src/Umbraco.Web.UI.Client/src/less/belle.less | 5 + .../src/less/components/umb-file-icon.less | 2 +- .../src/less/components/umb-media-grid.less | 52 +-- .../src/less/components/umb-range-slider.less | 30 +- .../src/less/mixins.less | 2 +- .../src/less/property-editors.less | 223 ++++++--- .../src/main.controller.js | 24 +- .../mediaentryeditor.controller.js | 183 ++++++++ .../mediaentryeditor/mediaentryeditor.html | 118 +++++ .../mediaentryeditor/mediaentryeditor.less | 122 +++++ .../mediapicker/mediapicker.controller.js | 70 ++- .../mediapicker/mediapicker.html | 104 +++-- .../components/imaging/umb-image-crop.html | 45 +- .../components/imaging/umb-image-gravity.html | 11 +- .../mediacard/umb-media-card-grid.less | 137 ++++++ .../components/mediacard/umb-media-card.html | 47 ++ .../components/mediacard/umb-media-card.less | 186 ++++++++ .../mediacard/umbMediaCard.component.js | 97 ++++ .../src/views/components/umb-media-grid.html | 65 +-- .../upload/umb-property-file-upload.html | 4 +- .../src/views/media/media.edit.controller.js | 64 +-- .../views/prevalueeditors/numberrange.html | 3 +- .../treesourcetypepicker.controller.js | 5 + .../umb-block-list-property-editor.less | 2 +- .../fileupload/fileupload.controller.js | 9 +- .../fileupload/fileupload.html | 3 +- .../imagecropper/imagecropper.controller.js | 6 +- .../imagecropper/imagecropper.html | 8 +- .../mediapicker/mediapicker.controller.js | 144 ++++-- .../mediapicker3/mediapicker3.html | 1 + .../prevalue/mediapicker3.crops.controller.js | 110 +++++ .../prevalue/mediapicker3.crops.html | 96 ++++ .../prevalue/mediapicker3.crops.less | 40 ++ .../umb-media-picker3-property-editor.html | 71 +++ .../umb-media-picker3-property-editor.less | 13 + ...umbMediaPicker3PropertyEditor.component.js | 431 ++++++++++++++++++ ...3PropertyEditor.createButton.controller.js | 18 + src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 12 + src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 12 + .../Umbraco/config/lang/en_us.xml | 12 + src/Umbraco.Web/Editors/MediaController.cs | 27 +- .../ImageCropperTemplateCoreExtensions.cs | 11 + .../ImageCropperTemplateExtensions.cs | 11 +- .../FileExtensionConfigItem.cs | 13 + .../FileUploadConfiguration.cs | 14 + .../FileUploadConfigurationEditor.cs | 12 + .../FileUploadPropertyEditor.cs | 4 + .../PropertyEditors/IFileExtensionConfig.cs | 13 + .../IFileExtensionConfigItem.cs | 11 + .../MediaPicker3Configuration.cs | 60 +++ .../MediaPicker3ConfigurationEditor.cs | 27 ++ .../MediaPicker3PropertyEditor.cs | 64 +++ .../MediaPickerWithCropsValueConverter.cs | 119 +++++ src/Umbraco.Web/Umbraco.Web.csproj | 9 + src/Umbraco.Web/UrlHelperRenderExtensions.cs | 26 ++ 82 files changed, 3344 insertions(+), 569 deletions(-) create mode 100644 src/Umbraco.Core/Models/MediaWithCrops.cs create mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/components/forms/validwhen.directive.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card-grid.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/components/mediacard/umbMediaCard.component.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/mediapicker3.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.createButton.controller.js create mode 100644 src/Umbraco.Web/PropertyEditors/FileExtensionConfigItem.cs create mode 100644 src/Umbraco.Web/PropertyEditors/FileUploadConfiguration.cs create mode 100644 src/Umbraco.Web/PropertyEditors/FileUploadConfigurationEditor.cs create mode 100644 src/Umbraco.Web/PropertyEditors/IFileExtensionConfig.cs create mode 100644 src/Umbraco.Web/PropertyEditors/IFileExtensionConfigItem.cs create mode 100644 src/Umbraco.Web/PropertyEditors/MediaPicker3Configuration.cs create mode 100644 src/Umbraco.Web/PropertyEditors/MediaPicker3ConfigurationEditor.cs create mode 100644 src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs create mode 100644 src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index c1d7103a1c..c8233c8d34 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -118,6 +118,26 @@ namespace Umbraco.Core /// public const string Image = "Image"; + /// + /// MediaType alias for a video. + /// + public const string Video = "Video"; + + /// + /// MediaType alias for an audio. + /// + public const string Audio = "Audio"; + + /// + /// MediaType alias for an article. + /// + public const string Article = "Article"; + + /// + /// MediaType alias for vector graphics. + /// + public const string VectorGraphics = "VectorGraphics"; + /// /// MediaType alias indicating allowing auto-selection. /// diff --git a/src/Umbraco.Core/Constants-DataTypes.cs b/src/Umbraco.Core/Constants-DataTypes.cs index 673da8f9a3..f1af0ba99e 100644 --- a/src/Umbraco.Core/Constants-DataTypes.cs +++ b/src/Umbraco.Core/Constants-DataTypes.cs @@ -25,6 +25,10 @@ namespace Umbraco.Core public const int DropDownSingle = -39; public const int DropDownMultiple = -42; public const int Upload = -90; + public const int UploadVideo = -100; + public const int UploadAudio = -101; + public const int UploadArticle = -102; + public const int UploadVectorGraphics = -103; public const int DefaultContentListView = -95; public const int DefaultMediaListView = -96; @@ -42,7 +46,7 @@ namespace Umbraco.Core /// Defines the identifiers for Umbraco data types as constants for easy centralized access/management. /// public static class Guids - { + { /// /// Guid for Content Picker as string @@ -88,6 +92,49 @@ namespace Umbraco.Core public static readonly Guid MultipleMediaPickerGuid = new Guid(MultipleMediaPicker); + /// + /// Guid for Media Picker v3 as string + /// + public const string MediaPicker3 = "4309A3EA-0D78-4329-A06C-C80B036AF19A"; + + /// + /// Guid for Media Picker v3 + /// + public static readonly Guid MediaPicker3Guid = new Guid(MediaPicker3); + + /// + /// Guid for Media Picker v3 multiple as string + /// + public const string MediaPicker3Multiple = "1B661F40-2242-4B44-B9CB-3990EE2B13C0"; + + /// + /// Guid for Media Picker v3 multiple + /// + public static readonly Guid MediaPicker3MultipleGuid = new Guid(MediaPicker3Multiple); + + + /// + /// Guid for Media Picker v3 single-image as string + /// + public const string MediaPicker3SingleImage = "AD9F0CF2-BDA2-45D5-9EA1-A63CFC873FD3"; + + /// + /// Guid for Media Picker v3 single-image + /// + public static readonly Guid MediaPicker3SingleImageGuid = new Guid(MediaPicker3SingleImage); + + + /// + /// Guid for Media Picker v3 multi-image as string + /// + public const string MediaPicker3MultipleImages = "0E63D883-B62B-4799-88C3-157F82E83ECC"; + + /// + /// Guid for Media Picker v3 multi-image + /// + public static readonly Guid MediaPicker3MultipleImagesGuid = new Guid(MediaPicker3MultipleImages); + + /// /// Guid for Related Links as string /// @@ -307,6 +354,46 @@ namespace Umbraco.Core /// public static readonly Guid UploadGuid = new Guid(Upload); + /// + /// Guid for UploadVideo as string + /// + public const string UploadVideo = "70575fe7-9812-4396-bbe1-c81a76db71b5"; + + /// + /// Guid for UploadVideo + /// + public static readonly Guid UploadVideoGuid = new Guid(UploadVideo); + + /// + /// Guid for UploadAudio as string + /// + public const string UploadAudio = "8f430dd6-4e96-447e-9dc0-cb552c8cd1f3"; + + /// + /// Guid for UploadAudio + /// + public static readonly Guid UploadAudioGuid = new Guid(UploadAudio); + + /// + /// Guid for UploadArticle as string + /// + public const string UploadArticle = "bc1e266c-dac4-4164-bf08-8a1ec6a7143d"; + + /// + /// Guid for UploadArticle + /// + public static readonly Guid UploadArticleGuid = new Guid(UploadArticle); + + /// + /// Guid for UploadVectorGraphics as string + /// + public const string UploadVectorGraphics = "215cb418-2153-4429-9aef-8c0f0041191b"; + + /// + /// Guid for UploadVectorGraphics + /// + public static readonly Guid UploadVectorGraphicsGuid = new Guid(UploadVectorGraphics); + /// /// Guid for Label as string @@ -367,8 +454,8 @@ namespace Umbraco.Core /// Guid for Label decimal /// public static readonly Guid LabelDecimalGuid = new Guid(LabelDecimal); - - + + } } } diff --git a/src/Umbraco.Core/Constants-Icons.cs b/src/Umbraco.Core/Constants-Icons.cs index d5cc37c9a5..e15c1e162b 100644 --- a/src/Umbraco.Core/Constants-Icons.cs +++ b/src/Umbraco.Core/Constants-Icons.cs @@ -59,6 +59,26 @@ /// public const string MediaFile = "icon-document"; + /// + /// System media video icon + /// + public const string MediaVideo = "icon-video"; + + /// + /// System media audio icon + /// + public const string MediaAudio = "icon-sound-waves"; + + /// + /// System media article icon + /// + public const string MediaArticle = "icon-article"; + + /// + /// System media vector icon + /// + public const string MediaVectorGraphics = "icon-picture"; + /// /// System media folder icon /// @@ -93,7 +113,7 @@ /// System packages icon /// public const string Packages = "icon-box"; - + /// /// System property editor icon /// diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index 87739469d1..f69570dc08 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -95,12 +95,17 @@ namespace Umbraco.Core /// ListView. ///
    public const string ListView = "Umbraco.ListView"; - + /// /// Media Picker. /// public const string MediaPicker = "Umbraco.MediaPicker"; + /// + /// Media Picker v.3. + /// + public const string MediaPicker3 = "Umbraco.MediaPicker3"; + /// /// Multiple Media Picker. /// diff --git a/src/Umbraco.Core/Constants-PropertyTypeGroups.cs b/src/Umbraco.Core/Constants-PropertyTypeGroups.cs index d3402e69f8..20ada8c0f4 100644 --- a/src/Umbraco.Core/Constants-PropertyTypeGroups.cs +++ b/src/Umbraco.Core/Constants-PropertyTypeGroups.cs @@ -8,7 +8,7 @@ public static class PropertyTypeGroups { /// - /// Guid for a Image PropertyTypeGroup object. + /// Guid for an Image PropertyTypeGroup object. /// public const string Image = "79ED4D07-254A-42CF-8FA9-EBE1C116A596"; @@ -18,7 +18,27 @@ public const string File = "50899F9C-023A-4466-B623-ABA9049885FE"; /// - /// Guid for a Image PropertyTypeGroup object. + /// Guid for a Video PropertyTypeGroup object. + /// + public const string Video = "2F0A61B6-CF92-4FF4-B437-751AB35EB254"; + + /// + /// Guid for an Audio PropertyTypeGroup object. + /// + public const string Audio = "335FB495-0A87-4E82-B902-30EB367B767C"; + + /// + /// Guid for an Article PropertyTypeGroup object. + /// + public const string Article = "9AF3BD65-F687-4453-9518-5F180D1898EC"; + + /// + /// Guid for a VectorGraphics PropertyTypeGroup object. + /// + public const string VectorGraphics = "F199B4D7-9E84-439F-8531-F87D9AF37711"; + + /// + /// Guid for a Membership PropertyTypeGroup object. /// public const string Membership = "0756729D-D665-46E3-B84A-37ACEAA614F8"; } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 44de611348..264733e5b9 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -107,7 +107,11 @@ namespace Umbraco.Core.Migrations.Install InsertDataTypeNodeDto(Constants.DataTypes.LabelDateTime, 37, Constants.DataTypes.Guids.LabelDateTime, "Label (datetime)"); InsertDataTypeNodeDto(Constants.DataTypes.LabelTime, 38, Constants.DataTypes.Guids.LabelTime, "Label (time)"); InsertDataTypeNodeDto(Constants.DataTypes.LabelDecimal, 39, Constants.DataTypes.Guids.LabelDecimal, "Label (decimal)"); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Upload, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Upload}", SortOrder = 34, UniqueId = Constants.DataTypes.Guids.UploadGuid, Text = "Upload", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Upload, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Upload}", SortOrder = 34, UniqueId = Constants.DataTypes.Guids.UploadGuid, Text = "Upload File", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.UploadVideo, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.UploadVideo}", SortOrder = 35, UniqueId = Constants.DataTypes.Guids.UploadVideoGuid, Text = "Upload Video", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.UploadAudio, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.UploadAudio}", SortOrder = 36, UniqueId = Constants.DataTypes.Guids.UploadAudioGuid, Text = "Upload Audio", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.UploadArticle, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.UploadArticle}", SortOrder = 37, UniqueId = Constants.DataTypes.Guids.UploadArticleGuid, Text = "Upload Article", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.UploadVectorGraphics, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.UploadVectorGraphics}", SortOrder = 38, UniqueId = Constants.DataTypes.Guids.UploadVectorGraphicsGuid, Text = "Upload Vector Graphics", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Textarea, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Textarea}", SortOrder = 33, UniqueId = Constants.DataTypes.Guids.TextareaGuid, Text = "Textarea", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Textbox, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Textbox}", SortOrder = 32, UniqueId = Constants.DataTypes.Guids.TextstringGuid, Text = "Textstring", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.RichtextEditor, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.RichtextEditor}", SortOrder = 4, UniqueId = Constants.DataTypes.Guids.RichtextEditorGuid, Text = "Richtext editor", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); @@ -126,6 +130,10 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1031, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1031", SortOrder = 2, UniqueId = new Guid("f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d"), Text = Constants.Conventions.MediaTypes.Folder, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1032, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1032", SortOrder = 2, UniqueId = new Guid("cc07b313-0843-4aa8-bbda-871c8da728c8"), Text = Constants.Conventions.MediaTypes.Image, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1033, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1033", SortOrder = 2, UniqueId = new Guid("4c52d8ab-54e6-40cd-999c-7a5f24903e4d"), Text = Constants.Conventions.MediaTypes.File, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1034, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1034", SortOrder = 2, UniqueId = new Guid("f6c515bb-653c-4bdc-821c-987729ebe327"), Text = Constants.Conventions.MediaTypes.Video, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1035, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1035", SortOrder = 2, UniqueId = new Guid("a5ddeee0-8fd8-4cee-a658-6f1fcdb00de3"), Text = Constants.Conventions.MediaTypes.Audio, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1036, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1036", SortOrder = 2, UniqueId = new Guid("a43e3414-9599-4230-a7d3-943a21b20122"), Text = Constants.Conventions.MediaTypes.Article, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1037, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1037", SortOrder = 2, UniqueId = new Guid("c4b1efcf-a9d5-41c4-9621-e9d273b52a9c"), Text = "Vector Graphics (SVG)", NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Tags, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Tags}", SortOrder = 2, UniqueId = new Guid("b6b73142-b9c1-4bf8-a16d-e1c23320b549"), Text = "Tags", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.ImageCropper, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.ImageCropper}", SortOrder = 2, UniqueId = new Guid("1df9f033-e6d4-451f-b8d2-e0cbc50a836f"), Text = "Image Cropper", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1044, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1044", SortOrder = 0, UniqueId = new Guid("d59be02f-1df9-4228-aa1e-01917d806cda"), Text = Constants.Conventions.MemberTypes.DefaultAlias, NodeObjectType = Constants.ObjectTypes.MemberType, CreateDate = DateTime.Now }); @@ -133,9 +141,15 @@ namespace Umbraco.Core.Migrations.Install //New UDI pickers with newer Ids _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1046, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1046", SortOrder = 2, UniqueId = new Guid("FD1E0DA5-5606-4862-B679-5D0CF3A52A59"), Text = "Content Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1047, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1047", SortOrder = 2, UniqueId = new Guid("1EA2E01F-EBD8-4CE1-8D71-6B1149E63548"), Text = "Member Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "Media Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = new Guid("9DBBCBBB-2327-434A-B355-AF1B84E5010A"), Text = "Multiple Media Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "Media Picker (old)", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = new Guid("9DBBCBBB-2327-434A-B355-AF1B84E5010A"), Text = "Multiple Media Picker (old)", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1050, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1050", SortOrder = 2, UniqueId = new Guid("B4E3535A-1753-47E2-8568-602CF8CFEE6F"), Text = "Multi URL Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1051, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1051", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3Guid, Text = "Media Picker 3", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1052, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1052", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3MultipleGuid, Text = "Multiple Media Picker 3", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1053, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1053", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3SingleImageGuid, Text = "Image Media Picker 3", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1054, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1054", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3MultipleImagesGuid, Text = "Multiple Image Media Picker 3", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + } private void CreateLockData() @@ -160,6 +174,10 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Constants.Conventions.MediaTypes.Folder, Icon = Constants.Icons.MediaFolder, Thumbnail = Constants.Icons.MediaFolder, IsContainer = false, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Constants.Conventions.MediaTypes.Image, Icon = Constants.Icons.MediaImage, Thumbnail = Constants.Icons.MediaImage, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Constants.Conventions.MediaTypes.File, Icon = Constants.Icons.MediaFile, Thumbnail = Constants.Icons.MediaFile, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 540, NodeId = 1034, Alias = Constants.Conventions.MediaTypes.Video, Icon = Constants.Icons.MediaVideo, Thumbnail = Constants.Icons.MediaVideo, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 541, NodeId = 1035, Alias = Constants.Conventions.MediaTypes.Audio, Icon = Constants.Icons.MediaAudio, Thumbnail = Constants.Icons.MediaAudio, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 542, NodeId = 1036, Alias = Constants.Conventions.MediaTypes.Article, Icon = Constants.Icons.MediaArticle, Thumbnail = Constants.Icons.MediaArticle, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 543, NodeId = 1037, Alias = Constants.Conventions.MediaTypes.VectorGraphics, Icon = Constants.Icons.MediaVectorGraphics, Thumbnail = Constants.Icons.MediaVectorGraphics, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Constants.Conventions.MemberTypes.DefaultAlias, Icon = Constants.Icons.Member, Thumbnail = Constants.Icons.Member, Variations = (byte) ContentVariation.Nothing }); } @@ -207,20 +225,44 @@ namespace Umbraco.Core.Migrations.Install { _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 3, ContentTypeNodeId = 1032, Text = "Image", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Image) }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 4, ContentTypeNodeId = 1033, Text = "File", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.File) }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 52, ContentTypeNodeId = 1034, Text = "Video", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Video) }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 53, ContentTypeNodeId = 1035, Text = "Audio", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Audio) }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 54, ContentTypeNodeId = 1036, Text = "Article", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Article) }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 55, ContentTypeNodeId = 1037, Text = "Vector Graphics", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.VectorGraphics) }); //membership property group _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 11, ContentTypeNodeId = 1044, Text = "Membership", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Membership) }); } private void CreatePropertyTypeData() { - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = Constants.DataTypes.ImageCropper, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.File, Name = "Upload image", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = Constants.DataTypes.ImageCropper, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.File, Name = "Image", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 7, UniqueId = 7.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Width, Name = "Width", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in pixels", Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 8, UniqueId = 8.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Height, Name = "Height", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in pixels", Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 9, UniqueId = 9.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 10, UniqueId = 10.ToGuid(), DataTypeId = -92, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = Constants.DataTypes.Upload, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.File, Name = "Upload file", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = Constants.DataTypes.Upload, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.File, Name = "File", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 25, UniqueId = 25.ToGuid(), DataTypeId = -92, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 26, UniqueId = 26.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); + + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 40, UniqueId = 40.ToGuid(), DataTypeId = Constants.DataTypes.UploadVideo, ContentTypeId = 1034, PropertyTypeGroupId = 52, Alias = Constants.Conventions.Media.File, Name = "Video", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 41, UniqueId = 41.ToGuid(), DataTypeId = -92, ContentTypeId = 1034, PropertyTypeGroupId = 52, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 42, UniqueId = 42.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1034, PropertyTypeGroupId = 52, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); + + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 43, UniqueId = 43.ToGuid(), DataTypeId = Constants.DataTypes.UploadAudio, ContentTypeId = 1035, PropertyTypeGroupId = 53, Alias = Constants.Conventions.Media.File, Name = "Audio", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 44, UniqueId = 44.ToGuid(), DataTypeId = -92, ContentTypeId = 1035, PropertyTypeGroupId = 53, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 45, UniqueId = 45.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1035, PropertyTypeGroupId = 53, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); + + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 46, UniqueId = 46.ToGuid(), DataTypeId = Constants.DataTypes.UploadArticle, ContentTypeId = 1036, PropertyTypeGroupId = 54, Alias = Constants.Conventions.Media.File, Name = "Article", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 47, UniqueId = 47.ToGuid(), DataTypeId = -92, ContentTypeId = 1036, PropertyTypeGroupId = 54, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 48, UniqueId = 48.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1036, PropertyTypeGroupId = 54, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); + + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 49, UniqueId = 49.ToGuid(), DataTypeId = Constants.DataTypes.UploadVectorGraphics, ContentTypeId = 1037, PropertyTypeGroupId = 55, Alias = Constants.Conventions.Media.File, Name = "Vector Graphics", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 50, UniqueId = 50.ToGuid(), DataTypeId = -92, ContentTypeId = 1037, PropertyTypeGroupId = 55, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 51, UniqueId = 51.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1037, PropertyTypeGroupId = 55, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); + + + + //membership property types _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 28, UniqueId = 28.ToGuid(), DataTypeId = Constants.DataTypes.Textarea, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.Comments, Name = Constants.Conventions.Member.CommentsLabel, SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 29, UniqueId = 29.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.FailedPasswordAttempts, Name = Constants.Conventions.Member.FailedPasswordAttemptsLabel, SortOrder = 1, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); @@ -244,6 +286,10 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1031 }); _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1032 }); _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1033 }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1034 }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1035 }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1036 }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1037 }); } private void CreateDataTypeData() @@ -307,6 +353,63 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1049, EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker, DbType = "Ntext", Configuration = "{\"multiPicker\":1}" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1050, EditorAlias = Constants.PropertyEditors.Aliases.MultiUrlPicker, DbType = "Ntext" }); + + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto + { + NodeId = Constants.DataTypes.UploadVideo, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"mp4\"}, {\"id\":1, \"value\":\"webm\"}, {\"id\":2, \"value\":\"ogv\"}]}" + }); + + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto + { + NodeId = Constants.DataTypes.UploadAudio, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"mp3\"}, {\"id\":1, \"value\":\"weba\"}, {\"id\":2, \"value\":\"oga\"}, {\"id\":3, \"value\":\"opus\"}]}" + }); + + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto + { + NodeId = Constants.DataTypes.UploadArticle, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"pdf\"}, {\"id\":1, \"value\":\"docx\"}, {\"id\":2, \"value\":\"doc\"}]}" + }); + + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto + { + NodeId = Constants.DataTypes.UploadVectorGraphics, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"svg\"}]}" + }); + + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { + NodeId = 1051, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, + DbType = "Ntext", + Configuration = "{\"multiple\": false, \"validationLimit\":{\"min\":0,\"max\":1}}" + }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { + NodeId = 1052, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, + DbType = "Ntext", + Configuration = "{\"multiple\": true}" + }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { + NodeId = 1053, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, + DbType = "Ntext", + Configuration = "{\"filter\":\"" + Constants.Conventions.MediaTypes.Image + "\", \"multiple\": false, \"validationLimit\":{\"min\":0,\"max\":1}}" + }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { + NodeId = 1054, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, + DbType = "Ntext", + Configuration = "{\"filter\":\"" + Constants.Conventions.MediaTypes.Image + "\", \"multiple\": true}" + }); } private void CreateRelationTypeData() diff --git a/src/Umbraco.Core/Models/DataTypeExtensions.cs b/src/Umbraco.Core/Models/DataTypeExtensions.cs index f460edbde7..913aa4773e 100644 --- a/src/Umbraco.Core/Models/DataTypeExtensions.cs +++ b/src/Umbraco.Core/Models/DataTypeExtensions.cs @@ -62,6 +62,10 @@ namespace Umbraco.Core.Models Constants.DataTypes.Guids.TextstringGuid, Constants.DataTypes.Guids.TextareaGuid, Constants.DataTypes.Guids.UploadGuid, + Constants.DataTypes.Guids.UploadArticleGuid, + Constants.DataTypes.Guids.UploadAudioGuid, + Constants.DataTypes.Guids.UploadVectorGraphicsGuid, + Constants.DataTypes.Guids.UploadVideoGuid, Constants.DataTypes.Guids.LabelStringGuid, Constants.DataTypes.Guids.LabelDecimalGuid, Constants.DataTypes.Guids.LabelDateTimeGuid, diff --git a/src/Umbraco.Core/Models/MediaWithCrops.cs b/src/Umbraco.Core/Models/MediaWithCrops.cs new file mode 100644 index 0000000000..ef3205bd94 --- /dev/null +++ b/src/Umbraco.Core/Models/MediaWithCrops.cs @@ -0,0 +1,15 @@ +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors.ValueConverters; + +namespace Umbraco.Core.Models +{ + /// + /// Model used in Razor Views for rendering + /// + public class MediaWithCrops + { + public IPublishedContent MediaItem { get; set; } + + public ImageCropperValue LocalCrops { get; set; } + } +} diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentTypeDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentTypeDto.cs index e7a14a26e2..68036dab4b 100644 --- a/src/Umbraco.Core/Persistence/Dtos/ContentTypeDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/ContentTypeDto.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Persistence.Dtos public const string TableName = Constants.DatabaseSchema.Tables.ContentType; [Column("pk")] - [PrimaryKeyColumn(IdentitySeed = 535)] + [PrimaryKeyColumn(IdentitySeed = 700)] public int PrimaryKey { get; set; } [Column("nodeId")] diff --git a/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs b/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs index 572201c94a..f22e4453f4 100644 --- a/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Persistence.Dtos internal class PropertyTypeDto { [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 50)] + [PrimaryKeyColumn(IdentitySeed = 100)] public int Id { get; set; } [Column("dataTypeId")] diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs index 270c8c3b0b..694ebfde27 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core.Composing; @@ -18,6 +19,8 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters { private readonly PropertyEditorCollection _propertyEditors; + string[] ExcludedPropertyEditors = new string[] { Constants.PropertyEditors.Aliases.MediaPicker3 }; + /// /// Initializes a new instance of the class. /// @@ -28,13 +31,16 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters /// /// It is a converter for any value type that is "JSON" + /// Unless it's in the Excluded Property Editors list + /// The new MediaPicker 3 stores JSON but we want to use its own ValueConvertor /// /// /// public override bool IsConverter(IPublishedPropertyType propertyType) { return _propertyEditors.TryGet(propertyType.EditorAlias, out var editor) - && editor.GetValueEditor().ValueType.InvariantEquals(ValueTypes.Json); + && editor.GetValueEditor().ValueType.InvariantEquals(ValueTypes.Json) + && ExcludedPropertyEditors.Contains(propertyType.EditorAlias) == false; } public override Type GetPropertyValueType(IPublishedPropertyType propertyType) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 2ea5292d73..0a453ad75f 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -156,6 +156,7 @@ + diff --git a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs index b0c57b685b..d2bbf3e865 100644 --- a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs @@ -268,7 +268,7 @@ AnotherContentFinder public void GetDataEditors() { var types = _typeLoader.GetDataEditors(); - Assert.AreEqual(40, types.Count()); + Assert.AreEqual(41, types.Count()); } /// diff --git a/src/Umbraco.Tests/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs index ca8ee29ee3..339b3d4931 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs @@ -279,7 +279,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(dataTypeDefinitions, Is.Not.Null); Assert.That(dataTypeDefinitions.Any(), Is.True); Assert.That(dataTypeDefinitions.Any(x => x == null), Is.False); - Assert.That(dataTypeDefinitions.Length, Is.EqualTo(29)); + Assert.That(dataTypeDefinitions.Length, Is.EqualTo(37)); } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs index bb3286daed..e048886dbe 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs @@ -50,7 +50,7 @@ namespace Umbraco.Tests.Persistence.Repositories containerRepository.Save(container2); - var contentType = (IMediaType)MockedContentTypes.CreateVideoMediaType(); + var contentType = (IMediaType)MockedContentTypes.CreateNewMediaType(); contentType.ParentId = container2.Id; repository.Save(contentType); @@ -133,7 +133,7 @@ namespace Umbraco.Tests.Persistence.Repositories containerRepository.Save(container); - var contentType = MockedContentTypes.CreateVideoMediaType(); + var contentType = MockedContentTypes.CreateNewMediaType(); contentType.ParentId = container.Id; repository.Save(contentType); @@ -155,7 +155,7 @@ namespace Umbraco.Tests.Persistence.Repositories containerRepository.Save(container); - IMediaType contentType = MockedContentTypes.CreateVideoMediaType(); + IMediaType contentType = MockedContentTypes.CreateNewMediaType(); contentType.ParentId = container.Id; repository.Save(contentType); @@ -183,7 +183,7 @@ namespace Umbraco.Tests.Persistence.Repositories var repository = CreateRepository(provider); // Act - var contentType = MockedContentTypes.CreateVideoMediaType(); + var contentType = MockedContentTypes.CreateNewMediaType(); repository.Save(contentType); @@ -210,7 +210,7 @@ namespace Umbraco.Tests.Persistence.Repositories { var repository = CreateRepository(provider); - var videoMediaType = MockedContentTypes.CreateVideoMediaType(); + var videoMediaType = MockedContentTypes.CreateNewMediaType(); repository.Save(videoMediaType); @@ -249,7 +249,7 @@ namespace Umbraco.Tests.Persistence.Repositories var repository = CreateRepository(provider); // Act - var mediaType = MockedContentTypes.CreateVideoMediaType(); + var mediaType = MockedContentTypes.CreateNewMediaType(); repository.Save(mediaType); @@ -378,7 +378,7 @@ namespace Umbraco.Tests.Persistence.Repositories { var repository = CreateRepository(provider); - var mediaType = MockedContentTypes.CreateVideoMediaType(); + var mediaType = MockedContentTypes.CreateNewMediaType(); repository.Save(mediaType); @@ -406,7 +406,7 @@ namespace Umbraco.Tests.Persistence.Repositories { var repository = CreateRepository(provider); - var mediaType = MockedContentTypes.CreateVideoMediaType(); + var mediaType = MockedContentTypes.CreateNewMediaType(); repository.Save(mediaType); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs index f801d02c5b..bf84503837 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs @@ -1,29 +1,27 @@ -using System.Web; -using System.Xml.Linq; -using System.Xml.XPath; +using Examine; using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.TestHelpers.Entities; -using Umbraco.Tests.UmbracoExamine; -using Umbraco.Web; using System.Linq; using System.Threading; +using System.Web; using System.Xml; -using Examine; +using System.Xml.Linq; +using System.Xml.XPath; +using Umbraco.Core; using Umbraco.Core.Cache; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Strings; -using Umbraco.Examine; -using Current = Umbraco.Web.Composing.Current; -using Umbraco.Tests.Testing; using Umbraco.Core.Composing; +using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; +using Umbraco.Core.Strings; +using Umbraco.Examine; using Umbraco.Tests.LegacyXmlPublishedCache; +using Umbraco.Tests.TestHelpers.Entities; +using Umbraco.Tests.Testing; using Umbraco.Tests.Testing.Objects.Accessors; +using Umbraco.Tests.UmbracoExamine; +using Umbraco.Web; namespace Umbraco.Tests.PublishedContent { @@ -94,6 +92,7 @@ namespace Umbraco.Tests.PublishedContent Name = "Rich Text", DataTypeId = -87 //tiny mce }); + var existing = ServiceContext.MediaTypeService.GetAll(); ServiceContext.MediaTypeService.Save(mType); var media = MockedMedia.CreateMediaImage(mType, -1); media.Properties["content"].SetValue("
    This is some content
    "); diff --git a/src/Umbraco.Tests/Services/MediaServiceTests.cs b/src/Umbraco.Tests/Services/MediaServiceTests.cs index 52f26ecb4d..d5cec11211 100644 --- a/src/Umbraco.Tests/Services/MediaServiceTests.cs +++ b/src/Umbraco.Tests/Services/MediaServiceTests.cs @@ -163,7 +163,7 @@ namespace Umbraco.Tests.Services { // Arrange var mediaService = ServiceContext.MediaService; - var mediaType = MockedContentTypes.CreateVideoMediaType(); + var mediaType = MockedContentTypes.CreateNewMediaType(); ServiceContext.MediaTypeService.Save(mediaType); var media = mediaService.CreateMedia(string.Empty, -1, "video"); @@ -175,7 +175,7 @@ namespace Umbraco.Tests.Services public void Ensure_Content_Xml_Created() { var mediaService = ServiceContext.MediaService; - var mediaType = MockedContentTypes.CreateVideoMediaType(); + var mediaType = MockedContentTypes.CreateNewMediaType(); ServiceContext.MediaTypeService.Save(mediaType); var media = mediaService.CreateMedia("Test", -1, "video"); diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs index e3bb012dae..1b85787fee 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs @@ -378,13 +378,13 @@ namespace Umbraco.Tests.TestHelpers.Entities return contentType; } - public static MediaType CreateVideoMediaType() + public static MediaType CreateNewMediaType() { var mediaType = new MediaType(-1) { - Alias = "video", - Name = "Video", - Description = "ContentType used for videos", + Alias = "newMediaType", + Name = "New Media Type", + Description = "ContentType used for a new format", Icon = ".sprTreeDoc3", Thumbnail = "doc.png", SortOrder = 1, diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/validwhen.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/validwhen.directive.js new file mode 100644 index 0000000000..63681a380a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/validwhen.directive.js @@ -0,0 +1,12 @@ +angular.module("umbraco.directives").directive('validWhen', function () { + return { + require: 'ngModel', + restrict: 'A', + link: function (scope, element, attr, ngModel) { + + attr.$observe("validWhen", function (newValue) { + ngModel.$setValidity("validWhen", newValue === "true"); + }); + } + }; +}); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js index f1f2cb38e8..744e4280db 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js @@ -6,10 +6,14 @@ **/ angular.module("umbraco.directives") .directive('umbImageCrop', - function ($timeout, cropperHelper) { + function ($timeout, $window, cropperHelper) { + + const MAX_SCALE = 4; + return { restrict: 'E', replace: true, + transclude: true, templateUrl: 'views/components/imaging/umb-image-crop.html', scope: { src: '=', @@ -17,24 +21,29 @@ angular.module("umbraco.directives") height: '@', crop: "=", center: "=", - maxSize: '@' + maxSize: '@?', + alias: '@?', + forceUpdate: '@?' }, link: function (scope, element, attrs) { + var unsubscribe = []; let sliderRef = null; - scope.width = 400; - scope.height = 320; + scope.loaded = false; + scope.width = 0; + scope.height = 0; scope.dimensions = { + element: {}, image: {}, cropper: {}, viewport: {}, - margin: 20, + margin: {}, scale: { - min: 0, - max: 3, + min: 1, + max: MAX_SCALE, current: 1 } }; @@ -45,10 +54,10 @@ angular.module("umbraco.directives") "tooltips": [false], "format": { to: function (value) { - return parseFloat(parseFloat(value).toFixed(3)); //Math.round(value); + return parseFloat(parseFloat(value).toFixed(3)); }, from: function (value) { - return parseFloat(parseFloat(value).toFixed(3)); //Math.round(value); + return parseFloat(parseFloat(value).toFixed(3)); } }, "range": { @@ -59,19 +68,24 @@ angular.module("umbraco.directives") scope.setup = function (slider) { sliderRef = slider; - - // Set slider handle position - sliderRef.noUiSlider.set(scope.dimensions.scale.current); - - // Update slider range min/max - sliderRef.noUiSlider.updateOptions({ - "range": { - "min": scope.dimensions.scale.min, - "max": scope.dimensions.scale.max - } - }); + updateSlider(); }; + function updateSlider() { + if(sliderRef) { + // Update slider range min/max + sliderRef.noUiSlider.updateOptions({ + "range": { + "min": scope.dimensions.scale.min, + "max": scope.dimensions.scale.max + } + }); + + // Set slider handle position + sliderRef.noUiSlider.set(scope.dimensions.scale.current); + } + } + scope.slide = function (values) { if (values) { scope.dimensions.scale.current = parseFloat(values); @@ -84,77 +98,108 @@ angular.module("umbraco.directives") } }; + function onScroll(event) { + // cross-browser wheel delta + var delta = Math.max(-50, Math.min(50, (event.wheelDelta || -event.detail))); + + if(sliderRef) { + var currentScale =sliderRef.noUiSlider.get(); + + var newScale = Math.min(Math.max(currentScale + delta*.001*scope.dimensions.image.ratio, scope.dimensions.scale.min), scope.dimensions.scale.max); + sliderRef.noUiSlider.set(newScale); + scope.$evalAsync(() => { + scope.dimensions.scale.current = newScale; + }); + + if(event.preventDefault) { + event.preventDefault(); + } + } + } + + //live rendering of viewport and image styles - scope.style = function () { - return { - 'height': (parseInt(scope.dimensions.viewport.height, 10)) + 'px', - 'width': (parseInt(scope.dimensions.viewport.width, 10)) + 'px' - }; + function updateStyles() { + scope.maskStyle = { + 'height': (parseInt(scope.dimensions.cropper.height, 10)) + 'px', + 'width': (parseInt(scope.dimensions.cropper.width, 10)) + 'px', + 'top': (parseInt(scope.dimensions.margin.top, 10)) + 'px', + 'left': (parseInt(scope.dimensions.margin.left, 10)) + 'px' + } }; + updateStyles(); //elements var $viewport = element.find(".viewport"); var $image = element.find("img"); var $overlay = element.find(".overlay"); - var $container = element.find(".crop-container"); + + $overlay.bind("focus", function () { + $overlay.bind("DOMMouseScroll mousewheel onmousewheel", onScroll); + }); + $overlay.bind("blur", function () { + $overlay.unbind("DOMMouseScroll mousewheel onmousewheel", onScroll); + }); + //default constraints for drag n drop - var constraints = { left: { max: scope.dimensions.margin, min: scope.dimensions.margin }, top: { max: scope.dimensions.margin, min: scope.dimensions.margin } }; + var constraints = { left: { max: 0, min: 0 }, top: { max: 0, min: 0 } }; scope.constraints = constraints; //set constaints for cropping drag and drop var setConstraints = function () { - constraints.left.min = scope.dimensions.margin + scope.dimensions.cropper.width - scope.dimensions.image.width; - constraints.top.min = scope.dimensions.margin + scope.dimensions.cropper.height - scope.dimensions.image.height; + constraints.left.min = scope.dimensions.cropper.width - scope.dimensions.image.width; + constraints.top.min = scope.dimensions.cropper.height - scope.dimensions.image.height; }; - var setDimensions = function (originalImage) { - originalImage.width("auto"); - originalImage.height("auto"); + var setDimensions = function () { - var image = {}; - image.originalWidth = originalImage.width(); - image.originalHeight = originalImage.height(); - - image.width = image.originalWidth; - image.height = image.originalHeight; - image.left = originalImage[0].offsetLeft; - image.top = originalImage[0].offsetTop; - - scope.dimensions.image = image; + scope.dimensions.image.width = scope.dimensions.image.originalWidth; + scope.dimensions.image.height = scope.dimensions.image.originalHeight; //unscaled editor size - //var viewPortW = $viewport.width(); - //var viewPortH = $viewport.height(); - var _viewPortW = parseInt(scope.width, 10); - var _viewPortH = parseInt(scope.height, 10); + var _cropW = parseInt(scope.width, 10); + var _cropH = parseInt(scope.height, 10); - //if we set a constraint we will scale it down if needed - if (scope.maxSize) { - var ratioCalculation = cropperHelper.scaleToMaxSize( - _viewPortW, - _viewPortH, - scope.maxSize); + var ratioCalculation = cropperHelper.scaleToMaxSize( + _cropW, + _cropH, + scope.dimensions.viewport.width - 40, + scope.dimensions.viewport.height - 40); - //so if we have a max size, override the thumb sizes - _viewPortW = ratioCalculation.width; - _viewPortH = ratioCalculation.height; - } + //so if we have a max size, override the thumb sizes + _cropW = ratioCalculation.width; + _cropH = ratioCalculation.height; - scope.dimensions.viewport.width = _viewPortW + 2 * scope.dimensions.margin; - scope.dimensions.viewport.height = _viewPortH + 2 * scope.dimensions.margin; - scope.dimensions.cropper.width = _viewPortW; // scope.dimensions.viewport.width - 2 * scope.dimensions.margin; - scope.dimensions.cropper.height = _viewPortH; // scope.dimensions.viewport.height - 2 * scope.dimensions.margin; + // set margins: + scope.dimensions.margin.left = (scope.dimensions.viewport.width - _cropW) * 0.5; + scope.dimensions.margin.top = (scope.dimensions.viewport.height - _cropH) * 0.5; + + scope.dimensions.cropper.width = _cropW; + scope.dimensions.cropper.height = _cropH; + updateStyles(); }; //resize to a given ratio var resizeImageToScale = function (ratio) { - //do stuff - var size = cropperHelper.calculateSizeToRatio(scope.dimensions.image.originalWidth, scope.dimensions.image.originalHeight, ratio); - scope.dimensions.image.width = size.width; - scope.dimensions.image.height = size.height; + + var prevWidth = scope.dimensions.image.width; + var prevHeight = scope.dimensions.image.height; + + scope.dimensions.image.width = scope.dimensions.image.originalWidth * ratio; + scope.dimensions.image.height = scope.dimensions.image.originalHeight * ratio; + + var difW = (scope.dimensions.image.width - prevWidth); + var difH = (scope.dimensions.image.height - prevHeight); + + // normalized focus point: + var focusNormX = (-scope.dimensions.image.left + scope.dimensions.cropper.width*.5) / prevWidth; + var focusNormY = (-scope.dimensions.image.top + scope.dimensions.cropper.height*.5) / prevHeight; + + scope.dimensions.image.left = scope.dimensions.image.left - difW * focusNormX; + scope.dimensions.image.top = scope.dimensions.image.top - difH * focusNormY; setConstraints(); validatePosition(scope.dimensions.image.left, scope.dimensions.image.top); @@ -163,10 +208,10 @@ angular.module("umbraco.directives") //resize the image to a predefined crop coordinate var resizeImageToCrop = function () { scope.dimensions.image = cropperHelper.convertToStyle( - scope.crop, + runtimeCrop, { width: scope.dimensions.image.originalWidth, height: scope.dimensions.image.originalHeight }, scope.dimensions.cropper, - scope.dimensions.margin); + 0); var ratioCalculation = cropperHelper.calculateAspectRatioFit( scope.dimensions.image.originalWidth, @@ -178,25 +223,19 @@ angular.module("umbraco.directives") scope.dimensions.scale.current = scope.dimensions.image.ratio; // Update min and max based on original width/height + // Here we update the slider to use the scala of the current setup, i dont know why its made in this way but this is how it is. scope.dimensions.scale.min = ratioCalculation.ratio; - scope.dimensions.scale.max = 2; + // TODO: Investigate wether we can limit users to not scale bigger than the amount of pixels in the source: + //scope.dimensions.scale.max = ratioCalculation.ratio * Math.min(MAX_SCALE, scope.dimensions.image.originalWidth/scope.dimensions.cropper.width); + scope.dimensions.scale.max = ratioCalculation.ratio * MAX_SCALE; + + updateSlider(); }; var validatePosition = function (left, top) { - if (left > constraints.left.max) { - left = constraints.left.max; - } - if (left <= constraints.left.min) { - left = constraints.left.min; - } - - if (top > constraints.top.max) { - top = constraints.top.max; - } - if (top <= constraints.top.min) { - top = constraints.top.min; - } + left = Math.min(Math.max(left, constraints.left.min), constraints.left.max); + top = Math.min(Math.max(top, constraints.top.min), constraints.top.max); if (scope.dimensions.image.left !== left) { scope.dimensions.image.left = left; @@ -209,36 +248,54 @@ angular.module("umbraco.directives") //sets scope.crop to the recalculated % based crop - var calculateCropBox = function () { - scope.crop = cropperHelper.pixelsToCoordinates(scope.dimensions.image, scope.dimensions.cropper.width, scope.dimensions.cropper.height, scope.dimensions.margin); + function calculateCropBox() { + runtimeCrop = cropperHelper.pixelsToCoordinates(scope.dimensions.image, scope.dimensions.cropper.width, scope.dimensions.cropper.height, 0); }; + function saveCropBox() { + scope.crop = Utilities.copy(runtimeCrop); + } //Drag and drop positioning, using jquery ui draggable - var onStartDragPosition, top, left; + //var onStartDragPosition, top, left; + var dragStartPosition = {}; $overlay.draggable({ + start: function (event, ui) { + dragStartPosition.left = scope.dimensions.image.left; + dragStartPosition.top = scope.dimensions.image.top; + }, drag: function (event, ui) { scope.$apply(function () { - validatePosition(ui.position.left, ui.position.top); + validatePosition(dragStartPosition.left + (ui.position.left - ui.originalPosition.left), dragStartPosition.top + (ui.position.top - ui.originalPosition.top)); }); }, stop: function (event, ui) { scope.$apply(function () { //make sure that every validates one more time... - validatePosition(ui.position.left, ui.position.top); + validatePosition(dragStartPosition.left + (ui.position.left - ui.originalPosition.left), dragStartPosition.top + (ui.position.top - ui.originalPosition.top)); calculateCropBox(); - scope.dimensions.image.rnd = Math.random(); + saveCropBox(); }); } }); - var init = function (image) { - scope.loaded = false; + var runtimeCrop; + var init = function () { - //set dimensions on image, viewport, cropper etc - setDimensions(image); + // store original size: + scope.dimensions.image.originalWidth = $image.width(); + scope.dimensions.image.originalHeight = $image.height(); + // runtime Crop, should not be saved until we have interactions: + runtimeCrop = Utilities.copy(scope.crop); + + onViewportSizeChanged(); + + scope.loaded = true; + }; + + function setCrop() { //create a default crop if we haven't got one already var createDefaultCrop = !scope.crop; if (createDefaultCrop) { @@ -275,41 +332,67 @@ angular.module("umbraco.directives") resizeImageToCrop(); } } + } - //sets constaints for the cropper + + function onViewportSizeChanged() { + scope.dimensions.viewport.width = $viewport.width(); + scope.dimensions.viewport.height = $viewport.height(); + + setDimensions(); + setCrop(); setConstraints(); - scope.loaded = true; - }; + } // Watchers - scope.$watchCollection('[width, height]', function (newValues, oldValues) { + unsubscribe.push(scope.$watchCollection('[width, height, alias, forceUpdate]', function (newValues, oldValues) { // We have to reinit the whole thing if // one of the external params changes if (newValues !== oldValues) { - setDimensions($image); + runtimeCrop = Utilities.copy(scope.crop); + setDimensions(); + setCrop(); setConstraints(); } - }); + })); - var throttledResizing = _.throttle(function () { + var throttledScale = _.throttle(() => scope.$evalAsync(() => { resizeImageToScale(scope.dimensions.scale.current); calculateCropBox(); - }, 15); + saveCropBox(); + }), 16); // Happens when we change the scale - scope.$watch("dimensions.scale.current", function (newValue, oldValue) { + unsubscribe.push(scope.$watch("dimensions.scale.current", function (newValue, oldValue) { if (scope.loaded) { - throttledResizing(); + throttledScale(); } - }); + })); + // Init + + //if we have a max-size we will use it, to keep this backwards compatible. + // I dont see this max size begin usefull, as we should aim for responsive UI. + if (scope.maxSize) { + element.css("max-width", parseInt(scope.maxSize, 10) + "px"); + element.css("max-height", parseInt(scope.maxSize, 10) + "px"); + } + $image.on("load", function () { $timeout(function () { - init($image); + init(); }); }); + + windowResizeListener.register(onViewportSizeChanged); + + scope.$on('$destroy', function () { + $image.prop("src", ""); + windowResizeListener.unregister(onViewportSizeChanged); + unsubscribe.forEach(u => u()); + }) } }; }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js index fd9a236f87..277848811b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js @@ -13,8 +13,8 @@ top: 0 }; - var htmlImage = null; //DOM element reference - var htmlOverlay = null; //DOM element reference + var imageElement = null; //DOM element reference + var focalPointElement = null; //DOM element reference var draggable = null; vm.loaded = false; @@ -22,33 +22,33 @@ vm.$onChanges = onChanges; vm.$postLink = postLink; vm.$onDestroy = onDestroy; - vm.style = style; + vm.style = {}; + vm.overlayStyle = {}; vm.setFocalPoint = setFocalPoint; /** Sets the css style for the Dot */ - function style() { - - if (vm.dimensions.width <= 0 || vm.dimensions.height <= 0) { - //this initializes the dimensions since when the image element first loads - //there will be zero dimensions - setDimensions(); - } - - return { + function updateStyle() { + vm.style = { 'top': vm.dimensions.top + 'px', 'left': vm.dimensions.left + 'px' }; + vm.overlayStyle = { + 'width': vm.dimensions.width + 'px', + 'height': vm.dimensions.height + 'px' + }; + }; - function setFocalPoint (event) { + function setFocalPoint(event) { $scope.$emit("imageFocalPointStart"); - var offsetX = event.offsetX - 10; - var offsetY = event.offsetY - 10; + // We do this to get the right position, no matter the focalPoint was clicked. + var viewportPosition = imageElement[0].getBoundingClientRect(); + var offsetX = event.clientX - viewportPosition.left; + var offsetY = event.clientY - viewportPosition.top; calculateGravity(offsetX, offsetY); - - lazyEndEvent(); + $scope.$emit("imageFocalPointStop"); }; /** Initializes the component */ @@ -61,33 +61,30 @@ /** Called when the component has linked everything and the DOM is available */ function postLink() { //elements - htmlImage = $element.find("img"); - htmlOverlay = $element.find(".overlay"); + imageElement = $element.find("img"); + focalPointElement = $element.find(".focalPoint"); //Drag and drop positioning, using jquery ui draggable - draggable = htmlOverlay.draggable({ + draggable = focalPointElement.draggable({ containment: "parent", start: function () { - $scope.$apply(function () { - $scope.$emit("imageFocalPointStart"); - }); + $scope.$emit("imageFocalPointStart"); }, - stop: function () { - $scope.$apply(function () { - var offsetX = htmlOverlay[0].offsetLeft; - var offsetY = htmlOverlay[0].offsetTop; - calculateGravity(offsetX, offsetY); - }); + stop: function (event, ui) { + + var offsetX = ui.position.left; + var offsetY = ui.position.top; + + $scope.$evalAsync(calculateGravity(offsetX, offsetY)); + + $scope.$emit("imageFocalPointStop"); - lazyEndEvent(); } }); - $(window).on('resize.umbImageGravity', function () { - $scope.$apply(function () { - resized(); - }); - }); + window.addEventListener('resize.umbImageGravity', onResizeHandler); + window.addEventListener('resize', onResizeHandler); + //if any ancestor directive emits this event, we need to resize $scope.$on("editors.content.splitViewChanged", function () { @@ -95,12 +92,12 @@ }); //listen for the image DOM element loading - htmlImage.on("load", function () { + imageElement.on("load", function () { $timeout(function () { vm.isCroppable = true; vm.hasDimensions = true; - + if (vm.src) { if (vm.src.endsWith(".svg")) { vm.isCroppable = false; @@ -117,6 +114,8 @@ } setDimensions(); + updateStyle(); + vm.loaded = true; if (vm.onImageLoaded) { vm.onImageLoaded({ @@ -129,16 +128,19 @@ } function onDestroy() { - $(window).off('resize.umbImageGravity'); - if (htmlOverlay) { + window.removeEventListener('resize.umbImageGravity', onResizeHandler); + window.removeEventListener('resize', onResizeHandler); + /* + if (focalPointElement) { // TODO: This should be destroyed but this will throw an exception: // "cannot call methods on draggable prior to initialization; attempted to call method 'destroy'" // I've tried lots of things and cannot get this to work, we weren't destroying before so hopefully // there's no mem leaks? - //htmlOverlay.draggable("destroy"); + focalPointElement.draggable("destroy"); } - if (htmlImage) { - htmlImage.off("load"); + */ + if (imageElement) { + imageElement.off("load"); } } @@ -146,14 +148,21 @@ function resized() { $timeout(function () { setDimensions(); + updateStyle(); }); + /* // Make sure we can find the offset values for the overlay(dot) before calculating // fixes issue with resize event when printing the page (ex. hitting ctrl+p inside the rte) - if (htmlOverlay.is(':visible')) { - var offsetX = htmlOverlay[0].offsetLeft; - var offsetY = htmlOverlay[0].offsetTop; + if (focalPointElement.is(':visible')) { + var offsetX = focalPointElement[0].offsetLeft; + var offsetY = focalPointElement[0].offsetTop; calculateGravity(offsetX, offsetY); } + */ + } + + function onResizeHandler() { + $scope.$evalAsync(resized); } /** Watches the one way binding changes */ @@ -163,17 +172,18 @@ && !Utilities.equals(changes.center.currentValue, changes.center.previousValue)) { //when center changes update the dimensions setDimensions(); + updateStyle(); } } /** Sets the width/height/left/top dimentions based on the image size and the "center" value */ function setDimensions() { - if (vm.isCroppable && htmlImage && vm.center) { - vm.dimensions.width = htmlImage.width(); - vm.dimensions.height = htmlImage.height(); - vm.dimensions.left = vm.center.left * vm.dimensions.width - 10; - vm.dimensions.top = vm.center.top * vm.dimensions.height - 10; + if (vm.isCroppable && imageElement && vm.center) { + vm.dimensions.width = imageElement.width(); + vm.dimensions.height = imageElement.height(); + vm.dimensions.left = vm.center.left * vm.dimensions.width; + vm.dimensions.top = vm.center.top * vm.dimensions.height; } return vm.dimensions.width; @@ -185,31 +195,22 @@ * @param {any} offsetY */ function calculateGravity(offsetX, offsetY) { - vm.onValueChanged({ - left: (offsetX + 10) / vm.dimensions.width, - top: (offsetY + 10) / vm.dimensions.height + left: Math.min(Math.max(offsetX, 0), vm.dimensions.width) / vm.dimensions.width, + top: Math.min(Math.max(offsetY, 0), vm.dimensions.height) / vm.dimensions.height }); - - //vm.center.left = (offsetX + 10) / scope.dimensions.width; - //vm.center.top = (offsetY + 10) / scope.dimensions.height; }; - var lazyEndEvent = _.debounce(function () { - $scope.$apply(function () { - $scope.$emit("imageFocalPointStop"); - }); - }, 2000); - } var umbImageGravityComponent = { templateUrl: 'views/components/imaging/umb-image-gravity.html', bindings: { src: "<", - center: "<", + center: "<", onImageLoaded: "&?", - onValueChanged: "&" + onValueChanged: "&", + disableFocalPoint: " 0) { setFlexValues(scope.items); } @@ -188,7 +188,7 @@ Use this directive to generate a thumbnail grid of media items. } } } - + /** * Returns wether a item should be selectable or not. */ @@ -203,9 +203,9 @@ Use this directive to generate a thumbnail grid of media items. } else { return scope.onlyFolders !== "true"; } - + return false; - + } function setOriginalSize(item, maxHeight) { @@ -255,7 +255,7 @@ Use this directive to generate a thumbnail grid of media items. } } - + function setFlexValues(mediaItems) { var flexSortArray = mediaItems; @@ -292,8 +292,11 @@ Use this directive to generate a thumbnail grid of media items. mediaItem.flexStyle = flexStyle; } } - + scope.clickItem = function(item, $event, $index) { + if (item.isFolder === true && item.filtered) { + scope.clickItemName(item, $event, $index); + } if (scope.onClick) { scope.onClick(item, $event, $index); $event.stopPropagation(); @@ -312,7 +315,7 @@ Use this directive to generate a thumbnail grid of media items. scope.onDetailsHover(item, $event, hover); } }; - + var unbindItemsWatcher = scope.$watch('items', function(newValue, oldValue) { if (Utilities.isArray(newValue)) { activate(); @@ -333,7 +336,7 @@ Use this directive to generate a thumbnail grid of media items. //change sort scope.setSort = function (col) { if (scope.sortColumn === col) { - scope.sortReverse = !scope.sortReverse; + scope.sortReverse = !scope.sortReverse; } else { scope.sortColumn = col; @@ -345,9 +348,9 @@ Use this directive to generate a thumbnail grid of media items. } } scope.sortDirection = scope.sortReverse ? "desc" : "asc"; - + } - // sort function + // sort function scope.sortBy = function (item) { if (scope.sortColumn === "updateDate") { return [-item['isFolder'],item['updateDate']]; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js index db1e38adc6..5492fee1a0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js @@ -85,7 +85,7 @@ /** Called when the component has linked all elements, this is when the form controller is available */ function postLink() { - + } function initialize() { @@ -186,7 +186,7 @@ }); } } - + } } @@ -325,7 +325,8 @@ */ onFilesChanged: "&", onInit: "&", - required: "=" + required: "=", + acceptFileExt: ""; + return { restrict: "E", scope: { - rebuild: "=" + rebuild: "=", + acceptFileExt: "", - link: function (scope, el, attrs) { + template: "
    "+innerTemplate+"
    ", + link: function (scope, el) { scope.$watch("rebuild", function (newVal, oldVal) { if (newVal && newVal !== oldVal) { //recompile it! - el.html(""); + el.html(innerTemplate); $compile(el.contents())(scope); } }); @@ -30,4 +35,4 @@ function umbSingleFileUpload($compile) { }; } -angular.module('umbraco.directives').directive("umbSingleFileUpload", umbSingleFileUpload); \ No newline at end of file +angular.module('umbraco.directives').directive("umbSingleFileUpload", umbSingleFileUpload); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js index 5f8600c8c0..b07ab55436 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js @@ -2,8 +2,8 @@ * @ngdoc directive * @name umbraco.directives.directive:valServerMatch * @restrict A - * @description A custom validator applied to a form/ng-form within an umbProperty that validates server side validation data - * contained within the serverValidationManager. The data can be matched on "exact", "prefix", "suffix" or "contains" matches against + * @description A custom validator applied to a form/ng-form within an umbProperty that validates server side validation data + * contained within the serverValidationManager. The data can be matched on "exact", "prefix", "suffix" or "contains" matches against * a property validation key. The attribute value can be in multiple value types: * - STRING = The property validation key to have an exact match on. If matched, then the form will have a valServerMatch validator applied. * - OBJECT = A dictionary where the key is the match type: "contains", "prefix", "suffix" and the value is either: diff --git a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js index 83fd3d08c2..901e5fa93c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js @@ -17,6 +17,7 @@ function clipboardService($window, notificationsService, eventsService, localSto TYPES.ELEMENT_TYPE = "elementType"; TYPES.BLOCK = "block"; TYPES.RAW = "raw"; + TYPES.MEDIA = "media"; var clearPropertyResolvers = {}; var pastePropertyResolvers = {}; @@ -70,6 +71,9 @@ function clipboardService($window, notificationsService, eventsService, localSto propMethod(data[p], TYPES.RAW); } } + clipboardTypeResolvers[TYPES.MEDIA] = function(data, propMethod) { + // no resolving needed for this type currently. + } var STORAGE_KEY = "umbClipboardService"; @@ -147,6 +151,8 @@ function clipboardService($window, notificationsService, eventsService, localSto return entry.type === type && ( + allowedAliases === null + || (entry.alias && allowedAliases.filter(allowedAlias => allowedAlias === entry.alias).length > 0) || (entry.aliases && entry.aliases.filter(entryAlias => allowedAliases.filter(allowedAlias => allowedAlias === entryAlias).length > 0).length === entry.aliases.length) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/cropperhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/cropperhelper.service.js index 256a1461db..1f860f237c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/cropperhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/cropperhelper.service.js @@ -44,24 +44,23 @@ function cropperHelper(umbRequestHelper, $http) { return { width:srcWidth*ratio, height:srcHeight*ratio, ratio: ratio}; }, - scaleToMaxSize : function(srcWidth, srcHeight, maxSize) { - - var retVal = {height: srcHeight, width: srcWidth}; + scaleToMaxSize : function(srcWidth, srcHeight, maxWidth, maxHeight) { - if(srcWidth > maxSize ||srcHeight > maxSize){ - var ratio = [maxSize / srcWidth, maxSize / srcHeight ]; - ratio = Math.min(ratio[0], ratio[1]); - - retVal.height = srcHeight * ratio; - retVal.width = srcWidth * ratio; - } - - return retVal; + // fallback to maxHeight: + maxHeight = maxHeight || maxWidth; + + // get smallest ratio, if ratio exceeds 1 we will not scale(hence we parse 1 as the maximum allowed ratio) + var ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight, 1); + + return { + width: srcWidth * ratio, + height:srcHeight * ratio + }; }, //returns a ng-style object with top,left,width,height pixel measurements //expects {left,right,top,bottom} - {width,height}, {width,height}, int - //offset is just to push the image position a number of pixels from top,left + //offset is just to push the image position a number of pixels from top,left convertToStyle : function(coordinates, originalSize, viewPort, offset){ var coordinates_px = service.coordinatesToPixels(coordinates, originalSize, offset); @@ -85,14 +84,14 @@ function cropperHelper(umbRequestHelper, $http) { return style; }, - + coordinatesToPixels : function(coordinates, originalSize, offset){ var coordinates_px = { x1: Math.floor(coordinates.x1 * originalSize.width), y1: Math.floor(coordinates.y1 * originalSize.height), x2: Math.floor(coordinates.x2 * originalSize.width), - y2: Math.floor(coordinates.y2 * originalSize.height) + y2: Math.floor(coordinates.y2 * originalSize.height) }; return coordinates_px; @@ -106,25 +105,18 @@ function cropperHelper(umbRequestHelper, $http) { var x2_px = image.width - (x1_px + width); var y2_px = image.height - (y1_px + height); - //crop coordinates in % var crop = {}; - crop.x1 = x1_px / image.width; - crop.y1 = y1_px / image.height; - crop.x2 = x2_px / image.width; - crop.y2 = y2_px / image.height; - - for(var coord in crop){ - if(crop[coord] < 0){ - crop[coord] = 0; - } - } + crop.x1 = Math.max(x1_px / image.width, 0); + crop.y1 = Math.max(y1_px / image.height, 0); + crop.x2 = Math.max(x2_px / image.width, 0); + crop.y2 = Math.max(y2_px / image.height, 0); return crop; }, alignToCoordinates : function(image, center, viewport){ - + var min_left = (image.width) - (viewport.width); var min_top = (image.height) - (viewport.height); diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 359c3dd427..6f95608d7a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -215,6 +215,11 @@ @import "../views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less"; @import "../views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less"; @import "../views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less"; +@import "../views/components/mediacard/umb-media-card-grid.less"; +@import "../views/components/mediacard/umb-media-card.less"; +@import "../views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.less"; +@import "../views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.less"; +@import "../views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.less"; // Utilities diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less index febee80a97..ffe87277e6 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less @@ -20,7 +20,7 @@ > span { position: absolute; - color: @white; + color: @ui-active-type; background: @ui-active; padding: 1px 3px; font-size: 10px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less index 5f79d65de1..71be01e6ff 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less @@ -34,21 +34,6 @@ } -.umb-media-grid__item.-unselectable { - &::before { - content: ""; - position: absolute; - z-index: 1; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: @baseBorderRadius; - background-color: rgba(230, 230, 230, .8); - pointer-events: none; - } -} - .umb-media-grid__item.-selectable, .umb-media-grid__item.-folder {// If folders isnt selectable, they opens if clicked, therefor... cursor: pointer; @@ -59,21 +44,12 @@ } .umb-media-grid__item.-folder { - &.-selectable { .media-grid-item-edit:hover .umb-media-grid__item-name, .media-grid-item-edit:focus .umb-media-grid__item-name { text-decoration: underline; } } - - &.-unselectable { - &:hover, &:focus { - .umb-media-grid__item-name { - text-decoration: underline; - } - } - } } @@ -85,8 +61,7 @@ } .umb-media-grid__item.-selected, .umb-media-grid__item.-selectable:hover { - &::before { - content: ""; + .umb-media-grid__item-select { position: absolute; z-index:2; top: -2px; @@ -100,15 +75,21 @@ } } .umb-media-grid__item.-selectable:hover { - &::before { + .umb-media-grid__item-select { opacity: .33; } } .umb-media-grid__item.-selected:hover { - &::before { + .umb-media-grid__item-select { opacity: .75; } } +.umb-media-grid__item.-filtered:not(.-folder) { + cursor:not-allowed; + * { + pointer-events: none; + } +} .umb-media-grid__item-file-icon { transform: translate(-50%,-50%); @@ -189,14 +170,25 @@ } } -.umb-media-grid__item-name { - cursor: pointer; +.umb-media-grid__item-overlay { + cursor: pointer; + + &:hover .umb-media-grid__item-name{ + text-decoration: underline; + } +} + +.umb-media-grid__item-overlay:not(.-selected) { + &:hover + .umb-media-grid__item-select { + display: none; + } } .umb-media-grid__item-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less index 6ae92ffa4e..cc5c17ba70 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less @@ -2,19 +2,35 @@ .umb-range-slider.noUi-target { background: linear-gradient(to bottom, @grayLighter 0%, @grayLighter 100%); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: none; border-radius: 20px; - height: 10px; - border: none; + height: 8px; + border: 1px solid @inputBorder; + &:focus, &:focus-within { + border-color: @inputBorderFocus; + } +} +.umb-range-slider .noUi-connects { + cursor: pointer; + height: 20px; + top: -6px; +} +.umb-range-slider .noUi-tooltip { + padding: 2px 6px; } - .umb-range-slider .noUi-handle { + outline: none; + cursor: grab; border-radius: 100px; border: none; box-shadow: none; width: 20px !important; height: 20px !important; - background-color: @blueMid; + right: -10px !important; // half the handle width + background-color: @blueExtraDark; +} +.umb-range-slider .noUi-horizontal .noUi-handle { + top: -7px; } .umb-range-slider .noUi-handle::before { @@ -25,10 +41,6 @@ display: none; } -.umb-range-slider .noUi-handle { - right: -10px !important; // half the handle width -} - .umb-range-slider .noUi-marker-large.noUi-marker-horizontal { height: 10px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/mixins.less b/src/Umbraco.Web.UI.Client/src/less/mixins.less index 9739a90dae..b046ca69d9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/mixins.less +++ b/src/Umbraco.Web.UI.Client/src/less/mixins.less @@ -405,7 +405,7 @@ } } -.checkeredBackground(@backgroundColor: @gray-9, @fillColor: @black, @fillOpacity: 0.25) { +.checkeredBackground(@backgroundColor: @white, @fillColor: @black, @fillOpacity: 0.1) { background-image: url('data:image/svg+xml;charset=utf-8,\ \ \ diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index f5e652aa3d..328ba2229b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -463,9 +463,16 @@ .umb-cropper{ position: relative; + width: 100%; } -.umb-cropper img, .umb-cropper-gravity img{ +.umb-cropper .crop-container { + position: relative; + width: 100%; + padding-bottom: 9 / 16 * 100%; +} + +.umb-cropper img { position: relative; max-width: 100%; height: auto; @@ -477,75 +484,72 @@ max-width: none; } - .umb-cropper .overlay, .umb-cropper-gravity .overlay { - top: 0; - left: 0; + .umb-cropper .overlay { + position: absolute; + top: 0 !important; + bottom: 0; + left: 0 !important; + right: 0; cursor: move; z-index: @zindexCropperOverlay; - position: absolute; + border: 1px solid @inputBorder; + outline: none; + + &:focus { + border-color: @inputBorderFocus; + } } -.umb-cropper .viewport{ +.umb-cropper .viewport { + position: absolute; overflow: hidden; - position: relative; - margin: auto; - max-width: 100%; - height: auto; - } - -.umb-cropper-gravity .viewport{ - overflow: hidden; - position: relative; width: 100%; height: 100%; -} + .checkeredBackground(); + contain: strict; + > img { + position: absolute; + } + } -.umb-cropper .viewport:after { - content: ""; +.umb-cropper .viewport .__mask { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: @zindexCropperOverlay - 1; - opacity: .75; - box-shadow: inset 0 0 0 20px white,inset 0 0 0 21px rgba(0,0,0,.1),inset 0 0 20px 21px rgba(0,0,0,.2); + box-shadow: 0 0 0 2000px rgba(255, 255, 255, .8); +} +.umb-cropper .viewport .__mask-info { + position: absolute; + bottom: -20px; + height: 20px; + right: 0; + z-index: @zindexCropperOverlay - 1; + font-size: 12px; + opacity: 0.7; + padding: 0px 6px; } -.umb-cropper-gravity .overlay{ - width: 14px; - height: 14px; - text-align: center; - border-radius: 20px; - background: @pinkLight; - border: 3px solid @white; - opacity: 0.8; -} - -.umb-cropper-gravity .overlay i { - font-size: 26px; - line-height: 26px; - opacity: 0.8 !important; -} - -.umb-cropper .crop-container { - text-align: center; +.umb-cropper .crop-controls-wrapper { + display: flex; + height: 50px; + align-items: center; + background-color: #fff; + .btn:last-of-type { + margin-right: 10px; + } } .umb-cropper .crop-slider-wrapper { - padding: 10px; - border-top: 1px solid @gray-10; - margin-top: 10px; + flex: auto; display: flex; align-items: center; justify-content: center; flex-wrap: wrap; - @media (min-width: 769px) { - padding: 10px 50px 10px 50px; - } - i { color: @gray-3; flex: 0 0 25px; @@ -558,11 +562,20 @@ } .crop-slider { - padding: 50px 15px 40px 15px; - width: 66.6%; + width: calc(100% - 100px); } } +.umb-cropper .crop-controls-wrapper__icon-left { + margin-right: 10px; + +} +.umb-cropper .crop-controls-wrapper__icon-right { + margin-left: 10px; + font-size: 22px; +} + +/* .umb-cropper-gravity .viewport, .umb-cropper-gravity, .umb-cropper-imageholder { display: inline-block; max-width: 100%; @@ -572,30 +585,51 @@ float: left; } + .umb-cropper-imageholder umb-image-gravity { + display:block; + } + */ + + .umb-crop-thumbnail-container { + img { + max-width: unset; + } + } + .cropList { display: inline-block; position: relative; vertical-align: top; + flex:0; } - .gravity-container { - border: 1px solid @gray-8; + .umb-cropper-gravity .gravity-container { + border: 1px solid @inputBorder; + box-sizing: border-box; line-height: 0; + width: 100%; + height: 100%; + overflow: hidden; + .checkeredBackground(); + contain: content; + + &:focus, &:focus-within { + border-color: @inputBorderFocus; + } .viewport { - max-width: 600px; - .checkeredBackground(); + position: relative; + width: 100%; + height: 100%; + + display: flex; + justify-content: center; + align-items: center; img { display: block; - margin-left: auto; - margin-right: auto; - } - - img { - display: block; - margin-left: auto; - margin-right: auto; + max-width: 100%; + max-height: 100%; } &:hover { @@ -604,6 +638,62 @@ } } + + .umb-cropper-gravity img { + position: relative; + max-width: 100%; + height: auto; + top: 0; + left: 0; + } + + .umb-cropper-gravity .overlayViewport { + position: absolute; + top:0; + bottom:0; + left:0; + right:0; + contain: strict; + + display: flex; + justify-content: center; + align-items: center; + } + .umb-cropper-gravity .overlay { + position: relative; + display: block; + max-width: 100%; + max-height: 100%; + cursor: crosshair; + } + .umb-cropper-gravity .overlay .focalPoint { + position: absolute; + top: 0; + left: 0; + cursor: move; + z-index: @zindexCropperOverlay; + + width: 14px; + height: 14px; + // this element should have no width or height as its preventing the jQuery draggable-plugin to go all the way to the sides: + margin-left: -10px; + margin-top: -10px; + margin-right: -10px; + margin-bottom: -10px; + + text-align: center; + border-radius: 20px; + background: @pinkLight; + border: 3px solid @white; + opacity: 0.8; + } + + .umb-cropper-gravity .overlay .focalPoint i { + font-size: 26px; + line-height: 26px; + opacity: 0.8 !important; + } + .imagecropper { display: flex; align-items: flex-start; @@ -611,24 +701,13 @@ @media (max-width: 768px) { flex-direction: column; - float: left; - max-width: 100%; - } - - .viewport img { - .checkeredBackground(); } + } .imagecropper .umb-cropper__container { position: relative; - margin-bottom: 10px; - max-width: 100%; - border: 1px solid @gray-10; - - @media (min-width: 769px) { - width: 600px; - } + width: 100%; } .imagecropper .umb-cropper__container .button-drawer { diff --git a/src/Umbraco.Web.UI.Client/src/main.controller.js b/src/Umbraco.Web.UI.Client/src/main.controller.js index d21331f106..7d9431fcae 100644 --- a/src/Umbraco.Web.UI.Client/src/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/main.controller.js @@ -1,18 +1,18 @@ -/** +/** * @ngdoc controller - * @name Umbraco.MainController + * @name Umbraco.MainController * @function - * - * @description + * + * @description * The main application controller - * + * */ -function MainController($scope, $location, appState, treeService, notificationsService, - userService, historyService, updateChecker, navigationService, eventsService, +function MainController($scope, $location, appState, treeService, notificationsService, + userService, historyService, updateChecker, navigationService, eventsService, tmhDynamicLocale, localStorageService, editorService, overlayService, assetsService, tinyMceAssets) { - + //the null is important because we do an explicit bool check on this in the view - $scope.authenticated = null; + $scope.authenticated = null; $scope.touchDevice = appState.getGlobalState("touchDevice"); $scope.infiniteMode = false; $scope.overlay = {}; @@ -27,14 +27,14 @@ function MainController($scope, $location, appState, treeService, notificationsS assetsService.loadJs(tinyJsAsset, $scope); }); - // There are a number of ways to detect when a focus state should be shown when using the tab key and this seems to be the simplest solution. + // There are a number of ways to detect when a focus state should be shown when using the tab key and this seems to be the simplest solution. // For more information about this approach, see https://hackernoon.com/removing-that-ugly-focus-ring-and-keeping-it-too-6c8727fefcd2 function handleFirstTab(evt) { if (evt.keyCode === 9) { enableTabbingActive(); } } - + function enableTabbingActive() { $scope.tabbingActive = true; $scope.$digest(); @@ -185,7 +185,7 @@ function MainController($scope, $location, appState, treeService, notificationsS evts.push(eventsService.on("appState.overlay", function (name, args) { $scope.overlay = args; })); - + // events for tours evts.push(eventsService.on("appState.tour.start", function (name, args) { $scope.tour = args; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js new file mode 100644 index 0000000000..6c8a038536 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js @@ -0,0 +1,183 @@ +angular.module("umbraco") + .controller("Umbraco.Editors.MediaEntryEditorController", + function ($scope, localizationService, entityResource, editorService, overlayService, eventsService, mediaHelper) { + + var unsubscribe = []; + var vm = this; + + vm.loading = true; + vm.model = $scope.model; + vm.mediaEntry = vm.model.mediaEntry; + vm.currentCrop = null; + + localizationService.localizeMany([ + vm.model.createFlow ? "general_cancel" : "general_close", + vm.model.createFlow ? "general_create" : "buttons_submitChanges" + ]).then(function (data) { + vm.closeLabel = data[0]; + vm.submitLabel = data[1]; + }); + + vm.title = ""; + + function init() { + + updateMedia(); + + unsubscribe.push(eventsService.on("editors.media.saved", function(name, args) { + // if this media item uses the updated media type we want to reload the media file + if(args && args.media && args.media.key === vm.mediaEntry.mediaKey) { + updateMedia(); + } + })); + } + + function updateMedia() { + + vm.loading = true; + entityResource.getById(vm.mediaEntry.mediaKey, "Media").then(function (mediaEntity) { + vm.media = mediaEntity; + vm.imageSrc = mediaHelper.resolveFileFromEntity(mediaEntity, true); + vm.loading = false; + vm.hasDimensions = false; + vm.isCroppable = false; + + localizationService.localize("mediaPicker_editMediaEntryLabel", [vm.media.name, vm.model.documentName]).then(function (data) { + vm.title = data; + }); + }, function () { + localizationService.localize("mediaPicker_deletedItem").then(function (localized) { + vm.media = { + name: localized, + icon: "icon-picture", + trashed: true + }; + vm.loading = false; + vm.hasDimensions = false; + vm.isCroppable = false; + }); + }); + } + + vm.onImageLoaded = onImageLoaded; + function onImageLoaded(isCroppable, hasDimensions) { + vm.isCroppable = isCroppable; + vm.hasDimensions = hasDimensions; + }; + + + vm.repickMedia = repickMedia; + function repickMedia() { + vm.model.propertyEditor.changeMediaFor(vm.model.mediaEntry, onMediaReplaced); + } + + function onMediaReplaced() { + + // mark we have changes: + vm.imageCropperForm.$setDirty(); + + // un-select crop: + vm.currentCrop = null; + + // + updateMedia(); + } + + vm.openMedia = openMedia; + function openMedia() { + + var mediaEditor = { + id: vm.mediaEntry.mediaKey, + submit: function () { + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.mediaEditor(mediaEditor); + } + + + vm.focalPointChanged = function(left, top) { + //update the model focalpoint value + vm.mediaEntry.focalPoint = { + left: left, + top: top + }; + + //set form to dirty to track changes + setDirty(); + } + + + + vm.selectCrop = selectCrop; + function selectCrop(targetCrop) { + vm.currentCrop = targetCrop; + setDirty(); + // TODO: start watchin values of crop, first when changed set to dirty. + }; + + vm.deselectCrop = deselectCrop; + function deselectCrop() { + vm.currentCrop = null; + }; + + vm.resetCrop = resetCrop; + function resetCrop() { + if (vm.currentCrop) { + $scope.$evalAsync( () => { + vm.model.propertyEditor.resetCrop(vm.currentCrop); + vm.forceUpdateCrop = Math.random(); + }); + } + } + + function setDirty() { + vm.imageCropperForm.$setDirty(); + } + + + vm.submitAndClose = function () { + if (vm.model && vm.model.submit) { + vm.model.submit(vm.model); + } + } + + vm.close = function () { + if (vm.model && vm.model.close) { + if (vm.model.createFlow === true || vm.imageCropperForm.$dirty === true) { + var labels = vm.model.createFlow === true ? ["mediaPicker_confirmCancelMediaEntryCreationHeadline", "mediaPicker_confirmCancelMediaEntryCreationMessage"] : ["prompt_discardChanges", "mediaPicker_confirmCancelMediaEntryHasChanges"]; + localizationService.localizeMany(labels).then(function (localizations) { + const confirm = { + title: localizations[0], + view: "default", + content: localizations[1], + submitButtonLabelKey: "general_discard", + submitButtonStyle: "danger", + closeButtonLabelKey: "prompt_stay", + submit: function () { + overlayService.close(); + vm.model.close(vm.model); + }, + close: function () { + overlayService.close(); + } + }; + overlayService.open(confirm); + }); + } else { + vm.model.close(vm.model); + } + + } + } + + init(); + $scope.$on("$destroy", function () { + unsubscribe.forEach(x => x()); + }); + + } + ); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html new file mode 100644 index 0000000000..afa3451899 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html @@ -0,0 +1,118 @@ +
    + + + + + + + + +
    + +
    + This item is in the Recycle Bin +
    + +
    +
    + + + + +
    + +
    + +
    + + + + + +
    + +
    + + + + + + + +
    + + +
    +
    + +
    +
    + +
    + + + + + + + + + + + + + + + + +
    +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.less b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.less new file mode 100644 index 0000000000..1de962f7e1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.less @@ -0,0 +1,122 @@ +.umb-media-entry-editor { + + .umb-cropper-imageholder { + position: relative; + width: 100%; + height: 100%; + } + .umb-cropper-gravity { + height: 100%; + } + .umb-cropper__container { + width: 100%; + height: 100%; + } + .umb-cropper { + height: 100%; + } + .umb-cropper .crop-container { + padding-bottom: 0; + height: calc(100% - 50px) + } + .umb-cropper .crop-controls-wrapper { + justify-content: center; + } + .umb-cropper .crop-slider-wrapper { + max-width: 500px; + } +} + +.umb-media-entry-editor__pane { + display: flex; + flex-flow: row-reverse; + height: 100%; + width: 100%; +} + +.umb-media-entry-editor__crops { + background-color: white; + overflow: auto; + + > button { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: relative; + text-align: center; + padding: 4px 10px 0 10px; + border-bottom: 1px solid @gray-9; + box-sizing: border-box; + height: 120px; + width: 120px; + color: @ui-active-type; + + &:hover { + color: @ui-active-type-hover; + text-decoration: none; + } + + &:active { + .box-shadow(~"inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05)"); + } + + &::before { + content: ""; + position: absolute; + width: 0px; + max-height: 50px; + height: (100% - 16px); + top: auto; + bottom: auto; + background-color: @ui-light-active-border; + left: 0; + border-radius: 0 3px 3px 0; + opacity: 0; + transition: all .2s linear; + } + + &.--is-active { + color: @ui-light-active-type; + + &::before { + opacity: 1; + width: 4px; + } + } + &.--is-defined { + + } + + > .__icon { + font-size: 24px; + display: block; + text-align: center; + margin-bottom: 7px; + } + + > .__text { + font-size: 12px; + line-height: 1em; + margin-top: 4px; + } + } +} + +.umb-media-entry-editor__imagecropper { + flex: auto; + height: 100%; +} + +.umb-media-entry-editor__imageholder { + display: block; + position: relative; + height: calc(100% - 50px); +} +.umb-media-entry-editor__imageholder-actions { + background-color: white; + height: 50px; + display: flex; + justify-content: center; +} + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js index fec2e632c5..029dedf214 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js @@ -1,7 +1,7 @@ //used for the media picker dialog angular.module("umbraco") .controller("Umbraco.Editors.MediaPickerController", - function ($scope, $timeout, mediaResource, entityResource, userService, mediaHelper, mediaTypeHelper, eventsService, treeService, localStorageService, localizationService, editorService, umbSessionStorage, notificationsService) { + function ($scope, $timeout, mediaResource, entityResource, userService, mediaHelper, mediaTypeHelper, eventsService, treeService, localStorageService, localizationService, editorService, umbSessionStorage, notificationsService, clipboardService) { var vm = this; @@ -19,6 +19,8 @@ angular.module("umbraco") vm.enterSubmitFolder = enterSubmitFolder; vm.focalPointChanged = focalPointChanged; vm.changePagination = changePagination; + vm.onNavigationChanged = onNavigationChanged; + vm.clickClearClipboard = clickClearClipboard; vm.clickHandler = clickHandler; vm.clickItemName = clickItemName; @@ -27,7 +29,10 @@ angular.module("umbraco") vm.selectLayout = selectLayout; vm.showMediaList = false; + vm.navigation = []; + var dialogOptions = $scope.model; + vm.clipboardItems = dialogOptions.clipboardItems; $scope.disableFolderSelect = (dialogOptions.disableFolderSelect && dialogOptions.disableFolderSelect !== "0") ? true : false; $scope.disableFocalPoint = (dialogOptions.disableFocalPoint && dialogOptions.disableFocalPoint !== "0") ? true : false; @@ -100,10 +105,32 @@ angular.module("umbraco") function setTitle() { if (!$scope.model.title) { - localizationService.localize("defaultdialogs_selectMedia") + localizationService.localizeMany(["defaultdialogs_selectMedia", "mediaPicker_tabClipboard"]) .then(function (data) { - $scope.model.title = data; + $scope.model.title = data[0]; + + + vm.navigation = [{ + "alias": "empty", + "name": data[0], + "icon": "icon-umb-media", + "active": true, + "view": "" + }]; + + if(vm.clipboardItems) { + vm.navigation.push({ + "alias": "clipboard", + "name": data[1], + "icon": "icon-paste-in", + "view": "", + "disabled": vm.clipboardItems.length === 0 + }); + } + + vm.activeTab = vm.navigation[0]; }); + } } @@ -149,7 +176,7 @@ angular.module("umbraco") .then(function (node) { $scope.target = node; // Moving directly to existing node's folder - gotoFolder({ id: node.parentId }).then(function() { + gotoFolder({ id: node.parentId }).then(function () { selectMedia(node); $scope.target.url = mediaHelper.resolveFileFromEntity(node); $scope.target.thumbnail = mediaHelper.resolveFileFromEntity(node, true); @@ -169,10 +196,10 @@ angular.module("umbraco") function upload(v) { var fileSelect = $(".umb-file-dropzone .file-select"); - if (fileSelect.length === 0){ + if (fileSelect.length === 0) { localizationService.localize('media_uploadNotAllowed').then(function (message) { notificationsService.warning(message); }); } - else{ + else { fileSelect.trigger("click"); } } @@ -395,6 +422,19 @@ angular.module("umbraco") }); }; + function onNavigationChanged(tab) { + vm.activeTab.active = false; + vm.activeTab = tab; + vm.activeTab.active = true; + }; + + function clickClearClipboard() { + vm.onNavigationChanged(vm.navigation[0]); + vm.navigation[1].disabled = true; + vm.clipboardItems = []; + dialogOptions.clickClearClipboard(); + }; + var debounceSearchMedia = _.debounce(function () { $scope.$apply(function () { if (vm.searchOptions.filter) { @@ -504,13 +544,7 @@ angular.module("umbraco") var allowedTypes = dialogOptions.filter ? dialogOptions.filter.split(",") : null; for (var i = 0; i < data.length; i++) { - if (data[i].metaData.MediaPath !== null) { - data[i].thumbnail = mediaHelper.resolveFileFromEntity(data[i], true); - data[i].image = mediaHelper.resolveFileFromEntity(data[i], false); - } - if (data[i].metaData.UpdateDate !== null){ - data[i].updateDate = data[i].metaData.UpdateDate; - } + setDefaultData(data[i]); data[i].filtered = allowedTypes && allowedTypes.indexOf(data[i].metaData.ContentTypeAlias) < 0; } @@ -523,6 +557,16 @@ angular.module("umbraco") }); } + function setDefaultData(item) { + if (item.metaData.MediaPath !== null) { + item.thumbnail = mediaHelper.resolveFileFromEntity(item, true); + item.image = mediaHelper.resolveFileFromEntity(item, false); + } + if (item.metaData.UpdateDate !== null) { + item.updateDate = item.metaData.UpdateDate; + } + } + function preSelectMedia() { for (var folderIndex = 0; folderIndex < $scope.images.length; folderIndex++) { var folderImage = $scope.images[folderIndex]; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html index df0c8e3cef..d1f0699b13 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html @@ -3,13 +3,15 @@ - +
    @@ -19,21 +21,20 @@
    @@ -49,20 +50,20 @@
    -
    +
    + layouts="vm.layout.layouts" + active-layout="vm.layout.activeLayout" + on-layout-select="vm.selectLayout(layout)">
    + layouts="vm.layout.layouts" + active-layout="vm.layout.activeLayout" + on-layout-select="vm.selectLayout(layout)">
    @@ -86,31 +87,29 @@ + class="umb-breadcrumbs__add-ancestor" + ng-show="model.showFolderInput" + ng-model="model.newFolderName" + ng-keydown="enterSubmitFolder($event)" + ng-blur="vm.submitFolder()" + focus-when="{{model.showFolderInput}}" />
    - + - - +
    - @@ -145,11 +142,30 @@
    - - - + + + +
    + + +
    + +
    + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-crop.html b/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-crop.html index 933551bbff..af692f8322 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-crop.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-crop.html @@ -1,25 +1,36 @@
    -
    - -
    -
    +
    + +
    +
    {{width}}px x {{height}}px
    +
    +
    +
    -
    - +
    +
    + -
    - - +
    + + +
    + +
    - - +
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-gravity.html b/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-gravity.html index edd840a47f..10aa6a774a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-gravity.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-gravity.html @@ -1,12 +1,17 @@
    - +
    - + -
    +
    +
    + +
    +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card-grid.less b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card-grid.less new file mode 100644 index 0000000000..f7e5764335 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card-grid.less @@ -0,0 +1,137 @@ +.umb-media-card-grid { + /* Grid Setup */ + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + grid-auto-rows: minmax(100px, auto); + grid-gap: 10px; + + justify-items: center; + align-items: center; +} +.umb-media-card-grid__cell { + position: relative; + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; +} + +.umb-media-card-grid--inline-create-button { + position: absolute; + height: 100%; + z-index: 1; + opacity: 0; + outline: none; + left: 0; + width: 12px; + margin-left: -7px; + padding-left: 6px; + margin-right: -6px; + transition: opacity 240ms; + + &::before { + content: ''; + position: absolute; + background: @blueMid; + background: linear-gradient(0deg, rgba(@blueMid,0) 0%, rgba(@blueMid,1) 50%, rgba(@blueMid,0) 100%); + border-left: 1px solid white; + border-right: 1px solid white; + border-radius: 2px; + left: 0; + top: 0; + bottom: 0; + width: 2px; + animation: umb-media-card-grid--inline-create-button_before 400ms ease-in-out alternate infinite; + transform: scaleX(.99); + transition: transform 240ms ease-out; + + @keyframes umb-media-card-grid--inline-create-button_before { + 0% { opacity: 1; } + 100% { opacity: 0.5; } + } + } + + > .__plus { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + pointer-events: none; // lets stop avoiding the mouse values in JS move event. + box-sizing: border-box; + width: 28px; + height: 28px; + margin-left: -18px; + margin-top: -18px - 8px; + border-radius: 3em; + font-size: 14px; + border: 2px solid @blueMid; + color: @blueMid; + background-color: rgba(255, 255, 255, .96); + box-shadow: 0 0 0 2px rgba(255, 255, 255, .96); + transform: scale(0); + transition: transform 240ms ease-in; + + animation: umb-media-card-grid--inline-create-button__plus 400ms ease-in-out alternate infinite; + + @keyframes umb-media-card-grid--inline-create-button__plus { + 0% { color: rgba(@blueMid, 1); } + 100% { color: rgba(@blueMid, 0.8); } + } + + } + + &:focus { + > .__plus { + border-color: @ui-outline; + } + } + + &:hover, &:focus { + opacity: 1; + + &::before { + transform: scaleX(1); + } + > .__plus { + transform: scale(1); + transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); + + } + } +} + +.umb-media-card-grid__create-button { + position: relative; + width: 100%; + padding-bottom: 100%; + + border: 1px dashed @ui-action-discreet-border; + color: @ui-action-discreet-type; + font-weight: bold; + box-sizing: border-box; + border-radius: @baseBorderRadius; + + > div { + position: absolute; + height: 100%; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + } +} + +.umb-media-card-grid__create-button:hover { + color: @ui-action-discreet-type-hover; + border-color: @ui-action-discreet-border-hover; + text-decoration: none; +} + +.umb-media-card-grid__create-button.--disabled, +.umb-media-card-grid__create-button.--disabled:hover { + color: @gray-7; + border-color: @gray-7; + cursor: default; +} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.html b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.html new file mode 100644 index 0000000000..01ce31415e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.html @@ -0,0 +1,47 @@ + +
    + +
    + +

    + + +

    + +

    + + +

    + + + {{vm.media.name}} + + + + + + + + +
    + + + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.less b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.less new file mode 100644 index 0000000000..de3840b4d7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.less @@ -0,0 +1,186 @@ +.umb-media-card, +umb-media-card { + position: relative; + display: inline-block; + width: 100%; + //background-color: white; + border-radius: @baseBorderRadius; + //box-shadow: 0 1px 2px rgba(0,0,0,.2); + overflow: hidden; + + transition: box-shadow 120ms; + + cursor: pointer; + + .umb-outline(); + + &:hover { + box-shadow: 0 1px 3px rgba(@ui-action-type-hover, .5); + } + + &.--isOpen { + &::after { + content: ""; + position: absolute; + border: 2px solid @ui-active-border; + border-radius: @baseBorderRadius; + top:0; + bottom: 0; + left: 0; + right: 0; + } + } + + &.--hasError { + border: 2px solid @errorBackground; + } + + &.--sortable-placeholder { + &::after { + content: ""; + position: absolute; + background-color:rgba(@ui-drop-area-color, .05); + border: 2px solid rgba(@ui-drop-area-color, .1); + border-radius: @baseBorderRadius; + box-shadow: 0 0 4px rgba(@ui-drop-area-color, 0.05); + top:0; + bottom: 0; + left: 0; + right: 0; + animation: umb-block-card--sortable-placeholder 400ms ease-in-out alternate infinite; + @keyframes umb-block-card--sortable-placeholder { + 0% { opacity: 1; } + 100% { opacity: 0.5; } + } + } + box-shadow: none; + } + + .__status { + + position: absolute; + top: 0; + left: 0; + right: 0; + padding: 2px; + + &.--error { + background-color: @errorBackground; + color: @errorText; + } + } + + .__showcase { + position: relative; + max-width: 100%; + min-height: 120px; + max-height: 240px; + text-align: center; + //padding-bottom: 10/16*100%; + //background-color: @gray-12; + + img { + object-fit: contain; + max-height: 240px; + } + + umb-file-icon { + width: 100%; + padding-bottom: 100%; + display: block; + .umb-file-icon { + position: absolute; + top: 0; + bottom: 0; + left: 10px; + right: 10px; + display: flex; + align-items: center; + justify-content: center; + } + } + } + + .__info { + position: absolute; + text-align: left; + bottom: 0; + width: 100%; + background-color: #fff; + padding-top: 6px; + padding-bottom: 7px;// 7 + 1 to compentiate for the -1 substraction in margin-bottom. + + opacity: 0; + transition: opacity 120ms; + + &.--error { + opacity: 1; + background-color: @errorBackground; + .__name, .__subname { + color: @errorText; + } + } + + .__name { + font-weight: bold; + font-size: 13px; + color: @ui-action-type; + margin-left: 16px; + margin-bottom: -1px; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .__subname { + color: @gray-4; + font-size: 12px; + margin-left: 16px; + margin-top: 1px; + margin-bottom: -1px; + line-height: 1.5em; + } + } + + &:hover, &:focus, &:focus-within { + .__info { + opacity: 1; + } + .__info:not(.--error) { + .__name { + color: @ui-action-type-hover; + } + } + } + + .__actions { + position: absolute; + top: 10px; + right: 10px; + font-size: 0; + background-color: rgba(255, 255, 255, .96); + border-radius: 16px; + padding-left: 5px; + padding-right: 5px; + + opacity: 0; + transition: opacity 120ms; + .__action { + position: relative; + display: inline-block; + padding: 5px; + font-size: 18px; + + color: @ui-action-discreet-type; + &:hover { + color: @ui-action-discreet-type-hover; + } + } + } + &:hover, &:focus, &:focus-within { + .__actions { + opacity: 1; + } + } + +} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umbMediaCard.component.js b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umbMediaCard.component.js new file mode 100644 index 0000000000..24b20367aa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umbMediaCard.component.js @@ -0,0 +1,97 @@ +(function () { + "use strict"; + + angular + .module("umbraco") + .component("umbMediaCard", { + templateUrl: "views/components/mediacard/umb-media-card.html", + controller: MediaCardController, + controllerAs: "vm", + transclude: true, + bindings: { + mediaKey: " { + if(newValue !== oldValue) { + vm.updateThumbnail(); + } + })); + + function checkErrorState() { + + vm.notAllowed = (vm.media &&vm.allowedTypes && vm.allowedTypes.length > 0 && vm.allowedTypes.indexOf(vm.media.metaData.ContentTypeAlias) === -1); + + if ( + vm.hasError === true || vm.notAllowed === true || (vm.media && vm.media.trashed === true) + ) { + $element.addClass("--hasError") + vm.mediaCardForm.$setValidity('error', false) + } else { + $element.removeClass("--hasError") + vm.mediaCardForm.$setValidity('error', true) + } + } + + vm.$onInit = function () { + + unsubscribe.push($scope.$watchGroup(["vm.media.trashed", "vm.hasError"], checkErrorState)); + + vm.updateThumbnail(); + + unsubscribe.push(eventsService.on("editors.media.saved", function(name, args) { + // if this media item uses the updated media type we want to reload the media file + if(args && args.media && args.media.key === vm.mediaKey) { + vm.updateThumbnail(); + } + })); + } + + + vm.$onDestroy = function () { + unsubscribe.forEach(x => x()); + } + + vm.updateThumbnail = function () { + + if(vm.mediaKey && vm.mediaKey !== "") { + vm.loading = true; + + entityResource.getById(vm.mediaKey, "Media").then(function (mediaEntity) { + vm.media = mediaEntity; + checkErrorState(); + vm.thumbnail = mediaHelper.resolveFileFromEntity(mediaEntity, true); + + vm.loading = false; + }, function () { + localizationService.localize("mediaPicker_deletedItem").then(function (localized) { + vm.media = { + name: localized, + icon: "icon-picture", + trashed: true + }; + vm.loading = false; + $element.addClass("--hasError") + vm.mediaCardForm.$setValidity('error', false) + }); + }); + } + + } + + } + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html index f41390bce3..9754056267 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html @@ -6,39 +6,42 @@ ng-click="clickItem(item, $event, $index)" ng-repeat="item in items | filter:filterBy" ng-style="item.flexStyle" - ng-class="{'-selected': item.selected, '-file': !item.thumbnail, '-folder': item.isFolder, '-svg': item.extension == 'svg', '-selectable': item.selectable, '-unselectable': !item.selectable}"> -
    - -
    - -
    {{item.name}}
    -
    - - -
    - - - {{item.name}} - - - {{item.name}} - - - {{item.name}} - - - - + ng-class="{'-selected': item.selected, '-file': !item.thumbnail, '-folder': item.isFolder, '-svg': item.extension == 'svg', '-selectable': item.selectable, '-unselectable': !item.selectable, '-filtered': item.filtered}"> + +
    + +
    {{item.name}}
    + + +
    + + +
    + + + {{item.name}} + + + {{item.name}} + + + {{item.name}} + + + + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html index 41e24a6cda..fadc0ac3b1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html @@ -6,9 +6,9 @@

    Click to upload

    - +
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js index f41f22a1a9..88d112e2d6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js @@ -2,29 +2,29 @@ * @ngdoc controller * @name Umbraco.Editors.Media.EditController * @function - * + * * @description * The controller for the media editor */ -function mediaEditController($scope, $routeParams, $location, $http, $q, appState, mediaResource, - entityResource, navigationService, notificationsService, localizationService, - serverValidationManager, contentEditingHelper, fileManager, formHelper, +function mediaEditController($scope, $routeParams, $location, $http, $q, appState, mediaResource, + entityResource, navigationService, notificationsService, localizationService, + serverValidationManager, contentEditingHelper, fileManager, formHelper, editorState, umbRequestHelper, eventsService) { - + var evts = []; var nodeId = null; var create = false; var infiniteMode = $scope.model && $scope.model.infiniteMode; - // when opening the editor through infinite editing get the + // when opening the editor through infinite editing get the // node id from the model instead of the route param if(infiniteMode && $scope.model.id) { nodeId = $scope.model.id; } else { nodeId = $routeParams.id; } - - // when opening the editor through infinite editing get the + + // when opening the editor through infinite editing get the // create option from the model instead of the route param if(infiniteMode) { create = $scope.model.create; @@ -72,22 +72,22 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat } function init() { - + var content = $scope.content; - + // we need to check whether an app is present in the current data, if not we will present the default app. var isAppPresent = false; - + // on first init, we dont have any apps. but if we are re-initializing, we do, but ... if ($scope.app) { - + // lets check if it still exists as part of our apps array. (if not we have made a change to our docType, even just a re-save of the docType it will turn into new Apps.) content.apps.forEach(app => { if (app === $scope.app) { isAppPresent = true; } }); - + // if we did reload our DocType, but still have the same app we will try to find it by the alias. if (isAppPresent === false) { content.apps.forEach(app => { @@ -98,9 +98,9 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat } }); } - + } - + // if we still dont have a app, lets show the first one: if (isAppPresent === false) { content.apps[0].active = true; @@ -108,16 +108,16 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat } editorState.set($scope.content); - + bindEvents(); } - + function bindEvents() { //bindEvents can be called more than once and we don't want to have multiple bound events for (var e in evts) { eventsService.unsubscribe(evts[e]); } - + evts.push(eventsService.on("editors.mediaType.saved", function(name, args) { // if this media item uses the updated media type we need to reload the media item if(args && args.mediaType && args.mediaType.key === $scope.content.contentType.key) { @@ -131,7 +131,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat })); } $scope.page.submitButtonLabelKey = "buttons_save"; - + /** Syncs the content item to it's tree node - this occurs on first load and after saving */ function syncTreeNode(content, path, initialLoad) { @@ -149,7 +149,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat //it's a child item, just sync the ui node to the parent navigationService.syncTree({ tree: "media", path: path.substring(0, path.lastIndexOf(",")).split(","), forceReload: initialLoad !== true }); - //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node + //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node // from the server so that we can load in the actions menu. umbRequestHelper.resourcePromise( $http.get(content.treeNodeUrl), @@ -176,7 +176,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat $scope.save = function () { if (formHelper.submitForm({ scope: $scope })) { - + $scope.page.saveButtonState = "busy"; mediaResource.save($scope.content, create, fileManager.getFiles()) @@ -200,12 +200,16 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat editorState.set($scope.content); syncTreeNode($scope.content, data.path); - + $scope.page.saveButtonState = "success"; init(); } + eventsService.emit("editors.media.saved", {media: data}); + + return data; + }, function(err) { formHelper.resetForm({ scope: $scope, hasErrors: true }); @@ -213,16 +217,16 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat err: err, rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) }); - + editorState.set($scope.content); $scope.page.saveButtonState = "error"; }); } else { - showValidationNotification(); + showValidationNotification(); } - + }; function loadMedia() { @@ -231,7 +235,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat .then(function (data) { $scope.content = data; - + if (data.isChildOfListView && data.trashed === false) { $scope.page.listViewPath = ($routeParams.page) ? "/media/media/edit/" + data.parentId + "?page=" + $routeParams.page @@ -247,9 +251,9 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat serverValidationManager.notifyAndClearAllSubscriptions(); if(!infiniteMode) { - syncTreeNode($scope.content, data.path, true); + syncTreeNode($scope.content, data.path, true); } - + if ($scope.content.parentId && $scope.content.parentId !== -1 && $scope.content.parentId !== -21) { //We fetch all ancestors of the node to generate the footer breadcrump navigation entityResource.getAncestors(nodeId, "media") @@ -279,7 +283,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat $scope.appChanged = function (app) { $scope.app = app; - + // setup infinite mode if(infiniteMode) { $scope.page.submitButtonLabelKey = "buttons_saveAndClose"; @@ -296,7 +300,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat $location.path($scope.page.listViewPath.split("?")[0]); } }; - + //ensure to unregister from all events! $scope.$on('$destroy', function () { for (var e in evts) { diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/numberrange.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/numberrange.html index d9d8cad982..6e67c94793 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/numberrange.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/numberrange.html @@ -4,6 +4,7 @@ type="number" ng-model="model.value.min" placeholder="0" + min="0" ng-max="model.value.max" fix-number /> @@ -11,7 +12,7 @@ type="number" ng-model="model.value.max" placeholder="∞" - ng-min="model.value.min" + ng-min="model.value.min || 0" fix-number /> diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js index dcc9add395..d02e626bfa 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js @@ -99,6 +99,11 @@ function TreeSourceTypePickerController($scope, contentTypeResource, mediaTypeRe eventsService.unsubscribe(evts[e]); } }); + + if ($scope.model.config.itemType) { + currentItemType = $scope.model.config.itemType; + init(); + } } angular.module('umbraco').controller("Umbraco.PrevalueEditors.TreeSourceTypePickerController", TreeSourceTypePickerController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less index 019a772fdd..66ef23c744 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less @@ -10,7 +10,7 @@ .umb-block-list__wrapper { position: relative; - max-width: 1024px; + .umb-property-editor--limit-width(); > .ui-sortable > .ui-sortable-helper > .umb-block-list__block > .umb-block-list__block--content > * { box-shadow: 0px 5px 10px 0 rgba(0,0,0,.2); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js index c485f4bbc6..4f1016e680 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js @@ -11,11 +11,14 @@ * */ function fileUploadController($scope, fileManager) { - + $scope.fileChanged = onFileChanged; //declare a special method which will be called whenever the value has changed from the server $scope.model.onValueChanged = onValueChanged; + + $scope.fileExtensionsString = $scope.model.config.fileExtensions ? $scope.model.config.fileExtensions.map(x => "."+x.value).join(",") : ""; + /** * Called when the file selection value changes * @param {any} value @@ -38,12 +41,12 @@ files: [] }); } - + }; angular.module("umbraco") .controller('Umbraco.PropertyEditors.FileUploadController', fileUploadController) - .run(function (mediaHelper, umbRequestHelper, assetsService) { + .run(function (mediaHelper) { if (mediaHelper && mediaHelper.registerFileResolver) { //NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html index 522278e99e..36509e8947 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html @@ -4,6 +4,7 @@ property-alias="{{model.alias}}" value="model.value" required="model.validation.mandatory" - on-files-selected="fileChanged(value)"> + on-files-selected="fileChanged(value)" + accept-file-ext="fileExtensionsString">
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js index 4df8f7e596..e9d9950bdd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js @@ -1,6 +1,6 @@ angular.module('umbraco') .controller("Umbraco.PropertyEditors.ImageCropperController", - function ($scope, fileManager, $timeout) { + function ($scope, fileManager, $timeout, mediaHelper) { var config = Utilities.copy($scope.model.config); @@ -18,6 +18,8 @@ angular.module('umbraco') //declare a special method which will be called whenever the value has changed from the server $scope.model.onValueChanged = onValueChanged; + var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; + $scope.acceptFileExt = mediaHelper.formatFileTypes(umbracoSettings.imageFileTypes); /** * Called when the umgImageGravity component updates the focal point value * @param {any} left @@ -150,7 +152,7 @@ angular.module('umbraco') // we have a crop open already - close the crop (this will discard any changes made) close(); - // the crop editor needs a digest cycle to close down properly, otherwise its state + // the crop editor needs a digest cycle to close down properly, otherwise its state // is reused for the new crop... and that's really bad $timeout(function () { crop(targetCrop); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html index 241d61660e..9dc1a3b91a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html @@ -13,11 +13,12 @@ on-files-selected="filesSelected(value, files)" on-files-changed="filesChanged(files)" on-init="fileUploaderInit(value, files)" - hide-selection="true"> + hide-selection="true" + accept-file-ext="acceptFileExt">
    -
    +
    @@ -25,7 +26,6 @@ width="{{currentCrop.width}}" crop="currentCrop.coordinates" center="model.value.focalPoint" - max-size="450" src="imageSrc">
    @@ -49,7 +49,7 @@
    - +
    • diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js index ca46f30bb7..c6320a7cf2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js @@ -1,7 +1,7 @@ //this controller simply tells the dialogs service to open a mediaPicker window //with a specified callback, this callback will receive an object with a selection on it angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerController", - function ($scope, entityResource, mediaHelper, $timeout, userService, localizationService, editorService, overlayService) { + function ($scope, entityResource, mediaHelper, $timeout, userService, localizationService, editorService, overlayService, clipboardService) { var vm = this; @@ -10,6 +10,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl vm.add = add; vm.remove = remove; + vm.copyItem = copyItem; vm.editItem = editItem; vm.showAdd = showAdd; @@ -53,7 +54,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl // it's prone to someone "fixing" it at some point without knowing the effects. Rather use toString() // compares and be completely sure it works. var found = medias.find(m => m.udi.toString() === id.toString() || m.id.toString() === id.toString()); - + var mediaItem = found || { name: vm.labels.deletedItem, @@ -67,33 +68,36 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl return mediaItem; }); - medias.forEach(media => { - if (!media.extension && media.id && media.metaData) { - media.extension = mediaHelper.getFileExtension(media.metaData.MediaPath); - } - - // if there is no thumbnail, try getting one if the media is not a placeholder item - if (!media.thumbnail && media.id && media.metaData) { - media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); - } - - vm.mediaItems.push(media); - - if ($scope.model.config.idType === "udi") { - selectedIds.push(media.udi); - } else { - selectedIds.push(media.id); - } - }); + medias.forEach(media => appendMedia(media)); sync(); }); } } + function appendMedia(media) { + if (!media.extension && media.id && media.metaData) { + media.extension = mediaHelper.getFileExtension(media.metaData.MediaPath); + } + + // if there is no thumbnail, try getting one if the media is not a placeholder item + if (!media.thumbnail && media.id && media.metaData) { + media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); + } + + vm.mediaItems.push(media); + + if ($scope.model.config.idType === "udi") { + selectedIds.push(media.udi); + } else { + selectedIds.push(media.id); + } + } + function sync() { $scope.model.value = selectedIds.join(); removeAllEntriesAction.isDisabled = selectedIds.length === 0; + copyAllEntriesAction.isDisabled = removeAllEntriesAction.isDisabled; } function setDirty() { @@ -103,9 +107,9 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl } function reloadUpdatedMediaItems(updatedMediaNodes) { - // because the images can be edited through the media picker we need to + // because the images can be edited through the media picker we need to // reload. We only reload the images that is already picked but has been updated. - // We have to get the entities from the server because the media + // We have to get the entities from the server because the media // can be edited without being selected vm.mediaItems.forEach(media => { if (updatedMediaNodes.indexOf(media.udi) !== -1) { @@ -129,7 +133,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl ]; localizationService.localizeMany(labelKeys) - .then(function(data) { + .then(function (data) { vm.labels.deletedItem = data[0]; vm.labels.trashed = data[1]; @@ -143,7 +147,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl else { $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; $scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; - } + } } // only allow users to add and edit media if they have access to the media section @@ -163,6 +167,50 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl setDirty(); } + function copyAllEntries() { + if($scope.mediaItems.length > 0) { + + // gather aliases + var aliases = $scope.mediaItems.map(mediaEntity => mediaEntity.metaData.ContentTypeAlias); + + // remove duplicate aliases + aliases = aliases.filter((item, index) => aliases.indexOf(item) === index); + + var data = $scope.mediaItems.map(mediaEntity => { return {"mediaKey": mediaEntity.key }}); + + localizationService.localize("clipboard_labelForArrayOfItems", [$scope.model.label]).then(function(localizedLabel) { + clipboardService.copyArray(clipboardService.TYPES.MEDIA, aliases, data, localizedLabel, "icon-thumbnail-list", $scope.model.id); + }); + } + } + + function copyItem(mediaItem) { + + var mediaEntry = {}; + mediaEntry.mediaKey = mediaItem.key; + + clipboardService.copy(clipboardService.TYPES.MEDIA, mediaItem.metaData.ContentTypeAlias, mediaEntry, mediaItem.name, mediaItem.icon, mediaItem.udi); + } + + function pasteFromClipboard(pasteEntry, pasteType) { + + if (pasteEntry === undefined) { + return; + } + + pasteEntry = clipboardService.parseContentForPaste(pasteEntry, pasteType); + + entityResource.getById(pasteEntry.mediaKey, "Media").then(function (mediaEntity) { + + if(disableFolderSelect === true && mediaEntity.metaData.ContentTypeAlias === "Folder") { + return; + } + + appendMedia(mediaEntity); + sync(); + }); + } + function editItem(item) { var mediaEditor = { id: item.id, @@ -174,7 +222,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl if (model && model.mediaNode) { entityResource.getById(model.mediaNode.id, "Media") .then(function (mediaEntity) { - // if an image is selecting more than once + // if an image is selecting more than once // we need to update all the media items vm.mediaItems.forEach(media => { if (media.id === model.mediaNode.id) { @@ -200,6 +248,22 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl multiPicker: multiPicker, onlyImages: onlyImages, disableFolderSelect: disableFolderSelect, + clickPasteItem: function(item, mouseEvent) { + if (Array.isArray(item.data)) { + var indexIncrementor = 0; + item.data.forEach(function (entry) { + if (pasteFromClipboard(entry, item.type)) { + indexIncrementor++; + } + }); + } else { + pasteFromClipboard(item.data, item.type); + } + if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { + editorService.close(); + } + setDirty(); + }, submit: function (model) { editorService.close(); @@ -231,6 +295,21 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl } } + + var allowedTypes = null; + if(onlyImages) { + allowedTypes = ["Image"]; // Media Type Image Alias. + } + + mediaPicker.clickClearClipboard = function ($event) { + clipboardService.clearEntriesOfType(clipboardService.TYPES.Media, allowedTypes); + }; + + mediaPicker.clipboardItems = clipboardService.retriveEntriesOfType(clipboardService.TYPES.MEDIA, allowedTypes); + mediaPicker.clipboardItems.sort( (a, b) => { + return b.date - a.date + }); + editorService.mediaPicker(mediaPicker); } @@ -262,6 +341,14 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl }); } + var copyAllEntriesAction = { + labelKey: 'clipboard_labelForCopyAllEntries', + labelTokens: ['Media'], + icon: "documents", + method: copyAllEntries, + isDisabled: true + } + var removeAllEntriesAction = { labelKey: 'clipboard_labelForRemoveAllEntries', labelTokens: [], @@ -269,9 +356,10 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl method: removeAllEntries, isDisabled: true }; - + if (multiPicker === true) { var propertyActions = [ + copyAllEntriesAction, removeAllEntriesAction ]; @@ -289,12 +377,12 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl cancel: ".unsortable", update: function () { setDirty(); - $timeout(function() { + $timeout(function () { // TODO: Instead of doing this with a timeout would be better to use a watch like we do in the // content picker. Then we don't have to worry about setting ids, render models, models, we just set one and let the // watch do all the rest. selectedIds = vm.mediaItems.map(media => $scope.model.config.idType === "udi" ? media.udi : media.id); - + sync(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/mediapicker3.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/mediapicker3.html new file mode 100644 index 0000000000..5e67aafe3e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/mediapicker3.html @@ -0,0 +1 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.controller.js new file mode 100644 index 0000000000..922370a032 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.controller.js @@ -0,0 +1,110 @@ +angular.module("umbraco").controller("Umbraco.PropertyEditors.MediaPicker3.CropConfigurationController", + function ($scope) { + + var unsubscribe = []; + + if (!$scope.model.value) { + $scope.model.value = []; + } + + $scope.setFocus = false; + + $scope.remove = function (crop, evt) { + evt.preventDefault(); + const i = $scope.model.value.indexOf(crop); + if (i > -1) { + $scope.model.value.splice(i, 1); + } + }; + + $scope.edit = function (crop, evt) { + evt.preventDefault(); + crop.editMode = true; + }; + + $scope.addNewCrop = function (evt) { + evt.preventDefault(); + + var crop = {}; + crop.editMode = true; + + $scope.model.value.push(crop); + $scope.validate(crop); + } + $scope.setChanges = function (crop) { + $scope.validate(crop); + if( + crop.hasWidthError !== true && + crop.hasHeightError !== true && + crop.hasAliasError !== true + ) { + crop.editMode = false; + window.dispatchEvent(new Event('resize.umbImageGravity')); + } + }; + $scope.useForAlias = function (crop) { + if (crop.alias == null || crop.alias === "") { + crop.alias = (crop.label || "").toCamelCase(); + } + }; + $scope.validate = function(crop) { + $scope.validateWidth(crop); + $scope.validateHeight(crop); + $scope.validateAlias(crop); + } + $scope.validateWidth = function (crop) { + crop.hasWidthError = !(Utilities.isNumber(crop.width) && crop.width > 0); + }; + $scope.validateHeight = function (crop) { + crop.hasHeightError = !(Utilities.isNumber(crop.height) && crop.height > 0); + }; + $scope.validateAlias = function (crop, $event) { + var exists = $scope.model.value.find( x => crop !== x && crop.alias === x.alias); + if (exists !== undefined || crop.alias === "") { + // alias is not valid + crop.hasAliasError = true; + } else { + // everything was good: + crop.hasAliasError = false; + } + + }; + + $scope.confirmChanges = function (crop, event) { + if (event.keyCode == 13) { + $scope.setChanges(crop, event); + event.preventDefault(); + } + }; + $scope.focusNextField = function (event) { + if (event.keyCode == 13) { + + var el = event.target; + + var inputs = Array.from(document.querySelectorAll("input:not(disabled)")); + var inputIndex = inputs.indexOf(el); + if (inputIndex > -1) { + var nextIndex = inputs.indexOf(el) +1; + + if(inputs.length > nextIndex) { + inputs[nextIndex].focus(); + event.preventDefault(); + } + } + } + }; + + $scope.sortableOptions = { + axis: 'y', + containment: 'parent', + cursor: 'move', + tolerance: 'pointer' + }; + + $scope.$on("$destroy", function () { + for (const subscription of unsubscribe) { + subscription(); + } + }); + + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.html new file mode 100644 index 0000000000..46b9ddb15f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.html @@ -0,0 +1,96 @@ +
      + +
      +
      +
      +
      + Label +
      +
      + Alias +
      +
      + Width +
      +
      + Height +
      +
      + Actions +
      +
      +
      +
      + +
      + +
      {{crop.label}}
      +
      {{crop.alias}}
      +
      {{crop.width}}px
      +
      {{crop.height}}px
      +
      + + +
      + + +
      + +
      + +
      + +
      + +
      + +
      + +
      + +
      +
      + +
      + +
      +
      +
      + + + +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.less new file mode 100644 index 0000000000..5f5a2d4689 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.less @@ -0,0 +1,40 @@ +.umb-mediapicker3-crops { + + input.ng-invalid.ng-touched { + border-color:@formErrorBorder; + color:@formErrorBorder + } + + .umb-table button { + position: relative; + color: @ui-action-discreet-type; + margin-right: 10px; + font-size: 14px; + &:hover { + color: @ui-action-discreet-type-hover; + } + } + +} + +.umb-mediapicker3-crops__add { + + margin-top:10px; + + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: 1px dashed @ui-action-discreet-border; + color: @ui-action-discreet-type; + font-weight: bold; + padding: 5px 15px; + box-sizing: border-box; + width: 100%; +} + +.umb-mediapicker3-crops__add:hover { + color: @ui-action-discreet-type-hover; + border-color: @ui-action-discreet-border-hover; + text-decoration: none; +} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html new file mode 100644 index 0000000000..aa9f50b7df --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html @@ -0,0 +1,71 @@ +
      + + + +
      + +
      + +
      + + + + +
      + + +
      +
      + +
      +
      + + + +
      + + + + +
      +
      + Minimum %0% entries, needs %1% more. +
      + > +
      +
      +
      + Maximum %0% entries, %1% too many. +
      + +
      + +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.less new file mode 100644 index 0000000000..d02c0b055c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.less @@ -0,0 +1,13 @@ +.umb-mediapicker3 { + + .umb-media-card-grid { + padding: 20px; + border: 1px solid @inputBorder; + box-sizing: border-box; + .umb-property-editor--limit-width(); + + &.--singleMode { + max-width: 202px; + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js new file mode 100644 index 0000000000..675381d46e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js @@ -0,0 +1,431 @@ +(function () { + "use strict"; + + + /** + * @ngdoc directive + * @name umbraco.directives.directive:umbMediaPicker3PropertyEditor + * @function + * + * @description + * The component for the Media Picker property editor. + */ + angular + .module("umbraco") + .component("umbMediaPicker3PropertyEditor", { + templateUrl: "views/propertyeditors/MediaPicker3/umb-media-picker3-property-editor.html", + controller: MediaPicker3Controller, + controllerAs: "vm", + bindings: { + model: "=" + }, + require: { + propertyForm: "^form", + umbProperty: "?^umbProperty", + umbVariantContent: '?^^umbVariantContent', + umbVariantContentEditors: '?^^umbVariantContentEditors', + umbElementEditorContent: '?^^umbElementEditorContent' + } + }); + + function MediaPicker3Controller($scope, editorService, clipboardService, localizationService, overlayService, userService, entityResource) { + + var unsubscribe = []; + + // Property actions: + var copyAllMediasAction = null; + var removeAllMediasAction = null; + + var vm = this; + + vm.loading = true; + + vm.supportCopy = clipboardService.isSupported(); + + + vm.labels = {}; + + localizationService.localizeMany(["grid_addElement", "content_createEmpty"]).then(function (data) { + vm.labels.grid_addElement = data[0]; + vm.labels.content_createEmpty = data[1]; + }); + + vm.$onInit = function() { + + vm.validationLimit = vm.model.config.validationLimit || {}; + // If single-mode we only allow 1 item as the maximum: + if(vm.model.config.multiple === false) { + vm.validationLimit.max = 1; + } + vm.model.config.crops = vm.model.config.crops || []; + vm.singleMode = vm.validationLimit.max === 1; + vm.allowedTypes = vm.model.config.filter ? vm.model.config.filter.split(",") : null; + + copyAllMediasAction = { + labelKey: "clipboard_labelForCopyAllEntries", + labelTokens: [vm.model.label], + icon: "documents", + method: requestCopyAllMedias, + isDisabled: true + }; + + removeAllMediasAction = { + labelKey: 'clipboard_labelForRemoveAllEntries', + labelTokens: [], + icon: 'trash', + method: requestRemoveAllMedia, + isDisabled: true + }; + + var propertyActions = []; + if(vm.supportCopy) { + propertyActions.push(copyAllMediasAction); + } + propertyActions.push(removeAllMediasAction); + + if (vm.umbProperty) { + vm.umbProperty.setPropertyActions(propertyActions); + } + + if(vm.model.value === null || !Array.isArray(vm.model.value)) { + vm.model.value = []; + } + + vm.model.value.forEach(mediaEntry => updateMediaEntryData(mediaEntry)); + + userService.getCurrentUser().then(function (userData) { + + if (!vm.model.config.startNodeId) { + if (vm.model.config.ignoreUserStartNodes === true) { + vm.model.config.startNodeId = -1; + vm.model.config.startNodeIsVirtual = true; + } else { + vm.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + vm.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; + } + } + + // only allow users to add and edit media if they have access to the media section + var hasAccessToMedia = userData.allowedSections.indexOf("media") !== -1; + vm.allowEdit = hasAccessToMedia; + vm.allowAdd = hasAccessToMedia; + + vm.loading = false; + }); + + }; + + function setDirty() { + if (vm.propertyForm) { + vm.propertyForm.$setDirty(); + } + } + + vm.addMediaAt = addMediaAt; + function addMediaAt(createIndex, $event) { + var mediaPicker = { + startNodeId: vm.model.config.startNodeId, + startNodeIsVirtual: vm.model.config.startNodeIsVirtual, + dataTypeKey: vm.model.dataTypeKey, + multiPicker: vm.singleMode !== true, + clickPasteItem: function(item, mouseEvent) { + + if (Array.isArray(item.data)) { + var indexIncrementor = 0; + item.data.forEach(function (entry) { + if (requestPasteFromClipboard(createIndex + indexIncrementor, entry, item.type)) { + indexIncrementor++; + } + }); + } else { + requestPasteFromClipboard(createIndex, item.data, item.type); + } + if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { + mediaPicker.close(); + } + }, + submit: function (model) { + editorService.close(); + + var indexIncrementor = 0; + model.selection.forEach((entry) => { + var mediaEntry = {}; + mediaEntry.key = String.CreateGuid(); + mediaEntry.mediaKey = entry.key; + updateMediaEntryData(mediaEntry); + vm.model.value.splice(createIndex + indexIncrementor, 0, mediaEntry); + indexIncrementor++; + }); + + setDirty(); + }, + close: function () { + editorService.close(); + } + } + + if(vm.model.config.filter) { + mediaPicker.filter = vm.model.config.filter; + } + + mediaPicker.clickClearClipboard = function ($event) { + clipboardService.clearEntriesOfType(clipboardService.TYPES.Media, vm.allowedTypes || null); + }; + + mediaPicker.clipboardItems = clipboardService.retriveEntriesOfType(clipboardService.TYPES.MEDIA, vm.allowedTypes || null); + mediaPicker.clipboardItems.sort( (a, b) => { + return b.date - a.date + }); + + editorService.mediaPicker(mediaPicker); + } + + // To be used by infinite editor. (defined here cause we need configuration from property editor) + function changeMediaFor(mediaEntry, onSuccess) { + var mediaPicker = { + startNodeId: vm.model.config.startNodeId, + startNodeIsVirtual: vm.model.config.startNodeIsVirtual, + dataTypeKey: vm.model.dataTypeKey, + multiPicker: false, + submit: function (model) { + editorService.close(); + + model.selection.forEach((entry) => {// only one. + mediaEntry.mediaKey = entry.key; + }); + + // reset focal and crops: + mediaEntry.crops = null; + mediaEntry.focalPoint = null; + updateMediaEntryData(mediaEntry); + + if(onSuccess) { + onSuccess(); + } + }, + close: function () { + editorService.close(); + } + } + + if(vm.model.config.filter) { + mediaPicker.filter = vm.model.config.filter; + } + + editorService.mediaPicker(mediaPicker); + } + + function resetCrop(cropEntry) { + Object.assign(cropEntry, vm.model.config.crops.find( c => c.alias === cropEntry.alias)); + cropEntry.coordinates = null; + setDirty(); + } + + function updateMediaEntryData(mediaEntry) { + + mediaEntry.crops = mediaEntry.crops || []; + mediaEntry.focalPoint = mediaEntry.focalPoint || { + left: 0.5, + top: 0.5 + }; + + // Copy config and only transfer coordinates. + var newCrops = Utilities.copy(vm.model.config.crops); + newCrops.forEach(crop => { + var oldCrop = mediaEntry.crops.filter(x => x.alias === crop.alias).shift(); + if (oldCrop && oldCrop.height === crop.height && oldCrop.width === crop.width) { + crop.coordinates = oldCrop.coordinates; + } + }); + mediaEntry.crops = newCrops; + + } + + vm.removeMedia = removeMedia; + function removeMedia(media) { + var index = vm.model.value.indexOf(media); + if(index !== -1) { + vm.model.value.splice(index, 1); + } + } + function deleteAllMedias() { + vm.model.value = []; + } + + vm.activeMediaEntry = null; + function setActiveMedia(mediaEntryOrNull) { + vm.activeMediaEntry = mediaEntryOrNull; + } + + vm.editMedia = editMedia; + function editMedia(mediaEntry, options, $event) { + + if($event) + $event.stopPropagation(); + + options = options || {}; + + setActiveMedia(mediaEntry); + + var documentInfo = getDocumentNameAndIcon(); + + // make a clone to avoid editing model directly. + var mediaEntryClone = Utilities.copy(mediaEntry); + + var mediaEditorModel = { + $parentScope: $scope, // pass in a $parentScope, this maintains the scope inheritance in infinite editing + $parentForm: vm.propertyForm, // pass in a $parentForm, this maintains the FormController hierarchy with the infinite editing view (if it contains a form) + createFlow: options.createFlow === true, + documentName: documentInfo.name, + mediaEntry: mediaEntryClone, + propertyEditor: { + changeMediaFor: changeMediaFor, + resetCrop: resetCrop + }, + enableFocalPointSetter: vm.model.config.enableLocalFocalPoint || false, + view: "views/common/infiniteeditors/mediaEntryEditor/mediaEntryEditor.html", + size: "large", + submit: function(model) { + vm.model.value[vm.model.value.indexOf(mediaEntry)] = mediaEntryClone; + setActiveMedia(null) + editorService.close(); + }, + close: function(model) { + if(model.createFlow === true) { + // This means that the user cancelled the creation and we should remove the media item. + // TODO: remove new media item. + } + setActiveMedia(null) + editorService.close(); + } + }; + + // open property settings editor + editorService.open(mediaEditorModel); + } + + var getDocumentNameAndIcon = function() { + // get node name + var contentNodeName = "?"; + var contentNodeIcon = null; + if(vm.umbVariantContent) { + contentNodeName = vm.umbVariantContent.editor.content.name; + if(vm.umbVariantContentEditors) { + contentNodeIcon = vm.umbVariantContentEditors.content.icon.split(" ")[0]; + } else if (vm.umbElementEditorContent) { + contentNodeIcon = vm.umbElementEditorContent.model.documentType.icon.split(" ")[0]; + } + } else if (vm.umbElementEditorContent) { + contentNodeName = vm.umbElementEditorContent.model.documentType.name; + contentNodeIcon = vm.umbElementEditorContent.model.documentType.icon.split(" ")[0]; + } + + return { + name: contentNodeName, + icon: contentNodeIcon + } + } + + var requestCopyAllMedias = function() { + var mediaKeys = vm.model.value.map(x => x.mediaKey) + entityResource.getByIds(mediaKeys, "Media").then(function (entities) { + + // gather aliases + var aliases = entities.map(mediaEntity => mediaEntity.metaData.ContentTypeAlias); + + // remove duplicate aliases + aliases = aliases.filter((item, index) => aliases.indexOf(item) === index); + + var documentInfo = getDocumentNameAndIcon(); + + localizationService.localize("clipboard_labelForArrayOfItemsFrom", [vm.model.label, documentInfo.name]).then(function(localizedLabel) { + clipboardService.copyArray(clipboardService.TYPES.MEDIA, aliases, vm.model.value, localizedLabel, documentInfo.icon || "icon-thumbnail-list", vm.model.id); + }); + }); + } + + vm.copyMedia = copyMedia; + function copyMedia(mediaEntry) { + entityResource.getById(mediaEntry.mediaKey, "Media").then(function (mediaEntity) { + clipboardService.copy(clipboardService.TYPES.MEDIA, mediaEntity.metaData.ContentTypeAlias, mediaEntry, mediaEntity.name, mediaEntity.icon, mediaEntry.key); + }); + } + function requestPasteFromClipboard(createIndex, pasteEntry, pasteType) { + + if (pasteEntry === undefined) { + return false; + } + + pasteEntry = clipboardService.parseContentForPaste(pasteEntry, pasteType); + + pasteEntry.key = String.CreateGuid(); + updateMediaEntryData(pasteEntry); + vm.model.value.splice(createIndex, 0, pasteEntry); + + + return true; + + } + + function requestRemoveAllMedia() { + localizationService.localizeMany(["mediaPicker_confirmRemoveAllMediaEntryMessage", "general_remove"]).then(function (data) { + overlayService.confirmDelete({ + title: data[1], + content: data[0], + close: function () { + overlayService.close(); + }, + submit: function () { + deleteAllMedias(); + overlayService.close(); + } + }); + }); + } + + + vm.sortableOptions = { + cursor: "grabbing", + handle: "umb-media-card", + cancel: "input,textarea,select,option", + classes: ".umb-media-card--dragging", + distance: 5, + tolerance: "pointer", + scroll: true, + update: function (ev, ui) { + setDirty(); + } + }; + + + function onAmountOfMediaChanged() { + + // enable/disable property actions + if (copyAllMediasAction) { + copyAllMediasAction.isDisabled = vm.model.value.length === 0; + } + if (removeAllMediasAction) { + removeAllMediasAction.isDisabled = vm.model.value.length === 0; + } + + // validate limits: + if (vm.propertyForm && vm.validationLimit) { + + var isMinRequirementGood = vm.validationLimit.min === null || vm.model.value.length >= vm.validationLimit.min; + vm.propertyForm.minCount.$setValidity("minCount", isMinRequirementGood); + + var isMaxRequirementGood = vm.validationLimit.max === null || vm.model.value.length <= vm.validationLimit.max; + vm.propertyForm.maxCount.$setValidity("maxCount", isMaxRequirementGood); + } + } + + unsubscribe.push($scope.$watch(() => vm.model.value.length, onAmountOfMediaChanged)); + + $scope.$on("$destroy", function () { + for (const subscription of unsubscribe) { + subscription(); + } + }); + } + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.createButton.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.createButton.controller.js new file mode 100644 index 0000000000..b561784d9f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.createButton.controller.js @@ -0,0 +1,18 @@ +(function () { + "use strict"; + + angular + .module("umbraco") + .controller("Umbraco.PropertyEditors.MediaPicker3PropertyEditor.CreateButtonController", + function Controller($scope) { + + var vm = this; + vm.plusPosY = 0; + + vm.onMouseMove = function($event) { + vm.plusPosY = $event.offsetY; + } + + }); + +})(); diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 737181c668..4abcdf8a40 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1100,6 +1100,17 @@ Mange hilsner fra Umbraco robotten Du har valgt et medie som er slettet eller lagt i papirkurven Du har valgt medier som er slettede eller lagt i papirkurven Slettet + Åben i mediebiblioteket + Skift medie + Nulstil medie beskæring + Rediger %0% på %1% + Annuller indsættelse? + + Du har foretaget ændringer til bruge af dette media. Er du sikker på at du vil annullere? + Fjern? + Fjern brugen af alle medier? + Udklipsholder + Ikke tilladt indtast eksternt link @@ -1845,6 +1856,7 @@ Mange hilsner fra Umbraco robotten Kopier %0% %0% fra %1% + Samling af %0% Fjern alle elementer Ryd udklipsholder diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 3f6c985a0f..cbb6902d74 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1353,6 +1353,17 @@ To manage your website, simply open the Umbraco backoffice and start adding cont You have picked a media item currently deleted or in the recycle bin You have picked media items currently deleted or in the recycle bin Trashed + Open in Media Library + Change Media Item + Reset media crop + Edit %0% on %1% + Discard creation? + + You have made changes to this content. Are you sure you want to discard them? + Remove? + Remove all medias? + Clipboard + Not allowed enter external link @@ -2377,6 +2388,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Copy %0% %0% from %1% + Collection of %0% Remove all items Clear clipboard diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 87b58e5063..590a248393 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1363,6 +1363,17 @@ To manage your website, simply open the Umbraco backoffice and start adding cont You have picked a media item currently deleted or in the recycle bin You have picked media items currently deleted or in the recycle bin Trashed + Open in Media Library + Change Media Item + Reset media crop + Edit %0% on %1% + Discard creation? + + You have made changes to this content. Are you sure you want to discard them? + Remove? + Remove all medias? + Clipboard + Not allowed enter external link @@ -2396,6 +2407,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Copy %0% %0% from %1% + Collection of %0% Remove all items Clear clipboard diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 8d13ccd4d7..7160a87351 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -704,7 +704,32 @@ namespace Umbraco.Web.Editors if (result.FormData["contentTypeAlias"] == Constants.Conventions.MediaTypes.AutoSelect) { - if (Current.Configs.Settings().Content.ImageFileTypes.Contains(ext)) + var mediaTypes = Services.MediaTypeService.GetAll(); + // Look up MediaTypes + foreach (var mediaTypeItem in mediaTypes) + { + var fileProperty = mediaTypeItem.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == "umbracoFile"); + if (fileProperty != null) { + var dataTypeKey = fileProperty.DataTypeKey; + var dataType = Services.DataTypeService.GetDataType(dataTypeKey); + + if (dataType != null && dataType.Configuration is IFileExtensionsConfig fileExtensionsConfig) { + var fileExtensions = fileExtensionsConfig.FileExtensions; + if (fileExtensions != null) + { + if (fileExtensions.Where(x => x.Value == ext).Count() != 0) + { + mediaType = mediaTypeItem.Alias; + break; + } + } + } + } + + } + + // If media type is still File then let's check if it's an image. + if (mediaType == Constants.Conventions.MediaTypes.File && Current.Configs.Settings().Content.ImageFileTypes.Contains(ext)) { mediaType = Constants.Conventions.MediaTypes.Image; } diff --git a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs index f39b267e18..766cb1e99f 100644 --- a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs @@ -28,6 +28,11 @@ namespace Umbraco.Web return mediaItem.GetCropUrl(imageUrlGenerator, cropAlias: cropAlias, useCropDimensions: true); } + public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias, IImageUrlGenerator imageUrlGenerator, ImageCropperValue imageCropperValue) + { + return mediaItem.Url().GetCropUrl(imageUrlGenerator, imageCropperValue, cropAlias: cropAlias, useCropDimensions: true); + } + /// /// Gets the ImageProcessor URL by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. /// @@ -375,5 +380,11 @@ namespace Umbraco.Web return imageUrlGenerator.GetImageUrl(options); } + + public static string GetLocalCropUrl(this MediaWithCrops mediaWithCrops, string alias, IImageUrlGenerator imageUrlGenerator, string cacheBusterValue) + { + return mediaWithCrops.LocalCrops.Src + mediaWithCrops.LocalCrops.GetCropUrl(alias, imageUrlGenerator, cacheBusterValue: cacheBusterValue); + + } } } diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs index dad2f9e3f3..51845946f1 100644 --- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -1,7 +1,5 @@ using System; -using Newtonsoft.Json.Linq; using System.Globalization; -using System.Text; using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Composing; @@ -32,6 +30,8 @@ namespace Umbraco.Web /// public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, cropAlias, Current.ImageUrlGenerator); + public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias, ImageCropperValue imageCropperValue) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, cropAlias, Current.ImageUrlGenerator, imageCropperValue); + /// /// Gets the ImageProcessor URL by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. /// @@ -118,6 +118,13 @@ namespace Umbraco.Web ImageCropRatioMode? ratioMode = null, bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, Current.ImageUrlGenerator, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale); + public static string GetLocalCropUrl(this MediaWithCrops mediaWithCrops, + string alias, + string cacheBusterValue = null) + => ImageCropperTemplateCoreExtensions.GetLocalCropUrl(mediaWithCrops, alias, Current.ImageUrlGenerator, cacheBusterValue); + + + /// /// Gets the ImageProcessor URL from the image path. /// diff --git a/src/Umbraco.Web/PropertyEditors/FileExtensionConfigItem.cs b/src/Umbraco.Web/PropertyEditors/FileExtensionConfigItem.cs new file mode 100644 index 0000000000..859b3b35eb --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/FileExtensionConfigItem.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Umbraco.Web.PropertyEditors +{ + public class FileExtensionConfigItem : IFileExtensionConfigItem + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("value")] + public string Value { get; set; } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadConfiguration.cs b/src/Umbraco.Web/PropertyEditors/FileUploadConfiguration.cs new file mode 100644 index 0000000000..55f947797a --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/FileUploadConfiguration.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Represents the configuration for the file upload address value editor. + /// + public class FileUploadConfiguration : IFileExtensionsConfig + { + [ConfigurationField("fileExtensions", "Accepted file extensions", "multivalues")] + public List FileExtensions { get; set; } = new List(); + } +} diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadConfigurationEditor.cs new file mode 100644 index 0000000000..abbd19a793 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/FileUploadConfigurationEditor.cs @@ -0,0 +1,12 @@ +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Represents the configuration editor for the file upload value editor. + /// + public class FileUploadConfigurationEditor : ConfigurationEditor + { + + } +} diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs index 052af18aa1..a105d490be 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs @@ -32,6 +32,10 @@ namespace Umbraco.Web.PropertyEditors _uploadAutoFillProperties = new UploadAutoFillProperties(_mediaFileSystem, logger, contentSection); } + + /// + protected override IConfigurationEditor CreateConfigurationEditor() => new FileUploadConfigurationEditor(); + /// /// Creates the corresponding property value editor. /// diff --git a/src/Umbraco.Web/PropertyEditors/IFileExtensionConfig.cs b/src/Umbraco.Web/PropertyEditors/IFileExtensionConfig.cs new file mode 100644 index 0000000000..c4934540c7 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/IFileExtensionConfig.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Umbraco.Web.PropertyEditors; + +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Marker interface for any editor configuration that supports defining file extensions + /// + public interface IFileExtensionsConfig + { + List FileExtensions { get; set; } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/IFileExtensionConfigItem.cs b/src/Umbraco.Web/PropertyEditors/IFileExtensionConfigItem.cs new file mode 100644 index 0000000000..682e881565 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/IFileExtensionConfigItem.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; + +namespace Umbraco.Web.PropertyEditors +{ + public interface IFileExtensionConfigItem + { + int Id { get; set; } + + string Value { get; set; } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/MediaPicker3Configuration.cs b/src/Umbraco.Web/PropertyEditors/MediaPicker3Configuration.cs new file mode 100644 index 0000000000..4c3c6564a5 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/MediaPicker3Configuration.cs @@ -0,0 +1,60 @@ +using Newtonsoft.Json; +using Umbraco.Core; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Represents the configuration for the media picker value editor. + /// + public class MediaPicker3Configuration : IIgnoreUserStartNodesConfig + { + [ConfigurationField("filter", "Accepted types", "treesourcetypepicker", + Description = "Limit to specific types")] + public string Filter { get; set; } + + [ConfigurationField("multiple", "Pick multiple items", "boolean", Description = "Outputs a IEnumerable")] + public bool Multiple { get; set; } + + [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of medias")] + public NumberRange ValidationLimit { get; set; } = new NumberRange(); + + public class NumberRange + { + [JsonProperty("min")] + public int? Min { get; set; } + + [JsonProperty("max")] + public int? Max { get; set; } + } + + [ConfigurationField("startNodeId", "Start node", "mediapicker")] + public Udi StartNodeId { get; set; } + + [ConfigurationField(Core.Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } + + [ConfigurationField("enableLocalFocalPoint", "Enable Focal Point", "boolean")] + public bool EnableLocalFocalPoint { get; set; } + + [ConfigurationField("crops", "Image Crops", "views/propertyeditors/MediaPicker3/prevalue/mediapicker3.crops.html", Description = "Local crops, stored on document")] + public CropConfiguration[] Crops { get; set; } + + public class CropConfiguration + { + [JsonProperty("alias")] + public string Alias { get; set; } + + [JsonProperty("label")] + public string Label { get; set; } + + [JsonProperty("width")] + public int Width { get; set; } + + [JsonProperty("height")] + public int Height { get; set; } + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/MediaPicker3ConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPicker3ConfigurationEditor.cs new file mode 100644 index 0000000000..37063aa153 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/MediaPicker3ConfigurationEditor.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Represents the configuration editor for the media picker value editor. + /// + public class MediaPicker3ConfigurationEditor : ConfigurationEditor + { + /// + /// Initializes a new instance of the class. + /// + public MediaPicker3ConfigurationEditor() + { + // configure fields + // this is not part of ContentPickerConfiguration, + // but is required to configure the UI editor (when editing the configuration) + + Field(nameof(MediaPicker3Configuration.StartNodeId)) + .Config = new Dictionary { { "idType", "udi" } }; + + Field(nameof(MediaPicker3Configuration.Filter)) + .Config = new Dictionary { { "itemType", "media" } }; + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs new file mode 100644 index 0000000000..526b4830c8 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; +using Umbraco.Web.PropertyEditors.ValueConverters; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Represents a media picker property editor. + /// + [DataEditor( + Constants.PropertyEditors.Aliases.MediaPicker3, + EditorType.PropertyValue, + "Media Picker v3", + "mediapicker3", + ValueType = ValueTypes.Json, + Group = Constants.PropertyEditors.Groups.Media, + Icon = Constants.Icons.MediaImage)] + public class MediaPicker3PropertyEditor : DataEditor + { + /// + /// Initializes a new instance of the class. + /// + public MediaPicker3PropertyEditor(ILogger logger) + : base(logger) + { + } + + /// + protected override IConfigurationEditor CreateConfigurationEditor() => new MediaPicker3ConfigurationEditor(); + + protected override IDataValueEditor CreateValueEditor() => new MediaPicker3PropertyValueEditor(Attribute); + + internal class MediaPicker3PropertyValueEditor : DataValueEditor, IDataValueReference + { + /// + /// Note: no FromEditor() and ToEditor() methods + /// We do not want to transform the way the data is stored in the DB and would like to keep a raw JSON string + /// + public MediaPicker3PropertyValueEditor(DataEditorAttribute attribute) : base(attribute) + { + } + + public IEnumerable GetReferences(object value) + { + var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + + if (rawJson.IsNullOrWhiteSpace()) + yield break; + + var mediaWithCropsDtos = JsonConvert.DeserializeObject(rawJson); + + foreach (var mediaWithCropsDto in mediaWithCropsDtos) + { + yield return new UmbracoEntityReference(GuidUdi.Create(Constants.UdiEntityType.Media, mediaWithCropsDto.MediaKey)); + } + } + + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs new file mode 100644 index 0000000000..f9b2ad75e1 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs @@ -0,0 +1,119 @@ +using Newtonsoft.Json; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.Serialization; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Web.PublishedCache; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + public class MediaPickerWithCropsValueConverter : PropertyValueConverterBase + { + + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + + public MediaPickerWithCropsValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor) + { + _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); + } + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; + + /// + /// Enusre this property value convertor is for the New Media Picker with Crops aka MediaPicker 3 + /// + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.Equals(Core.Constants.PropertyEditors.Aliases.MediaPicker3); + + /// + /// Check if the raw JSON value is not an empty array + /// + public override bool? IsValue(object value, PropertyValueLevel level) => value?.ToString() != "[]"; + + /// + /// What C# model type does the raw JSON return for Models & Views + /// + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + { + // Check do we want to return IPublishedContent collection still or a NEW model ? + var isMultiple = IsMultipleDataType(propertyType.DataType); + return isMultiple + ? typeof(IEnumerable) + : typeof(MediaWithCrops); + } + + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) => source?.ToString(); + + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + { + var mediaItems = new List(); + var isMultiple = IsMultipleDataType(propertyType.DataType); + if (inter == null) + { + return isMultiple ? mediaItems: null; + } + + var dtos = JsonConvert.DeserializeObject>(inter.ToString()); + + foreach(var media in dtos) + { + var item = _publishedSnapshotAccessor.PublishedSnapshot.Media.GetById(media.MediaKey); + if (item != null) + { + mediaItems.Add(new MediaWithCrops + { + MediaItem = item, + LocalCrops = new ImageCropperValue + { + Crops = media.Crops, + FocalPoint = media.FocalPoint, + Src = item.Url() + } + }); + } + } + + return isMultiple ? mediaItems : FirstOrDefault(mediaItems); + } + + /// + /// Is the media picker configured to pick multiple media items + /// + /// + /// + private bool IsMultipleDataType(PublishedDataType dataType) + { + var config = dataType.ConfigurationAs(); + return config.Multiple; + } + + private object FirstOrDefault(IList mediaItems) + { + return mediaItems.Count == 0 ? null : mediaItems[0]; + } + + + /// + /// Model/DTO that represents the JSON that the MediaPicker3 stores + /// + [DataContract] + internal class MediaWithCropsDto + { + [DataMember(Name = "key")] + public Guid Key { get; set; } + + [DataMember(Name = "mediaKey")] + public Guid MediaKey { get; set; } + + [DataMember(Name = "crops")] + public IEnumerable Crops { get; set; } + + [DataMember(Name = "focalPoint")] + public ImageCropperValue.ImageCropperFocalPoint FocalPoint { get; set; } + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index a6cbefa825..ff988cf5bf 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -254,9 +254,17 @@ + + + + + + + + @@ -266,6 +274,7 @@ + diff --git a/src/Umbraco.Web/UrlHelperRenderExtensions.cs b/src/Umbraco.Web/UrlHelperRenderExtensions.cs index 0f5b0557f4..592c88945b 100644 --- a/src/Umbraco.Web/UrlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/UrlHelperRenderExtensions.cs @@ -262,6 +262,32 @@ namespace Umbraco.Web return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); } + public static IHtmlString GetCropUrl(this UrlHelper urlHelper, + ImageCropperValue imageCropperValue, + string cropAlias, + int? width = null, + int? height = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = true, + string cacheBusterValue = null, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true, + bool htmlEncode = true) + { + if (imageCropperValue == null) return EmptyHtmlString; + + var imageUrl = imageCropperValue.Src; + var url = imageUrl.GetCropUrl(imageCropperValue, width, height, cropAlias, quality, imageCropMode, + imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, + upScale); + return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); + } + + #endregion /// From 0dfefb72cb90bb8f2e35a2a78ecbbe17396b6ef1 Mon Sep 17 00:00:00 2001 From: Arkadiusz Biel Date: Sat, 31 Oct 2020 22:09:39 +0000 Subject: [PATCH 109/289] use v8 branch as default for API Docs --- src/ApiDocs/docfx.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ApiDocs/docfx.json b/src/ApiDocs/docfx.json index 76677d45d5..8283b351c4 100644 --- a/src/ApiDocs/docfx.json +++ b/src/ApiDocs/docfx.json @@ -50,6 +50,7 @@ ], "globalMetadata": { "_appTitle": "Umbraco c# Api docs", + "branch": "v8/contrib", "_enableSearch": true, "_disableContribution": false }, From c885831e02a8c2e4b312dec48945ca5d491130a6 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 22 Apr 2021 16:34:11 +0200 Subject: [PATCH 110/289] Revert "use v8 branch as default for API Docs" This reverts commit 0dfefb72cb90bb8f2e35a2a78ecbbe17396b6ef1. --- src/ApiDocs/docfx.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ApiDocs/docfx.json b/src/ApiDocs/docfx.json index 8283b351c4..76677d45d5 100644 --- a/src/ApiDocs/docfx.json +++ b/src/ApiDocs/docfx.json @@ -50,7 +50,6 @@ ], "globalMetadata": { "_appTitle": "Umbraco c# Api docs", - "branch": "v8/contrib", "_enableSearch": true, "_disableContribution": false }, From 4369747c720d2b5fa7e3c8b7c3617e3ba29a4421 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 22 Apr 2021 16:55:18 +0200 Subject: [PATCH 111/289] skip client side validation --- .../components/content/edit.controller.js | 87 +++++++++---------- 1 file changed, 39 insertions(+), 48 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 196c885b4e..bce797d5c8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -442,7 +442,6 @@ // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish function performSave(args) { - //Used to check validility of nested form - coming from Content Apps mostly //Set them all to be invalid var fieldsToRollback = checkValidility(); @@ -476,8 +475,6 @@ return $q.when(data); }, function (err) { - - syncTreeNode($scope.content, $scope.content.path); if($scope.contentForm.$invalid !== true) { @@ -739,54 +736,48 @@ clearNotifications($scope.content); // TODO: Add "..." to save button label if there are more than one variant to publish - currently it just adds the elipses if there's more than 1 variant if (hasVariants($scope.content)) { - - //before we launch the dialog we want to execute all client side validations first - if (formHelper.submitForm({ scope: $scope, action: "openSaveDialog", skipValidation:true, keepServerValidation:true })) { - var dialog = { - parentScope: $scope, - view: "views/content/overlays/save.html", - variants: $scope.content.variants, //set a model property for the dialog - skipFormValidation: true, //when submitting the overlay form, skip any client side validation - submitButtonLabelKey: "buttons_save", - submit: function (model) { - model.submitButtonState = "busy"; + var dialog = { + parentScope: $scope, + view: "views/content/overlays/save.html", + variants: $scope.content.variants, //set a model property for the dialog + skipFormValidation: true, //when submitting the overlay form, skip any client side validation + submitButtonLabelKey: "buttons_save", + submit: function (model) { + model.submitButtonState = "busy"; + clearNotifications($scope.content); + //we need to return this promise so that the dialog can handle the result and wire up the validation response + return performSave({ + saveMethod: $scope.saveMethod(), + action: "save", + showNotifications: false, + skipValidation: true + }).then(function (data) { + //show all notifications manually here since we disabled showing them automatically in the save method + formHelper.showNotifications(data); clearNotifications($scope.content); - //we need to return this promise so that the dialog can handle the result and wire up the validation response - return performSave({ - saveMethod: $scope.saveMethod(), - action: "save", - showNotifications: false - }).then(function (data) { - //show all notifications manually here since we disabled showing them automatically in the save method - formHelper.showNotifications(data); - clearNotifications($scope.content); - overlayService.close(); - return $q.when(data); - }, - function (err) { - clearDirtyState($scope.content.variants); - //model.submitButtonState = "error"; - // Because this is the "save"-action, then we actually save though there was a validation error, therefor we will show success and display the validation errors politely. - if(err && err.data && err.data.ModelState && Object.keys(err.data.ModelState).length > 0) { - model.submitButtonState = "success"; - } else { - model.submitButtonState = "error"; - } - //re-map the dialog model since we've re-bound the properties - dialog.variants = $scope.content.variants; - handleHttpException(err); - }); - }, - close: function (oldModel) { overlayService.close(); - } - }; + return $q.when(data); + }, + function (err) { + clearDirtyState($scope.content.variants); + //model.submitButtonState = "error"; + // Because this is the "save"-action, then we actually save though there was a validation error, therefor we will show success and display the validation errors politely. + if(err && err.data && err.data.ModelState && Object.keys(err.data.ModelState).length > 0) { + model.submitButtonState = "success"; + } else { + model.submitButtonState = "error"; + } + //re-map the dialog model since we've re-bound the properties + dialog.variants = $scope.content.variants; + handleHttpException(err); + }); + }, + close: function (oldModel) { + overlayService.close(); + } + }; - overlayService.open(dialog); - } - else { - showValidationNotification(); - } + overlayService.open(dialog); } else { //ensure the flags are set From e418bc56c64d53be847d2a827745f011da4302d0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 23 Apr 2021 13:40:29 +1000 Subject: [PATCH 112/289] fix merge issue and add correct assembly binding attribute --- src/Umbraco.Web.UI/web.Template.Debug.config | 7 +++++++ src/Umbraco.Web.UI/web.Template.config | 5 +++++ src/Umbraco.Web/Umbraco.Web.csproj | 7 +++---- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI/web.Template.Debug.config b/src/Umbraco.Web.UI/web.Template.Debug.config index d4c39e15dd..4e0ac86862 100644 --- a/src/Umbraco.Web.UI/web.Template.Debug.config +++ b/src/Umbraco.Web.UI/web.Template.Debug.config @@ -87,6 +87,13 @@ + + + + + + diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index ae141e5408..1de20bd835 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -252,6 +252,11 @@ + + + + + diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 9a567ad20f..d116163dc3 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -77,7 +77,7 @@ - 2.1.152 + 2.2.85 @@ -1314,8 +1314,7 @@ - - + - + 1.0.0-beta2-19324-01 runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Umbraco.Examine/UmbracoExamineIndex.cs b/src/Umbraco.Examine/UmbracoExamineIndex.cs index 511d78db92..c9ab2cd446 100644 --- a/src/Umbraco.Examine/UmbracoExamineIndex.cs +++ b/src/Umbraco.Examine/UmbracoExamineIndex.cs @@ -25,8 +25,7 @@ namespace Umbraco.Examine // wrapping all operations that end up calling base.SafelyProcessQueueItems in a safe call // context because they will fork a thread/task/whatever which should *not* capture our // call context (and the database it can contain)! - // TODO: FIX Examine to not flow the ExecutionContext so callers don't need to worry about this! - + /// /// Used to store the path of a content object /// @@ -99,13 +98,7 @@ namespace Umbraco.Examine { if (CanInitialize()) { - // Use SafeCallContext to prevent the current CallContext flow to child - // tasks executed in the base class so we don't leak Scopes. - // TODO: See notes at the top of this class - using (new SafeCallContext()) - { - base.PerformDeleteFromIndex(itemIds, onComplete); - } + base.PerformDeleteFromIndex(itemIds, onComplete); } } @@ -113,13 +106,7 @@ namespace Umbraco.Examine { if (CanInitialize()) { - // Use SafeCallContext to prevent the current CallContext flow to child - // tasks executed in the base class so we don't leak Scopes. - // TODO: See notes at the top of this class - using (new SafeCallContext()) - { - base.PerformIndexItems(values, onComplete); - } + base.PerformIndexItems(values, onComplete); } } diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 9d4bc4294c..242040836b 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -79,7 +79,7 @@ - + 1.8.14 diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 58baadcb35..707c773dc6 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -88,7 +88,7 @@ - + diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index a6cbefa825..ecf0e65b70 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -63,7 +63,7 @@ - + 2.7.0.100 @@ -1306,4 +1306,4 @@ - + \ No newline at end of file From 17e43a6b099eb9a2d893ab96463d893b92d843ab Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Fri, 2 Apr 2021 16:19:08 +0200 Subject: [PATCH 148/289] Update to noUiSlider v14.6.4 --- src/Umbraco.Web.UI.Client/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 95f5bcef49..16d6ff980b 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -41,7 +41,7 @@ "lazyload-js": "1.0.0", "moment": "2.22.2", "ng-file-upload": "12.2.13", - "nouislider": "14.6.3", + "nouislider": "14.6.4", "npm": "^6.14.7", "signalr": "2.4.0", "spectrum-colorpicker2": "2.0.8", From 499d22aa20901d3dafc5bd3c8b096f43bc54df13 Mon Sep 17 00:00:00 2001 From: Patrick de Mooij Date: Mon, 8 Feb 2021 12:58:10 +0100 Subject: [PATCH 149/289] Move the SetWeight logic to WeightedCollectionBuilderBase so other collections can use it too --- .../WeightedCollectionBuilderBase.cs | 16 +++++++++++++ .../Composing/CollectionBuildersTests.cs | 13 +++++++++++ .../Dashboards/DashboardCollectionBuilder.cs | 23 +------------------ 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs index 88eb61de76..b333fa646d 100644 --- a/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs @@ -16,6 +16,8 @@ namespace Umbraco.Core.Composing { protected abstract TBuilder This { get; } + private readonly Dictionary _customWeights = new Dictionary(); + /// /// Clears all types in the collection. /// @@ -107,6 +109,18 @@ namespace Umbraco.Core.Composing return This; } + /// + /// Changes the default weight of an item + /// + /// The type of item + /// The new weight + /// + public TBuilder SetWeight(int weight) where T : TItem + { + _customWeights[typeof(T)] = weight; + return This; + } + protected override IEnumerable GetRegisteringTypes(IEnumerable types) { var list = types.ToList(); @@ -118,6 +132,8 @@ namespace Umbraco.Core.Composing protected virtual int GetWeight(Type type) { + if (_customWeights.ContainsKey(type)) + return _customWeights[type]; var attr = type.GetCustomAttributes(typeof(WeightAttribute), false).OfType().SingleOrDefault(); return attr?.Weight ?? DefaultWeight; } diff --git a/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs b/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs index 1d8390e07e..87971360de 100644 --- a/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs +++ b/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs @@ -442,6 +442,19 @@ namespace Umbraco.Tests.Composing AssertCollection(col, typeof(Resolved2), typeof(Resolved1)); } + [Test] + public void WeightedBuilderSetWeight() + { + var builder = _composition.WithCollectionBuilder() + .Add() + .Add(); + builder.SetWeight(10); + + var factory = _composition.CreateFactory(); + var col = builder.CreateCollection(factory); + AssertCollection(col, typeof(Resolved1), typeof(Resolved2)); + } + #region Assertions private static void AssertCollection(IEnumerable col, params Type[] expected) diff --git a/src/Umbraco.Web/Dashboards/DashboardCollectionBuilder.cs b/src/Umbraco.Web/Dashboards/DashboardCollectionBuilder.cs index 1c05da8906..37697d848c 100644 --- a/src/Umbraco.Web/Dashboards/DashboardCollectionBuilder.cs +++ b/src/Umbraco.Web/Dashboards/DashboardCollectionBuilder.cs @@ -10,22 +10,8 @@ namespace Umbraco.Web.Dashboards { public class DashboardCollectionBuilder : WeightedCollectionBuilderBase { - private Dictionary _customWeights = new Dictionary(); - protected override DashboardCollectionBuilder This => this; - /// - /// Changes the default weight of a dashboard - /// - /// The type of dashboard - /// The new dashboard weight - /// - public DashboardCollectionBuilder SetWeight(int weight) where T : IDashboard - { - _customWeights[typeof(T)] = weight; - return this; - } - protected override IEnumerable CreateItems(IFactory factory) { // get the manifest parser just-in-time - injecting it in the ctor would mean that @@ -47,20 +33,13 @@ namespace Umbraco.Web.Dashboards private int GetWeight(IDashboard dashboard) { - var typeOfDashboard = dashboard.GetType(); - if(_customWeights.ContainsKey(typeOfDashboard)) - { - return _customWeights[typeOfDashboard]; - } - switch (dashboard) { case ManifestDashboard manifestDashboardDefinition: return manifestDashboardDefinition.Weight; default: - var weightAttribute = dashboard.GetType().GetCustomAttribute(false); - return weightAttribute?.Weight ?? DefaultWeight; + return GetWeight(dashboard.GetType()); } } } From 980d9ad5cfbfb60abd475473548ad46063ffea8e Mon Sep 17 00:00:00 2001 From: Adam Nelson Date: Wed, 12 May 2021 12:36:13 +1000 Subject: [PATCH 150/289] #10193 The listview search within a dialog (eg. minilistview) shows the loading indicator forever for zero results (#10202) * The listview search within a dialog (eg. minilistview) shows the loading indicator forever for zero results #10193 * Updated comment. * Another updated comment. Co-authored-by: Adam Nelson --- .../directives/components/umbminilistview.directive.js | 3 +++ .../src/views/components/umb-mini-list-view.html | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js index 783cd7f90a..f7b634a710 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js @@ -58,6 +58,9 @@ entityResource.getPagedChildren(miniListView.node.id, scope.entityType, miniListView.pagination) .then(function (data) { + if (!data.items) { + data.items = []; + } if (scope.onItemsLoaded) { scope.onItemsLoaded({items: data.items}); } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html index 6ec8b08da6..e2319f099d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html @@ -74,14 +74,14 @@
      {{ child.name }}
    - -
    + +
    No items have been added Sorry, we can not find what you are looking for.
    - -
    + +
    From 474a76fd2b954fbc385a9080e1c316259297ff36 Mon Sep 17 00:00:00 2001 From: Chad Currie Date: Sat, 15 May 2021 20:23:24 +1200 Subject: [PATCH 151/289] Use ContainsKey for Dictionaries. O(1) --- src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs b/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs index a1f4bad9a1..65aafafbcd 100644 --- a/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs +++ b/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs @@ -29,7 +29,7 @@ namespace Umbraco.Core.Models.Entities /// public virtual bool IsPropertyDirty(string propertyName) { - return _currentChanges != null && _currentChanges.Any(x => x.Key == propertyName); + return _currentChanges != null && _currentChanges.ContainsKey(propertyName); } /// @@ -61,7 +61,7 @@ namespace Umbraco.Core.Models.Entities /// public virtual bool WasPropertyDirty(string propertyName) { - return _savedChanges != null && _savedChanges.Any(x => x.Key == propertyName); + return _savedChanges != null && _savedChanges.ContainsKey(propertyName); } /// From aaf5cd5faeed798f5b508d2c999405160e7fc6a5 Mon Sep 17 00:00:00 2001 From: Paul Seal Date: Fri, 14 May 2021 10:02:46 +0100 Subject: [PATCH 152/289] Added a link to 404 documentation I think it will be useful to include a link to the documentation on how to create a custom 404 --- src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs b/src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs index 0045cf33dc..a6e4f4b450 100644 --- a/src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs +++ b/src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs @@ -42,7 +42,7 @@ namespace Umbraco.Web.Routing response.Write(""); if (string.IsNullOrWhiteSpace(_message) == false) response.Write("

    " + _message + "

    "); - response.Write("

    This page can be replaced with a custom 404. Check the documentation for \"custom 404\".

    "); + response.Write("

    This page can be replaced with a custom 404. Check the documentation for Custom 404 Error Pages.

    "); response.Write("

    This page is intentionally left ugly ;-)

    "); response.Write(""); } From cce3c49e78db32ecca916a0824783af6f4bca859 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 May 2021 20:34:33 +0000 Subject: [PATCH 153/289] Bump underscore from 1.9.1 to 1.12.1 in /src/Umbraco.Web.UI.Client Bumps [underscore](https://github.com/jashkenas/underscore) from 1.9.1 to 1.12.1. - [Release notes](https://github.com/jashkenas/underscore/releases) - [Commits](https://github.com/jashkenas/underscore/compare/1.9.1...1.12.1) Signed-off-by: dependabot[bot] --- src/Umbraco.Web.UI.Client/package-lock.json | 6 +++--- src/Umbraco.Web.UI.Client/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 0e8f48084f..3f53638fc6 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -15755,9 +15755,9 @@ "dev": true }, "underscore": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", - "integrity": "sha1-BtzjSg5op7q8KbNluOdLiSUgOWE=" + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" }, "undertaker": { "version": "1.2.1", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 16d6ff980b..6514f2f217 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -47,7 +47,7 @@ "spectrum-colorpicker2": "2.0.8", "tinymce": "4.9.11", "typeahead.js": "0.11.1", - "underscore": "1.9.1", + "underscore": "1.12.1", "wicg-inert": "^3.0.2" }, "devDependencies": { From df8b2d5580bf8589119b629ff5870122728cc96c Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 17 May 2021 10:50:25 +0200 Subject: [PATCH 154/289] Bump version to 8.14.0-rc --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 2a7386cb45..91918498f9 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.13.0")] -[assembly: AssemblyInformationalVersion("8.13.0-rc")] +[assembly: AssemblyFileVersion("8.14.0")] +[assembly: AssemblyInformationalVersion("8.14.0-rc")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 707c773dc6..a887159256 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -348,9 +348,9 @@ False True - 8130 + 8140 / - http://localhost:8130 + http://localhost:8140 False False From eb2f1e55fdc43df24fe1661d29642a03746607fa Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 17 May 2021 16:10:35 -0700 Subject: [PATCH 155/289] adds msgpack format to the default web.config --- src/Umbraco.Web.UI/web.Template.config | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index 1de20bd835..8c4b421839 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -49,6 +49,7 @@ + - -## Umbraco version - -I am seeing this issue on Umbraco version: - - -Reproduction ------------- - -If you're filing a bug, please describe how to reproduce it. Include as much -relevant information as possible, such as: - -### Bug summary - - - -### Specifics - - - -### Steps to reproduce - - - -### Expected result - - - -### Actual result - - diff --git a/.github/ISSUE_TEMPLATE/2_Feature_request.md b/.github/ISSUE_TEMPLATE/2_Feature_request.md deleted file mode 100644 index 16ec2568dd..0000000000 --- a/.github/ISSUE_TEMPLATE/2_Feature_request.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: 📮 Feature Request -about: Open a feature request, if you want to propose a new feature. ---- - -A brief description of your feature request goes here. - - - - -How can you help? -------------------------------- - - diff --git a/.github/ISSUE_TEMPLATE/3_BugNetCore.md b/.github/ISSUE_TEMPLATE/3_BugNetCore.md deleted file mode 100644 index 989904d4d8..0000000000 --- a/.github/ISSUE_TEMPLATE/3_BugNetCore.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -name: 🌟 .Net Core Bug Report -about: For bugs specifically for the upcoming .NET Core release of Umbraco, don't use this if you're working with Umbraco version 7 or 8 -labels: project/net-core ---- - -ℹ️ If this bug **also** appears on the current version 8 of Umbraco then please [report it as a regular bug](https://github.com/umbraco/Umbraco-CMS/issues/new?template=1_Bug.md), fixes in version 8 will be merged to the .NET Core version. - -A brief description of the issue goes here. - - - - -Reproduction ------------- - -If you're filing a bug, please describe how to reproduce it. Include as much -relevant information as possible, such as: - -### Bug summary - - - -### Specifics - - - -### Steps to reproduce - - - -### Expected result - - - -### Actual result - - diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 37d1be9158..d5418ad270 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,4 @@ -blank_issues_enabled: true +blank_issues_enabled: false contact_links: - name: ⁉️ Support Question url: https://our.umbraco.com @@ -8,4 +8,4 @@ contact_links: about: Documentation issues should be reported on the Umbraco documentation repository. - name: 🔐 Security Issue url: https://umbraco.com/about-us/trust-center/security-and-umbraco/how-to-report-a-vulnerability-in-umbraco/ - about: Discovered a Security Issue in Umbraco? \ No newline at end of file + about: Discovered a Security Issue in Umbraco? From 92cdf4e6e82aa95f7161c3775b17196daed28eb9 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 19 May 2021 10:20:01 +0200 Subject: [PATCH 158/289] Update CONTRIBUTING.md --- .github/CONTRIBUTING.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f4e237a1f2..d156e9de88 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -44,6 +44,15 @@ We have [documented what we consider small and large changes](CONTRIBUTION_GUIDE Remember, it is always worth working on an issue from the `Up for grabs` list or even asking for some feedback before you send us a PR. This way, your PR will not be closed as unwanted. +#### Ownership and copyright + +It is your responsibility to make sure that you're allowed to share the code you're providing us. +For example, you should have permission from your employer or customer to share code. + +Similarly, if your contribution is copied or adapted from somewhere else, make sure that the license allows you to reuse that for a contribution to Umbraco-CMS. + +If you're not sure, leave a note on your contribution and we will be happy to guide you. + ### What can I start with? Unsure where to begin contributing to Umbraco? You can start by looking through [these `Up for grabs` issues](https://github.com/umbraco/Umbraco-CMS/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs+) From 14558aea067294b017f911827aafea50ac152b9b Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 19 May 2021 10:21:44 +0200 Subject: [PATCH 159/289] Update CONTRIBUTING.md --- .github/CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d156e9de88..8f969bf235 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -16,6 +16,7 @@ This project and everyone participating in it, is governed by the [our Code of C [Contributing code changes](#contributing-code-changes) * [Guidelines for contributions we welcome](#guidelines-for-contributions-we-welcome) + * [Ownership and copyright](#ownership-and-copyright) * [What can I start with?](#what-can-i-start-with) * [How do I begin?](#how-do-i-begin) * [Pull requests](#pull-requests) From 017b56eee1b45cdca44e14510e72dd675412b0b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 19 May 2021 12:06:17 +0200 Subject: [PATCH 160/289] #10274 Variant sorting should take into account that there might not be any language (#10278) (#10284) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Niels Lyngsø Co-authored-by: Mads Rasmussen --- .../forms/umbfocuslock.directive.js | 69 ++++++++++--------- .../services/contenteditinghelper.service.js | 15 ++-- 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 03d376e36a..e1639dde26 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -40,7 +40,9 @@ function getDomNodes(){ infiniteEditorsWrapper = document.querySelector('.umb-editors'); - infiniteEditors = Array.from(infiniteEditorsWrapper.querySelectorAll('.umb-editor')); + if(infiniteEditorsWrapper) { + infiniteEditors = Array.from(infiniteEditorsWrapper.querySelectorAll('.umb-editor') || []); + } } function getFocusableElements(targetElm) { @@ -84,22 +86,24 @@ var defaultFocusedElement = getAutoFocusElement(focusableElements); var lastKnownElement; - // If an inifite editor is being closed then we reset the focus to the element that triggered the the overlay + // If an infinite editor is being closed then we reset the focus to the element that triggered the the overlay if(closingEditor){ - var lastItemIndex = $rootScope.lastKnownFocusableElements.length - 1; - var editorInfo = infiniteEditors[0].querySelector('.editor-info'); // If there is only one editor open, search for the "editor-info" inside it and set focus on it // This is relevant when a property editor has been selected and the editor where we selected it from // is closed taking us back to the first layer // Otherwise set it to the last element in the lastKnownFocusedElements array - if(infiniteEditors.length === 1 && editorInfo !== null){ - lastKnownElement = editorInfo; + if(infiniteEditors && infiniteEditors.length === 1){ + var editorInfo = infiniteEditors[0].querySelector('.editor-info'); + if(infiniteEditors && infiniteEditors.length === 1 && editorInfo !== null) { + lastKnownElement = editorInfo; - // Clear the array - clearLastKnownFocusedElements(); + // Clear the array + clearLastKnownFocusedElements(); + } } else { + var lastItemIndex = $rootScope.lastKnownFocusableElements.length - 1; lastKnownElement = $rootScope.lastKnownFocusableElements[lastItemIndex]; // Remove the last item from the array so we always set the correct lastKnowFocus for each layer @@ -149,20 +153,24 @@ } function cleanupEventHandlers() { - var activeEditor = infiniteEditors[infiniteEditors.length - 1]; - var inactiveEditors = infiniteEditors.filter(editor => editor !== activeEditor); + //if we're in infinite editing mode + if(infiniteEditors.length > 0) { + var activeEditor = infiniteEditors[infiniteEditors.length - 1]; + var inactiveEditors = infiniteEditors.filter(editor => editor !== activeEditor); - if(inactiveEditors.length > 0) { - for (var index = 0; index < inactiveEditors.length; index++) { - var inactiveEditor = inactiveEditors[index]; + if(inactiveEditors.length > 0) { + for (var index = 0; index < inactiveEditors.length; index++) { + var inactiveEditor = inactiveEditors[index]; - // Remove event handlers from inactive editors - inactiveEditor.removeEventListener('keydown', handleKeydown); + // Remove event handlers from inactive editors + inactiveEditor.removeEventListener('keydown', handleKeydown); + } + } + else { + // Why is this one only begin called if there is no other infinite editors, wouldn't it make sense always to clean this up? + // Remove event handlers from the active editor + activeEditor.removeEventListener('keydown', handleKeydown); } - } - else { - // Remove event handlers from the active editor - activeEditor.removeEventListener('keydown', handleKeydown); } } @@ -173,10 +181,7 @@ // Fetch the DOM nodes we need getDomNodes(); - // Cleanup event handlers if we're in infinite editing mode - if(infiniteEditors.length > 0){ - cleanupEventHandlers(); - } + cleanupEventHandlers(); getFocusableElements(targetElm); @@ -204,17 +209,19 @@ // Make sure to disconnect the observer so we potentially don't end up with having many active ones disconnectObserver = true; - // Pass the correct editor in order to find the focusable elements - var newTarget = infiniteEditors[infiniteEditors.length - 2]; + if(infiniteEditors && infiniteEditors.length > 1) { + // Pass the correct editor in order to find the focusable elements + var newTarget = infiniteEditors[infiniteEditors.length - 2]; - if(infiniteEditors.length > 1){ - // Setting closing till true will let us re-apply the last known focus to then opened layer that then becomes - // active - closingEditor = true; + if(infiniteEditors.length > 1) { + // Setting closing till true will let us re-apply the last known focus to then opened layer that then becomes + // active + closingEditor = true; - onInit(newTarget); + onInit(newTarget); - return; + return; + } } // Clear lastKnownFocusableElements diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index bab665579c..67e466af35 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -789,10 +789,10 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt */ sortVariants: function (a, b) { const statesOrder = {'PublishedPendingChanges':1, 'Published': 1, 'Draft': 2, 'NotCreated': 3}; - const compareDefault = (a,b) => (!a.language.isDefault ? 1 : -1) - (!b.language.isDefault ? 1 : -1); + const compareDefault = (a,b) => (a.language && a.language.isDefault ? -1 : 1) - (b.language && b.language.isDefault ? -1 : 1); // Make sure mandatory variants goes on top, unless they are published, cause then they already goes to the top and then we want to mix them with other published variants. - const compareMandatory = (a,b) => (a.state === 'PublishedPendingChanges' || a.state === 'Published') ? 0 : (!a.language.isMandatory ? 1 : -1) - (!b.language.isMandatory ? 1 : -1); + const compareMandatory = (a,b) => (a.state === 'PublishedPendingChanges' || a.state === 'Published') ? 0 : (a.language && a.language.isMandatory ? -1 : 1) - (b.language && b.language.isMandatory ? -1 : 1); const compareState = (a, b) => (statesOrder[a.state] || 99) - (statesOrder[b.state] || 99); const compareName = (a, b) => a.displayName.localeCompare(b.displayName); @@ -813,17 +813,18 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt */ getSortedVariantsAndSegments: function (variantsAndSegments) { const sortedVariants = variantsAndSegments.filter(variant => !variant.segment).sort(this.sortVariants); - let segments = variantsAndSegments.filter(variant => variant.segment); + let variantsWithSegments = variantsAndSegments.filter(variant => variant.segment); let sortedAvailableVariants = []; sortedVariants.forEach((variant) => { - const sortedMatchedSegments = segments.filter(segment => segment.language.culture === variant.language.culture).sort(this.sortVariants); - segments = segments.filter(segment => segment.language.culture !== variant.language.culture); + const sortedMatchedSegments = variantsWithSegments.filter(segment => segment.language && variant.language && segment.language.culture === variant.language.culture).sort(this.sortVariants); + // remove variants for this culture + variantsWithSegments = variantsWithSegments.filter(segment => !segment.language || segment.language && variant.language && segment.language.culture !== variant.language.culture); sortedAvailableVariants = [...sortedAvailableVariants, ...[variant], ...sortedMatchedSegments]; }) - // if we have segments without a parent language variant we need to add the remaining segments to the array - sortedAvailableVariants = [...sortedAvailableVariants, ...segments.sort(this.sortVariants)]; + // if we have segments without a parent language variant we need to add the remaining variantsWithSegments to the array + sortedAvailableVariants = [...sortedAvailableVariants, ...variantsWithSegments.sort(this.sortVariants)]; return sortedAvailableVariants; } From 36c49b0a2592758e2f525588047fe3b89b976844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 18 May 2021 11:08:00 +0200 Subject: [PATCH 161/289] #10274 Variant sorting should take into account that there might not be any language (#10278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Variant sorting should take into account that there might not be any language available * fix languages Co-authored-by: Niels Lyngsø Co-authored-by: Mads Rasmussen (cherry picked from commit e19a5989e9a9c77f1d35622fe70b0fe573df59c9) --- .../services/contenteditinghelper.service.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index 8524b960c6..26f8dc86ea 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -775,10 +775,10 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt */ sortVariants: function (a, b) { const statesOrder = {'PublishedPendingChanges':1, 'Published': 1, 'Draft': 2, 'NotCreated': 3}; - const compareDefault = (a,b) => (!a.language.isDefault ? 1 : -1) - (!b.language.isDefault ? 1 : -1); + const compareDefault = (a,b) => (a.language && a.language.isDefault ? -1 : 1) - (b.language && b.language.isDefault ? -1 : 1); // Make sure mandatory variants goes on top, unless they are published, cause then they already goes to the top and then we want to mix them with other published variants. - const compareMandatory = (a,b) => (a.state === 'PublishedPendingChanges' || a.state === 'Published') ? 0 : (!a.language.isMandatory ? 1 : -1) - (!b.language.isMandatory ? 1 : -1); + const compareMandatory = (a,b) => (a.state === 'PublishedPendingChanges' || a.state === 'Published') ? 0 : (a.language && a.language.isMandatory ? -1 : 1) - (b.language && b.language.isMandatory ? -1 : 1); const compareState = (a, b) => (statesOrder[a.state] || 99) - (statesOrder[b.state] || 99); const compareName = (a, b) => a.displayName.localeCompare(b.displayName); @@ -799,17 +799,18 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt */ getSortedVariantsAndSegments: function (variantsAndSegments) { const sortedVariants = variantsAndSegments.filter(variant => !variant.segment).sort(this.sortVariants); - let segments = variantsAndSegments.filter(variant => variant.segment); + let variantsWithSegments = variantsAndSegments.filter(variant => variant.segment); let sortedAvailableVariants = []; sortedVariants.forEach((variant) => { - const sortedMatchedSegments = segments.filter(segment => segment.language.culture === variant.language.culture).sort(this.sortVariants); - segments = segments.filter(segment => segment.language.culture !== variant.language.culture); + const sortedMatchedSegments = variantsWithSegments.filter(segment => segment.language && variant.language && segment.language.culture === variant.language.culture).sort(this.sortVariants); + // remove variants for this culture + variantsWithSegments = variantsWithSegments.filter(segment => !segment.language || segment.language && variant.language && segment.language.culture !== variant.language.culture); sortedAvailableVariants = [...sortedAvailableVariants, ...[variant], ...sortedMatchedSegments]; }) - // if we have segments without a parent language variant we need to add the remaining segments to the array - sortedAvailableVariants = [...sortedAvailableVariants, ...segments.sort(this.sortVariants)]; + // if we have segments without a parent language variant we need to add the remaining variantsWithSegments to the array + sortedAvailableVariants = [...sortedAvailableVariants, ...variantsWithSegments.sort(this.sortVariants)]; return sortedAvailableVariants; } From 7c2f95613030b2e52f3ce66629916ef095ac3601 Mon Sep 17 00:00:00 2001 From: Mike Chambers Date: Tue, 27 Apr 2021 13:56:31 +0100 Subject: [PATCH 162/289] Update ModelsBuilderComposer.cs issue #10186 typo in IsExternalModelsBuilderInstalled -> Umbraco.ModelsBuider (cherry picked from commit b783399c5cd730cfcbff3fdcedf2f7a4ab2eb25c) --- .../Compose/ModelsBuilderComposer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs index 01010cca66..fee9b6f62e 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs @@ -47,7 +47,7 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose { var assemblyNames = new[] { - "Umbraco.ModelsBuider", + "Umbraco.ModelsBuilder", "ModelsBuilder.Umbraco" }; From 9730a18c9b3bad19d9415af49e17977bda375ae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 19 May 2021 12:06:17 +0200 Subject: [PATCH 163/289] #10274 Variant sorting should take into account that there might not be any language (#10278) (#10284) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Niels Lyngsø Co-authored-by: Mads Rasmussen (cherry picked from commit 017b56eee1b45cdca44e14510e72dd675412b0b5) --- .../forms/umbfocuslock.directive.js | 69 ++++++++++--------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 03d376e36a..e1639dde26 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -40,7 +40,9 @@ function getDomNodes(){ infiniteEditorsWrapper = document.querySelector('.umb-editors'); - infiniteEditors = Array.from(infiniteEditorsWrapper.querySelectorAll('.umb-editor')); + if(infiniteEditorsWrapper) { + infiniteEditors = Array.from(infiniteEditorsWrapper.querySelectorAll('.umb-editor') || []); + } } function getFocusableElements(targetElm) { @@ -84,22 +86,24 @@ var defaultFocusedElement = getAutoFocusElement(focusableElements); var lastKnownElement; - // If an inifite editor is being closed then we reset the focus to the element that triggered the the overlay + // If an infinite editor is being closed then we reset the focus to the element that triggered the the overlay if(closingEditor){ - var lastItemIndex = $rootScope.lastKnownFocusableElements.length - 1; - var editorInfo = infiniteEditors[0].querySelector('.editor-info'); // If there is only one editor open, search for the "editor-info" inside it and set focus on it // This is relevant when a property editor has been selected and the editor where we selected it from // is closed taking us back to the first layer // Otherwise set it to the last element in the lastKnownFocusedElements array - if(infiniteEditors.length === 1 && editorInfo !== null){ - lastKnownElement = editorInfo; + if(infiniteEditors && infiniteEditors.length === 1){ + var editorInfo = infiniteEditors[0].querySelector('.editor-info'); + if(infiniteEditors && infiniteEditors.length === 1 && editorInfo !== null) { + lastKnownElement = editorInfo; - // Clear the array - clearLastKnownFocusedElements(); + // Clear the array + clearLastKnownFocusedElements(); + } } else { + var lastItemIndex = $rootScope.lastKnownFocusableElements.length - 1; lastKnownElement = $rootScope.lastKnownFocusableElements[lastItemIndex]; // Remove the last item from the array so we always set the correct lastKnowFocus for each layer @@ -149,20 +153,24 @@ } function cleanupEventHandlers() { - var activeEditor = infiniteEditors[infiniteEditors.length - 1]; - var inactiveEditors = infiniteEditors.filter(editor => editor !== activeEditor); + //if we're in infinite editing mode + if(infiniteEditors.length > 0) { + var activeEditor = infiniteEditors[infiniteEditors.length - 1]; + var inactiveEditors = infiniteEditors.filter(editor => editor !== activeEditor); - if(inactiveEditors.length > 0) { - for (var index = 0; index < inactiveEditors.length; index++) { - var inactiveEditor = inactiveEditors[index]; + if(inactiveEditors.length > 0) { + for (var index = 0; index < inactiveEditors.length; index++) { + var inactiveEditor = inactiveEditors[index]; - // Remove event handlers from inactive editors - inactiveEditor.removeEventListener('keydown', handleKeydown); + // Remove event handlers from inactive editors + inactiveEditor.removeEventListener('keydown', handleKeydown); + } + } + else { + // Why is this one only begin called if there is no other infinite editors, wouldn't it make sense always to clean this up? + // Remove event handlers from the active editor + activeEditor.removeEventListener('keydown', handleKeydown); } - } - else { - // Remove event handlers from the active editor - activeEditor.removeEventListener('keydown', handleKeydown); } } @@ -173,10 +181,7 @@ // Fetch the DOM nodes we need getDomNodes(); - // Cleanup event handlers if we're in infinite editing mode - if(infiniteEditors.length > 0){ - cleanupEventHandlers(); - } + cleanupEventHandlers(); getFocusableElements(targetElm); @@ -204,17 +209,19 @@ // Make sure to disconnect the observer so we potentially don't end up with having many active ones disconnectObserver = true; - // Pass the correct editor in order to find the focusable elements - var newTarget = infiniteEditors[infiniteEditors.length - 2]; + if(infiniteEditors && infiniteEditors.length > 1) { + // Pass the correct editor in order to find the focusable elements + var newTarget = infiniteEditors[infiniteEditors.length - 2]; - if(infiniteEditors.length > 1){ - // Setting closing till true will let us re-apply the last known focus to then opened layer that then becomes - // active - closingEditor = true; + if(infiniteEditors.length > 1) { + // Setting closing till true will let us re-apply the last known focus to then opened layer that then becomes + // active + closingEditor = true; - onInit(newTarget); + onInit(newTarget); - return; + return; + } } // Clear lastKnownFocusableElements From 779c94c66f499730bb2449409f4febfa61dae1e7 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 19 May 2021 13:29:02 +0200 Subject: [PATCH 164/289] Update CONTRIBUTING.md --- .github/CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 8f969bf235..f83861acd9 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -54,6 +54,8 @@ Similarly, if your contribution is copied or adapted from somewhere else, make s If you're not sure, leave a note on your contribution and we will be happy to guide you. +When your contribution has been accepted, it will be MIT licensed from that time onwards. + ### What can I start with? Unsure where to begin contributing to Umbraco? You can start by looking through [these `Up for grabs` issues](https://github.com/umbraco/Umbraco-CMS/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs+) From a2c461bcdba057418a7a996b050b20d4a2c1ffd9 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 19 May 2021 13:30:53 +0200 Subject: [PATCH 165/289] Update CONTRIBUTING.md --- .github/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f83861acd9..3432ac472a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -54,7 +54,7 @@ Similarly, if your contribution is copied or adapted from somewhere else, make s If you're not sure, leave a note on your contribution and we will be happy to guide you. -When your contribution has been accepted, it will be MIT licensed from that time onwards. +When your contribution has been accepted, it will be [MIT licensed](https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/LICENSE.md) from that time onwards. ### What can I start with? From 3f596e27ccba6f30bd17779a94a41f9633d6f68e Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 19 May 2021 16:31:52 +0200 Subject: [PATCH 166/289] Bump version to 8.13.1 --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 1009ec125b..451a4b0d77 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.13.0")] -[assembly: AssemblyInformationalVersion("8.13.0")] +[assembly: AssemblyFileVersion("8.13.1")] +[assembly: AssemblyInformationalVersion("8.13.1")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 58baadcb35..f69fdf7026 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -348,9 +348,9 @@ False True - 8130 + 8131 / - http://localhost:8130 + http://localhost:8131 False False From 710ecf2537a8630d00db793877d5c169c5cf8095 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Thu, 20 May 2021 01:00:23 +0200 Subject: [PATCH 167/289] Add option to remove/cancel added crops (#10267) * Add option to remove/cancel added crops * Move vm functions to top * Only show cancel button for empty/new crop --- .../prevalue/mediapicker3.crops.controller.js | 20 ++++++--- .../prevalue/mediapicker3.crops.html | 4 +- ...umbMediaPicker3PropertyEditor.component.js | 44 +++++++++---------- 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.controller.js index 922370a032..b26c6f8549 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.controller.js @@ -22,7 +22,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.MediaPicker3.CropC crop.editMode = true; }; - $scope.addNewCrop = function (evt) { + $scope.addNewCrop = function (evt) { evt.preventDefault(); var crop = {}; @@ -30,7 +30,8 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.MediaPicker3.CropC $scope.model.value.push(crop); $scope.validate(crop); - } + }; + $scope.setChanges = function (crop) { $scope.validate(crop); if( @@ -42,22 +43,31 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.MediaPicker3.CropC window.dispatchEvent(new Event('resize.umbImageGravity')); } }; + + $scope.isEmpty = function (crop) { + return !crop.label && !crop.alias && !crop.width && !crop.height; + }; + $scope.useForAlias = function (crop) { if (crop.alias == null || crop.alias === "") { crop.alias = (crop.label || "").toCamelCase(); } }; - $scope.validate = function(crop) { + + $scope.validate = function (crop) { $scope.validateWidth(crop); $scope.validateHeight(crop); $scope.validateAlias(crop); - } + }; + $scope.validateWidth = function (crop) { crop.hasWidthError = !(Utilities.isNumber(crop.width) && crop.width > 0); }; + $scope.validateHeight = function (crop) { crop.hasHeightError = !(Utilities.isNumber(crop.height) && crop.height > 0); }; + $scope.validateAlias = function (crop, $event) { var exists = $scope.model.value.find( x => crop !== x && crop.alias === x.alias); if (exists !== undefined || crop.alias === "") { @@ -67,7 +77,6 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.MediaPicker3.CropC // everything was good: crop.hasAliasError = false; } - }; $scope.confirmChanges = function (crop, event) { @@ -76,6 +85,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.MediaPicker3.CropC event.preventDefault(); } }; + $scope.focusNextField = function (event) { if (event.keyCode == 13) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.html index 46b9ddb15f..de7e7b1767 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.html @@ -20,7 +20,7 @@
    -
    +
    @@ -33,7 +33,6 @@
    -
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js index 675381d46e..96f3126288 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js @@ -40,8 +40,13 @@ vm.loading = true; + vm.activeMediaEntry = null; vm.supportCopy = clipboardService.isSupported(); + vm.addMediaAt = addMediaAt; + vm.editMedia = editMedia; + vm.removeMedia = removeMedia; + vm.copyMedia = copyMedia; vm.labels = {}; @@ -121,7 +126,6 @@ } } - vm.addMediaAt = addMediaAt; function addMediaAt(createIndex, $event) { var mediaPicker = { startNodeId: vm.model.config.startNodeId, @@ -164,7 +168,7 @@ } } - if(vm.model.config.filter) { + if (vm.model.config.filter) { mediaPicker.filter = vm.model.config.filter; } @@ -182,6 +186,7 @@ // To be used by infinite editor. (defined here cause we need configuration from property editor) function changeMediaFor(mediaEntry, onSuccess) { + var mediaPicker = { startNodeId: vm.model.config.startNodeId, startNodeIsVirtual: vm.model.config.startNodeIsVirtual, @@ -199,16 +204,16 @@ mediaEntry.focalPoint = null; updateMediaEntryData(mediaEntry); - if(onSuccess) { + if (onSuccess) { onSuccess(); } }, close: function () { editorService.close(); } - } + }; - if(vm.model.config.filter) { + if (vm.model.config.filter) { mediaPicker.filter = vm.model.config.filter; } @@ -238,26 +243,23 @@ } }); mediaEntry.crops = newCrops; - } - vm.removeMedia = removeMedia; function removeMedia(media) { var index = vm.model.value.indexOf(media); - if(index !== -1) { + if (index !== -1) { vm.model.value.splice(index, 1); } } + function deleteAllMedias() { vm.model.value = []; } - vm.activeMediaEntry = null; function setActiveMedia(mediaEntryOrNull) { vm.activeMediaEntry = mediaEntryOrNull; } - - vm.editMedia = editMedia; + function editMedia(mediaEntry, options, $event) { if($event) @@ -304,13 +306,13 @@ editorService.open(mediaEditorModel); } - var getDocumentNameAndIcon = function() { + var getDocumentNameAndIcon = function () { // get node name var contentNodeName = "?"; var contentNodeIcon = null; - if(vm.umbVariantContent) { + if (vm.umbVariantContent) { contentNodeName = vm.umbVariantContent.editor.content.name; - if(vm.umbVariantContentEditors) { + if (vm.umbVariantContentEditors) { contentNodeIcon = vm.umbVariantContentEditors.content.icon.split(" ")[0]; } else if (vm.umbElementEditorContent) { contentNodeIcon = vm.umbElementEditorContent.model.documentType.icon.split(" ")[0]; @@ -324,9 +326,9 @@ name: contentNodeName, icon: contentNodeIcon } - } + }; - var requestCopyAllMedias = function() { + var requestCopyAllMedias = function () { var mediaKeys = vm.model.value.map(x => x.mediaKey) entityResource.getByIds(mediaKeys, "Media").then(function (entities) { @@ -338,18 +340,18 @@ var documentInfo = getDocumentNameAndIcon(); - localizationService.localize("clipboard_labelForArrayOfItemsFrom", [vm.model.label, documentInfo.name]).then(function(localizedLabel) { + localizationService.localize("clipboard_labelForArrayOfItemsFrom", [vm.model.label, documentInfo.name]).then(function (localizedLabel) { clipboardService.copyArray(clipboardService.TYPES.MEDIA, aliases, vm.model.value, localizedLabel, documentInfo.icon || "icon-thumbnail-list", vm.model.id); }); }); - } + }; - vm.copyMedia = copyMedia; function copyMedia(mediaEntry) { entityResource.getById(mediaEntry.mediaKey, "Media").then(function (mediaEntity) { clipboardService.copy(clipboardService.TYPES.MEDIA, mediaEntity.metaData.ContentTypeAlias, mediaEntry, mediaEntity.name, mediaEntity.icon, mediaEntry.key); }); } + function requestPasteFromClipboard(createIndex, pasteEntry, pasteType) { if (pasteEntry === undefined) { @@ -362,9 +364,7 @@ updateMediaEntryData(pasteEntry); vm.model.value.splice(createIndex, 0, pasteEntry); - return true; - } function requestRemoveAllMedia() { @@ -383,7 +383,6 @@ }); } - vm.sortableOptions = { cursor: "grabbing", handle: "umb-media-card", @@ -397,7 +396,6 @@ } }; - function onAmountOfMediaChanged() { // enable/disable property actions From 9bf01741b64bdbd43ced0d6ed28d20c450e330e8 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 20 May 2021 09:29:45 +0200 Subject: [PATCH 168/289] Fix invalid XML --- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 08448fa355..8e33f102c7 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1907,7 +1907,6 @@ Mange hilsner fra Umbraco robotten Tilføj indhold Tilføj %0% Feltet %0% bruger editor %1% som ikke er supporteret for blokke. - Hvad er Indholdsskabeloner? From 71a1817a061983bbdb0d8ece856a634e4f028ff2 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 11 May 2021 17:28:11 +0200 Subject: [PATCH 169/289] Fixed issue with data types not being cleared in cache refresh operations. --- src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs index c9caf5aaa7..37d36fca45 100644 --- a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs @@ -3,6 +3,7 @@ using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Core.Services; using Umbraco.Web.PublishedCache; @@ -56,6 +57,11 @@ namespace Umbraco.Web.Cache foreach (var payload in payloads) { _idkMap.ClearCache(payload.Id); + + if (dataTypeCache.Success) + { + dataTypeCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + } } // TODO: not sure I like these? From 12328ba2f761b4c20b50df3dc83bb85b5f652cf6 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 11 May 2021 17:28:11 +0200 Subject: [PATCH 170/289] Fixed issue with data types not being cleared in cache refresh operations. (cherry picked from commit 71a1817a061983bbdb0d8ece856a634e4f028ff2) --- src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs index c9caf5aaa7..37d36fca45 100644 --- a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs @@ -3,6 +3,7 @@ using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Core.Services; using Umbraco.Web.PublishedCache; @@ -56,6 +57,11 @@ namespace Umbraco.Web.Cache foreach (var payload in payloads) { _idkMap.ClearCache(payload.Id); + + if (dataTypeCache.Success) + { + dataTypeCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + } } // TODO: not sure I like these? From 008480a17b652bc2c267ab2bfd68c4ae70162394 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Fri, 21 May 2021 15:01:51 +0200 Subject: [PATCH 171/289] Can't use optimized version of this method call until 8.14 --- src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs index 37d36fca45..431f560400 100644 --- a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs @@ -60,7 +60,7 @@ namespace Umbraco.Web.Cache if (dataTypeCache.Success) { - dataTypeCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + dataTypeCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); } } From 43ee6f288ea0b4249a6f411b03f29273fa809837 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 24 May 2021 11:25:03 -0700 Subject: [PATCH 172/289] fixes initialize flag --- src/Umbraco.Web/Search/ExamineFinalComponent.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Search/ExamineFinalComponent.cs b/src/Umbraco.Web/Search/ExamineFinalComponent.cs index 22dcb43cc0..335d19baf0 100644 --- a/src/Umbraco.Web/Search/ExamineFinalComponent.cs +++ b/src/Umbraco.Web/Search/ExamineFinalComponent.cs @@ -56,6 +56,8 @@ namespace Umbraco.Web.Search // double check lock, we must only do this once if (!_initialized) { + _initialized = true; + if (!_mainDom.IsMainDom) return; var bootState = _syncBootStateAccessor.GetSyncBootState(); @@ -63,7 +65,8 @@ namespace Umbraco.Web.Search _examineManager.ConfigureIndexes(_mainDom, _logger); // if it's a cold boot, rebuild all indexes including non-empty ones - _indexRebuilder.RebuildIndexes(bootState != SyncBootState.ColdBoot, 0); + // delay one minute since a cold boot also triggers nucache rebuilds + _indexRebuilder.RebuildIndexes(bootState != SyncBootState.ColdBoot, 60000); } } } From be3bfe1b6370d5d2cfded7a569c9fa1a969baa20 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 24 May 2021 12:32:57 -0700 Subject: [PATCH 173/289] Removes Booting event, backports lazy changes for PublishedSnapshotService from v9. --- .../Sync/ISyncBootStateAccessor.cs | 5 - .../NuCache/PublishedSnapshotService.cs | 202 ++++++++++-------- .../Search/ExamineFinalComponent.cs | 22 +- 3 files changed, 119 insertions(+), 110 deletions(-) diff --git a/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs index 249b038cae..8d4cda093c 100644 --- a/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs +++ b/src/Umbraco.Core/Sync/ISyncBootStateAccessor.cs @@ -12,10 +12,5 @@ namespace Umbraco.Core.Sync ///
    /// SyncBootState GetSyncBootState(); - - /// - /// Raised when the boot state is known - /// - event EventHandler Booting; // TODO: This should be removed in netcore } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index b35a3d0edb..f70160cde8 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -36,6 +36,8 @@ namespace Umbraco.Web.PublishedCache.NuCache internal class PublishedSnapshotService : PublishedSnapshotServiceBase { + private readonly PublishedSnapshotServiceOptions _options; + private readonly IMainDom _mainDom; private readonly ServiceContext _serviceContext; private readonly IPublishedContentTypeFactory _publishedContentTypeFactory; private readonly IScopeProvider _scopeProvider; @@ -50,12 +52,13 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly IDefaultCultureAccessor _defaultCultureAccessor; private readonly UrlSegmentProviderCollection _urlSegmentProviders; - // volatile because we read it with no lock - private volatile bool _isReady; + private bool _isReady; + private bool _isReadSet; + private object _isReadyLock; - private readonly ContentStore _contentStore; - private readonly ContentStore _mediaStore; - private readonly SnapDictionary _domainStore; + private ContentStore _contentStore; + private ContentStore _mediaStore; + private SnapDictionary _domainStore; private readonly object _storesLock = new object(); private readonly object _elementsLock = new object(); @@ -88,9 +91,12 @@ namespace Umbraco.Web.PublishedCache.NuCache ISyncBootStateAccessor syncBootStateAccessor) : base(publishedSnapshotAccessor, variationContextAccessor) { + //if (Interlocked.Increment(ref _singletonCheck) > 1) // throw new Exception("Singleton must be instantiated only once!"); + _options = options; + _mainDom = mainDom; _serviceContext = serviceContext; _publishedContentTypeFactory = publishedContentTypeFactory; _dataSource = dataSource; @@ -123,44 +129,6 @@ namespace Umbraco.Web.PublishedCache.NuCache if (runtime.Level != RuntimeLevel.Run) return; - // lock this entire call, we only want a single thread to be accessing the stores at once and within - // the call below to mainDom.Register, a callback may occur on a threadpool thread to MainDomRelease - // at the same time as we are trying to write to the stores. MainDomRelease also locks on _storesLock so - // it will not be able to close the stores until we are done populating (if the store is empty) - lock (_storesLock) - { - if (options.IgnoreLocalDb == false) - { - var registered = mainDom.Register(MainDomRegister, MainDomRelease); - - // stores are created with a db so they can write to it, but they do not read from it, - // stores need to be populated, happens in OnResolutionFrozen which uses _localDbExists to - // figure out whether it can read the databases or it should populate them from sql - - _logger.Info("Creating the content store, localContentDbExists? {LocalContentDbExists}", _localContentDbExists); - _contentStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger, _localContentDb); - _logger.Info("Creating the media store, localMediaDbExists? {LocalMediaDbExists}", _localMediaDbExists); - _mediaStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger, _localMediaDb); - } - else - { - _logger.Info("Creating the content store (local db ignored)"); - _contentStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger); - _logger.Info("Creating the media store (local db ignored)"); - _mediaStore = new ContentStore(publishedSnapshotAccessor, variationContextAccessor, logger); - } - - _domainStore = new SnapDictionary(); - - _syncBootStateAccessor.Booting += (sender, args) => - { - LoadCachesOnStartup(args); - }; - } - - Guid GetUid(ContentStore store, int id) => store.LiveSnapshot.Get(id)?.Uid ?? default; - int GetId(ContentStore store, Guid uid) => store.LiveSnapshot.Get(uid)?.Id ?? default; - if (idkMap != null) { idkMap.SetMapper(UmbracoObjectTypes.Document, id => GetUid(_contentStore, id), uid => GetId(_contentStore, uid)); @@ -168,6 +136,18 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + private int GetId(ContentStore store, Guid uid) + { + EnsureCaches(); + return store.LiveSnapshot.Get(uid)?.Id ?? default; + } + + private Guid GetUid(ContentStore store, int id) + { + EnsureCaches(); + return store.LiveSnapshot.Get(id)?.Uid ?? default; + } + /// /// Install phase of /// @@ -219,51 +199,82 @@ namespace Umbraco.Web.PublishedCache.NuCache } /// - /// Populates the stores + /// Lazily populates the stores only when they are first requested /// - /// This is called inside of a lock for _storesLock - private void LoadCachesOnStartup(SyncBootState bootState) - { - // TODO: This is super ugly that this does this as part of the ctor. - // In netcore this will be different, the initialization will happen - // outside of the ctor. - - var okContent = false; - var okMedia = false; - - try + internal void EnsureCaches() => LazyInitializer.EnsureInitialized( + ref _isReady, + ref _isReadSet, + ref _isReadyLock, + () => { - if (bootState != SyncBootState.ColdBoot && _localContentDbExists) + // lock this entire call, we only want a single thread to be accessing the stores at once and within + // the call below to mainDom.Register, a callback may occur on a threadpool thread to MainDomRelease + // at the same time as we are trying to write to the stores. MainDomRelease also locks on _storesLock so + // it will not be able to close the stores until we are done populating (if the store is empty) + lock (_storesLock) { - okContent = LockAndLoadContent(scope => LoadContentFromLocalDbLocked(true)); - if (!okContent) - _logger.Warn("Loading content from local db raised warnings, will reload from database."); + if (!_options.IgnoreLocalDb) + { + var registered = _mainDom.Register(MainDomRegister, MainDomRelease); + + // stores are created with a db so they can write to it, but they do not read from it, + // stores need to be populated, happens in OnResolutionFrozen which uses _localDbExists to + // figure out whether it can read the databases or it should populate them from sql + + _logger.Info("Creating the content store, localContentDbExists? {LocalContentDbExists}", _localContentDbExists); + _contentStore = new ContentStore(PublishedSnapshotAccessor, VariationContextAccessor, _logger, _localContentDb); + _logger.Info("Creating the media store, localMediaDbExists? {LocalMediaDbExists}", _localMediaDbExists); + _mediaStore = new ContentStore(PublishedSnapshotAccessor, VariationContextAccessor, _logger, _localMediaDb); + } + else + { + _logger.Info("Creating the content store (local db ignored)"); + _contentStore = new ContentStore(PublishedSnapshotAccessor, VariationContextAccessor, _logger); + _logger.Info("Creating the media store (local db ignored)"); + _mediaStore = new ContentStore(PublishedSnapshotAccessor, VariationContextAccessor, _logger); + } + + _domainStore = new SnapDictionary(); + + SyncBootState bootState = _syncBootStateAccessor.GetSyncBootState(); + + var okContent = false; + var okMedia = false; + + try + { + if (bootState != SyncBootState.ColdBoot && _localContentDbExists) + { + okContent = LockAndLoadContent(scope => LoadContentFromLocalDbLocked(true)); + if (!okContent) + _logger.Warn("Loading content from local db raised warnings, will reload from database."); + } + + if (bootState != SyncBootState.ColdBoot && _localMediaDbExists) + { + okMedia = LockAndLoadMedia(scope => LoadMediaFromLocalDbLocked(true)); + if (!okMedia) + _logger.Warn("Loading media from local db raised warnings, will reload from database."); + } + + if (!okContent) + LockAndLoadContent(scope => LoadContentFromDatabaseLocked(scope, true)); + + if (!okMedia) + LockAndLoadMedia(scope => LoadMediaFromDatabaseLocked(scope, true)); + + LockAndLoadDomains(); + } + catch (Exception ex) + { + _logger.Fatal(ex, "Panic, exception while loading cache data."); + throw; + } + + // finally, cache is ready! + return true; } - - if (bootState != SyncBootState.ColdBoot && _localMediaDbExists) - { - okMedia = LockAndLoadMedia(scope => LoadMediaFromLocalDbLocked(true)); - if (!okMedia) - _logger.Warn("Loading media from local db raised warnings, will reload from database."); - } - - if (!okContent) - LockAndLoadContent(scope => LoadContentFromDatabaseLocked(scope, true)); - - if (!okMedia) - LockAndLoadMedia(scope => LoadMediaFromDatabaseLocked(scope, true)); - - LockAndLoadDomains(); - } - catch (Exception ex) - { - _logger.Fatal(ex, "Panic, exception while loading cache data."); - throw; - } - - // finally, cache is ready! - _isReady = true; - } + }); private void InitializeRepositoryEvents() { @@ -1146,9 +1157,13 @@ namespace Umbraco.Web.PublishedCache.NuCache public override IPublishedSnapshot CreatePublishedSnapshot(string previewToken) { + EnsureCaches(); + // no cache, no joy - if (_isReady == false) + if (Volatile.Read(ref _isReady) == false) + { throw new InvalidOperationException("The published snapshot service has not properly initialized."); + } var preview = previewToken.IsNullOrWhiteSpace() == false; return new PublishedSnapshot(this, preview); @@ -1159,6 +1174,8 @@ namespace Umbraco.Web.PublishedCache.NuCache // even though the underlying elements may not change (store snapshots) public PublishedSnapshot.PublishedSnapshotElements GetElements(bool previewDefault) { + EnsureCaches(); + // note: using ObjectCacheAppCache for elements and snapshot caches // is not recommended because it creates an inner MemoryCache which is a heavy // thing - better use a dictionary-based cache which "just" creates a concurrent @@ -1805,6 +1822,8 @@ AND cmsContentNu.nodeId IS NULL public void Collect() { + EnsureCaches(); + var contentCollect = _contentStore.CollectAsync(); var mediaCollect = _mediaStore.CollectAsync(); System.Threading.Tasks.Task.WaitAll(contentCollect, mediaCollect); @@ -1814,8 +1833,17 @@ AND cmsContentNu.nodeId IS NULL #region Internals/Testing - internal ContentStore GetContentStore() => _contentStore; - internal ContentStore GetMediaStore() => _mediaStore; + internal ContentStore GetContentStore() + { + EnsureCaches(); + return _contentStore; + } + + internal ContentStore GetMediaStore() + { + EnsureCaches(); + return _mediaStore; + } #endregion } diff --git a/src/Umbraco.Web/Search/ExamineFinalComponent.cs b/src/Umbraco.Web/Search/ExamineFinalComponent.cs index 335d19baf0..6dd775ab64 100644 --- a/src/Umbraco.Web/Search/ExamineFinalComponent.cs +++ b/src/Umbraco.Web/Search/ExamineFinalComponent.cs @@ -29,26 +29,10 @@ namespace Umbraco.Web.Search _indexRebuilder = indexRebuilder; _mainDom = mainDom; _syncBootStateAccessor = syncBootStateAccessor; - - // must add the handler in the ctor because it will be too late in Initialize - // TODO: All of this boot synchronization for cold boot logic needs should be fixed in netcore - _syncBootStateAccessor.Booting += _syncBootStateAccessor_Booting; - } - - /// - /// Once the boot state is known we can see if we require rebuilds - /// - /// - /// - private void _syncBootStateAccessor_Booting(object sender, SyncBootState e) - { - UmbracoModule.RouteAttempt += UmbracoModule_RouteAttempt; } private void UmbracoModule_RouteAttempt(object sender, RoutableAttemptEventArgs e) { - UmbracoModule.RouteAttempt -= UmbracoModule_RouteAttempt; - if (!_initialized) { lock (_locker) @@ -58,6 +42,8 @@ namespace Umbraco.Web.Search { _initialized = true; + UmbracoModule.RouteAttempt -= UmbracoModule_RouteAttempt; + if (!_mainDom.IsMainDom) return; var bootState = _syncBootStateAccessor.GetSyncBootState(); @@ -74,12 +60,12 @@ namespace Umbraco.Web.Search } public void Initialize() - { + { + UmbracoModule.RouteAttempt += UmbracoModule_RouteAttempt; } public void Terminate() { - _syncBootStateAccessor.Booting -= _syncBootStateAccessor_Booting; } } } From eba3c82a86ceea384a4a063d7b5bb50edf5ad6ed Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 24 May 2021 12:51:40 -0700 Subject: [PATCH 174/289] Changes startup logic, no more "Boot" method, the component just handles events --- .../Sync/DatabaseServerMessenger.cs | 16 +++------ .../BatchedDatabaseServerMessenger.cs | 33 ++++++++----------- ...aseServerRegistrarAndMessengerComponent.cs | 12 ++++--- 3 files changed, 26 insertions(+), 35 deletions(-) diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index 2b52a0948e..75ccf5e4f9 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -111,17 +111,11 @@ namespace Umbraco.Core.Sync #region Sync - /// - /// Boots the messenger. - /// - /// - /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded. - /// Callers MUST ensure thread-safety. - /// + [Obsolete("This is no longer used and will be removed in future versions")] protected void Boot() { - var bootState = GetSyncBootState(); - Booting?.Invoke(this, bootState); + // if called, just forces the boot logic + _ = GetSyncBootState(); } private SyncBootState BootInternal() @@ -545,8 +539,6 @@ namespace Umbraco.Core.Sync + "/D" + AppDomain.CurrentDomain.Id // eg 22 + "] " + Guid.NewGuid().ToString("N").ToUpper(); // make it truly unique - public event EventHandler Booting; - private string GetDistCacheFilePath(IGlobalSettings globalSettings) { var fileName = HttpRuntime.AppDomainAppId.ReplaceNonAlphanumericChars(string.Empty) + "-lastsynced.txt"; @@ -565,7 +557,7 @@ namespace Umbraco.Core.Sync #endregion - public SyncBootState GetSyncBootState() => _getSyncBootState.Value; + public virtual SyncBootState GetSyncBootState() => _getSyncBootState.Value; #region Notify refreshers diff --git a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs index 6596fee245..f89f62eb3d 100644 --- a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs @@ -26,6 +26,7 @@ namespace Umbraco.Web public class BatchedDatabaseServerMessenger : DatabaseServerMessenger { private readonly IUmbracoDatabaseFactory _databaseFactory; + private readonly Lazy _syncBootState; [Obsolete("This overload should not be used, enableDistCalls has no effect")] [EditorBrowsable(EditorBrowsableState.Never)] @@ -39,28 +40,22 @@ namespace Umbraco.Web : base(runtime, scopeProvider, sqlContext, proflog, globalSettings, true, options) { _databaseFactory = databaseFactory; - } - - // invoked by DatabaseServerRegistrarAndMessengerComponent - internal void Startup() - { - UmbracoModule.EndRequest += UmbracoModule_EndRequest; - - if (_databaseFactory.CanConnect == false) + _syncBootState = new Lazy(() => { - Logger.Warn("Cannot connect to the database, distributed calls will not be enabled for this server."); - } - else - { - Boot(); - } + if (_databaseFactory.CanConnect == false) + { + Logger.Warn("Cannot connect to the database, distributed calls will not be enabled for this server."); + return SyncBootState.Unknown; + } + else + { + return base.GetSyncBootState(); + } + }); } - private void UmbracoModule_EndRequest(object sender, UmbracoRequestEventArgs e) - { - // will clear the batch - will remain in HttpContext though - that's ok - FlushBatch(); - } + // override to deal with database connectivity + public override SyncBootState GetSyncBootState() => _syncBootState.Value; protected override void DeliverRemote(ICacheRefresher refresher, MessageType messageType, IEnumerable ids = null, string json = null) { diff --git a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs index ef13e166a5..d696ae5527 100644 --- a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs +++ b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs @@ -59,15 +59,19 @@ namespace Umbraco.Web.Compose if (_registrar != null || _messenger != null) { UmbracoModule.RouteAttempt += RegisterBackgroundTasksOnce; - } - - // must come last, as it references some _variables - _messenger?.Startup(); + UmbracoModule.EndRequest += UmbracoModule_EndRequest; + } } public void Terminate() { } + private void UmbracoModule_EndRequest(object sender, UmbracoRequestEventArgs e) + { + // will clear the batch - will remain in HttpContext though - that's ok + _messenger?.FlushBatch(); + } + /// /// Handle when a request is made /// From ecdc687e39846908f44552d3c65de2dbc6cbafa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 25 May 2021 15:02:55 +0200 Subject: [PATCH 175/289] use label --- .../infiniteeditors/mediaentryeditor/mediaentryeditor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html index afa3451899..f4302551ab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html @@ -35,7 +35,7 @@ width="{{crop.width}}" max-size="75"> - {{crop.alias}} + {{crop.label}} From a68a6b2ac29ea4eab94b92101c301ce60af979d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 25 May 2021 14:58:07 +0200 Subject: [PATCH 176/289] added windowResizeListener --- .../directives/components/imaging/umbimagecrop.directive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js index 744e4280db..60ba71d7a5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js @@ -26,7 +26,7 @@ angular.module("umbraco.directives") forceUpdate: '@?' }, - link: function (scope, element, attrs) { + link: function (scope, element, attrs, windowResizeListener) { var unsubscribe = []; let sliderRef = null; From 630fa6a98906fa005d8eb796255594e97e6d2058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 25 May 2021 14:57:19 +0200 Subject: [PATCH 177/289] dont remove outline --- .../src/less/components/umb-range-slider.less | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less index cc5c17ba70..6c1980a6e4 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less @@ -19,7 +19,6 @@ padding: 2px 6px; } .umb-range-slider .noUi-handle { - outline: none; cursor: grab; border-radius: 100px; border: none; From 0a9c1c7c8ebc899525ac5b0333be39704419ded9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 26 May 2021 13:08:25 +0200 Subject: [PATCH 178/289] Complex validation for RTE property editor (#10328) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Niels Lyngsø --- .../services/contenteditinghelper.service.js | 2 -- .../src/common/services/tinymce.service.js | 36 ++++++++++++------- .../propertyeditors/rte/rte.controller.js | 36 +++++++++---------- .../src/views/propertyeditors/rte/rte.html | 10 +++--- 4 files changed, 48 insertions(+), 36 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index 67e466af35..faa0d2a3c0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -48,8 +48,6 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt }); } else if (value[0]) { - //notificationsService.error("Validation", value[0]); - console.log({type:messageType, header:"Validation", message:value[0]}) notificationsService.showNotification({type:messageType, header:"Validation", message:value[0]}) } } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index 1d7154288f..9b5d240776 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -436,7 +436,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s // We are not ready to limit the pasted elements further than default, we will return to this feature. ( TODO: Make this feature an option. ) // We keep spans here, cause removing spans here also removes b-tags inside of them, instead we strip them out later. (TODO: move this definition to the config file... ) var validPasteElements = "-strong/b,-em/i,-u,-span,-p,-ol,-ul,-li,-p/div,-a[href|name],sub,sup,strike,br,del,table[width],tr,td[colspan|rowspan|width],th[colspan|rowspan|width],thead,tfoot,tbody,img[src|alt|width|height],ul,ol,li,hr,pre,dl,dt,figure,figcaption,wbr" - + // add elements from user configurated styleFormats to our list of validPasteElements. // (This means that we only allow H3-element if its configured as a styleFormat on this specific propertyEditor.) var style, i = 0; @@ -605,7 +605,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s 'contenteditable': false }, embed.preview); - + // Only replace if activeElement is an Embed element. if (activeElement && activeElement.nodeName.toUpperCase() === "DIV" && activeElement.classList.contains("embeditem")){ activeElement.replaceWith(wrapper); // directly replaces the html node @@ -733,9 +733,9 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s id: "__mcenew", "data-udi": img.udi }; - + editor.selection.setContent(editor.dom.createHTML('img', data)); - + // Using settimeout to wait for a DoM-render, so we can find the new element by ID. $timeout(function () { @@ -756,7 +756,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s } }); - + } } }, @@ -1396,11 +1396,26 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s function syncContent() { + if(args.model.value === args.editor.getContent()) { + return; + } + //stop watching before we update the value stopWatch(); angularHelper.safeApply($rootScope, function () { args.model.value = args.editor.getContent(); + + //make the form dirty manually so that the track changes works, setting our model doesn't trigger + // the angular bits because tinymce replaces the textarea. + if (args.currentForm) { + args.currentForm.$setDirty(); + } + // With complex validation we need to set a input field to dirty, not the form. but we will keep the old code for backwards compatibility. + if (args.currentFormInput) { + args.currentFormInput.$setDirty(); + } }); + //re-watch the value startWatch(); } @@ -1425,7 +1440,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s // Upload BLOB images (dragged/pasted ones) // find src attribute where value starts with `blob:` - // search is case-insensitive and allows single or double quotes + // search is case-insensitive and allows single or double quotes if(content.search(/src=["']blob:.*?["']/gi) !== -1){ args.editor.uploadImages(function(data) { // Once all images have been uploaded @@ -1491,6 +1506,9 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s args.editor.on('Change', function (e) { syncContent(); }); + args.editor.on('Keyup', function (e) { + syncContent(); + }); //when we leave the editor (maybe) args.editor.on('blur', function (e) { @@ -1508,12 +1526,6 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s args.editor.on('Dirty', function (e) { syncContent(); // Set model.value to the RTE's content - - //make the form dirty manually so that the track changes works, setting our model doesn't trigger - // the angular bits because tinymce replaces the textarea. - if (args.currentForm) { - args.currentForm.$setDirty(); - } }); let self = this; 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 74a70118eb..9faa012a4d 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 @@ -5,12 +5,12 @@ angular.module("umbraco") // TODO: A lot of the code below should be shared between the grid rte and the normal rte $scope.isLoading = true; - + //To id the html textarea we need to use the datetime ticks because we can have multiple rte's per a single property alias // because now we have to support having 2x (maybe more at some stage) content editors being displayed at once. This is because // we have this mini content editor panel that can be launched with MNTP. $scope.textAreaHtmlId = $scope.model.alias + "_" + String.CreateGuid(); - + var editorConfig = $scope.model.config ? $scope.model.config.editor : null; if (!editorConfig || Utilities.isString(editorConfig)) { editorConfig = tinyMceService.defaultPrevalues(); @@ -28,14 +28,14 @@ angular.module("umbraco") $scope.containerOverflow = editorConfig.mode === "distraction-free" ? (height ? "auto" : "inherit") : "inherit"; var promises = []; - + // we need to make sure that the element is initialized before we can init TinyMCE, because we find the placeholder by ID, so it needs to be appended to document before. var initPromise = $q((resolve, reject) => { this.$onInit = resolve; }); - + promises.push(initPromise); - + //queue file loading tinyMceAssets.forEach(function (tinyJsAsset) { promises.push(assetsService.loadJs(tinyJsAsset, $scope)); @@ -50,50 +50,50 @@ angular.module("umbraco") toolbar: editorConfig.toolbar, mode: editorConfig.mode })); - + //wait for queue to end $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, width: width, height: height }; - + baseLineConfigObj.setup = function (editor) { - + //set the reference tinyMceEditor = editor; - + tinyMceEditor.on('init', function (e) { $timeout(function () { $scope.isLoading = false; }); }); - + //initialize the standard editor functionality for Umbraco tinyMceService.initializeEditor({ editor: editor, model: $scope.model, - currentForm: angularHelper.getCurrentForm($scope) + currentFormInput: $scope.rteForm.modelValue }); - + }; - + angular.extend(baseLineConfigObj, standardConfig); - + // We need to wait for DOM to have rendered before we can find the element by ID. $timeout(function () { tinymce.init(baseLineConfigObj); }, 150); - + //listen for formSubmitting event (the result is callback used to remove the event subscription) var unsubscribe = $scope.$on("formSubmitting", function () { if (tinyMceEditor !== undefined && tinyMceEditor != null && !$scope.isLoading) { 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 d0b5823f74..21e9065d92 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,8 +1,10 @@
    -
    - -
    -
    + +
    + +
    +
    +
    From 1ac44542f6542a916537d0605881e1e7e3fe9b84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 26 May 2021 13:08:25 +0200 Subject: [PATCH 179/289] Complex validation for RTE property editor (#10328) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Niels Lyngsø (cherry picked from commit 0a9c1c7c8ebc899525ac5b0333be39704419ded9) # Conflicts: # src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js --- .../src/common/services/tinymce.service.js | 36 ++++++++++++------- .../propertyeditors/rte/rte.controller.js | 36 +++++++++---------- .../src/views/propertyeditors/rte/rte.html | 10 +++--- 3 files changed, 48 insertions(+), 34 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index 1d7154288f..9b5d240776 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -436,7 +436,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s // We are not ready to limit the pasted elements further than default, we will return to this feature. ( TODO: Make this feature an option. ) // We keep spans here, cause removing spans here also removes b-tags inside of them, instead we strip them out later. (TODO: move this definition to the config file... ) var validPasteElements = "-strong/b,-em/i,-u,-span,-p,-ol,-ul,-li,-p/div,-a[href|name],sub,sup,strike,br,del,table[width],tr,td[colspan|rowspan|width],th[colspan|rowspan|width],thead,tfoot,tbody,img[src|alt|width|height],ul,ol,li,hr,pre,dl,dt,figure,figcaption,wbr" - + // add elements from user configurated styleFormats to our list of validPasteElements. // (This means that we only allow H3-element if its configured as a styleFormat on this specific propertyEditor.) var style, i = 0; @@ -605,7 +605,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s 'contenteditable': false }, embed.preview); - + // Only replace if activeElement is an Embed element. if (activeElement && activeElement.nodeName.toUpperCase() === "DIV" && activeElement.classList.contains("embeditem")){ activeElement.replaceWith(wrapper); // directly replaces the html node @@ -733,9 +733,9 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s id: "__mcenew", "data-udi": img.udi }; - + editor.selection.setContent(editor.dom.createHTML('img', data)); - + // Using settimeout to wait for a DoM-render, so we can find the new element by ID. $timeout(function () { @@ -756,7 +756,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s } }); - + } } }, @@ -1396,11 +1396,26 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s function syncContent() { + if(args.model.value === args.editor.getContent()) { + return; + } + //stop watching before we update the value stopWatch(); angularHelper.safeApply($rootScope, function () { args.model.value = args.editor.getContent(); + + //make the form dirty manually so that the track changes works, setting our model doesn't trigger + // the angular bits because tinymce replaces the textarea. + if (args.currentForm) { + args.currentForm.$setDirty(); + } + // With complex validation we need to set a input field to dirty, not the form. but we will keep the old code for backwards compatibility. + if (args.currentFormInput) { + args.currentFormInput.$setDirty(); + } }); + //re-watch the value startWatch(); } @@ -1425,7 +1440,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s // Upload BLOB images (dragged/pasted ones) // find src attribute where value starts with `blob:` - // search is case-insensitive and allows single or double quotes + // search is case-insensitive and allows single or double quotes if(content.search(/src=["']blob:.*?["']/gi) !== -1){ args.editor.uploadImages(function(data) { // Once all images have been uploaded @@ -1491,6 +1506,9 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s args.editor.on('Change', function (e) { syncContent(); }); + args.editor.on('Keyup', function (e) { + syncContent(); + }); //when we leave the editor (maybe) args.editor.on('blur', function (e) { @@ -1508,12 +1526,6 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s args.editor.on('Dirty', function (e) { syncContent(); // Set model.value to the RTE's content - - //make the form dirty manually so that the track changes works, setting our model doesn't trigger - // the angular bits because tinymce replaces the textarea. - if (args.currentForm) { - args.currentForm.$setDirty(); - } }); let self = this; 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 74a70118eb..9faa012a4d 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 @@ -5,12 +5,12 @@ angular.module("umbraco") // TODO: A lot of the code below should be shared between the grid rte and the normal rte $scope.isLoading = true; - + //To id the html textarea we need to use the datetime ticks because we can have multiple rte's per a single property alias // because now we have to support having 2x (maybe more at some stage) content editors being displayed at once. This is because // we have this mini content editor panel that can be launched with MNTP. $scope.textAreaHtmlId = $scope.model.alias + "_" + String.CreateGuid(); - + var editorConfig = $scope.model.config ? $scope.model.config.editor : null; if (!editorConfig || Utilities.isString(editorConfig)) { editorConfig = tinyMceService.defaultPrevalues(); @@ -28,14 +28,14 @@ angular.module("umbraco") $scope.containerOverflow = editorConfig.mode === "distraction-free" ? (height ? "auto" : "inherit") : "inherit"; var promises = []; - + // we need to make sure that the element is initialized before we can init TinyMCE, because we find the placeholder by ID, so it needs to be appended to document before. var initPromise = $q((resolve, reject) => { this.$onInit = resolve; }); - + promises.push(initPromise); - + //queue file loading tinyMceAssets.forEach(function (tinyJsAsset) { promises.push(assetsService.loadJs(tinyJsAsset, $scope)); @@ -50,50 +50,50 @@ angular.module("umbraco") toolbar: editorConfig.toolbar, mode: editorConfig.mode })); - + //wait for queue to end $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, width: width, height: height }; - + baseLineConfigObj.setup = function (editor) { - + //set the reference tinyMceEditor = editor; - + tinyMceEditor.on('init', function (e) { $timeout(function () { $scope.isLoading = false; }); }); - + //initialize the standard editor functionality for Umbraco tinyMceService.initializeEditor({ editor: editor, model: $scope.model, - currentForm: angularHelper.getCurrentForm($scope) + currentFormInput: $scope.rteForm.modelValue }); - + }; - + angular.extend(baseLineConfigObj, standardConfig); - + // We need to wait for DOM to have rendered before we can find the element by ID. $timeout(function () { tinymce.init(baseLineConfigObj); }, 150); - + //listen for formSubmitting event (the result is callback used to remove the event subscription) var unsubscribe = $scope.$on("formSubmitting", function () { if (tinyMceEditor !== undefined && tinyMceEditor != null && !$scope.isLoading) { 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 d0b5823f74..21e9065d92 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,8 +1,10 @@
    -
    - -
    -
    + +
    + +
    +
    +
    From 008e76c08f72a2e009e3518f13c2896b3677d7e7 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 26 May 2021 11:42:06 +0200 Subject: [PATCH 180/289] fixes #10297 8.14-RC - A few areas where focus lock doesn't work properly --- .../components/editor/umbeditors.directive.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js index 231ee4e866..590f9627ab 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js @@ -12,6 +12,11 @@ var isLeftColumnAbove = false; scope.editors = []; + /* we need to keep a count of open editors because the length of the editors array is first changed when animations are done + we do this because some infinite editors close more than one editor at the time and we get the wrong count from editors.length + because of the animation */ + let editorCount = 0; + function addEditor(editor) { editor.inFront = true; editor.moveRight = true; @@ -51,13 +56,16 @@ updateEditors(-1); - if(scope.editors.length === 1){ + if(scope.editors.length === 1) { if(isLeftColumnAbove){ $('#leftcolumn').addClass(aboveBackDropCssClass); } isLeftColumnAbove = false; + } + // when the last editor is closed remove the focus lock + if (editorCount === 0) { // Remove the inert attribute from the #mainwrapper focusLockService.removeInertAttribute(); } @@ -105,16 +113,19 @@ } evts.push(eventsService.on("appState.editors.open", function (name, args) { + editorCount = editorCount + 1; addEditor(args.editor); })); evts.push(eventsService.on("appState.editors.close", function (name, args) { // remove the closed editor if (args && args.editor) { + editorCount = editorCount - 1; removeEditor(args.editor); } // close all editors if (args && !args.editor && args.editors.length === 0) { + editorCount = 0; scope.editors = []; } })); From 9ef806d4e9544173b185c4702644ae7233eca5b5 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 26 May 2021 15:13:25 +0100 Subject: [PATCH 181/289] Rename Media Picker 3 and make current Media Picker Obsolete (#10332) --- .../Migrations/Install/DatabaseDataCreator.cs | 15 +++++++-------- src/Umbraco.Tests/Services/ContentServiceTests.cs | 2 +- .../TestHelpers/Entities/MockedContent.cs | 2 +- .../TestHelpers/Entities/MockedContentTypes.cs | 2 +- .../PropertyEditors/MediaPicker3PropertyEditor.cs | 2 +- .../PropertyEditors/MediaPickerPropertyEditor.cs | 6 ++++-- 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 264733e5b9..6a56141491 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -141,14 +141,14 @@ namespace Umbraco.Core.Migrations.Install //New UDI pickers with newer Ids _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1046, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1046", SortOrder = 2, UniqueId = new Guid("FD1E0DA5-5606-4862-B679-5D0CF3A52A59"), Text = "Content Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1047, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1047", SortOrder = 2, UniqueId = new Guid("1EA2E01F-EBD8-4CE1-8D71-6B1149E63548"), Text = "Member Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "Media Picker (old)", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = new Guid("9DBBCBBB-2327-434A-B355-AF1B84E5010A"), Text = "Multiple Media Picker (old)", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "(Obsolete) Media Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = new Guid("9DBBCBBB-2327-434A-B355-AF1B84E5010A"), Text = "(Obsolete) Multiple Media Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1050, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1050", SortOrder = 2, UniqueId = new Guid("B4E3535A-1753-47E2-8568-602CF8CFEE6F"), Text = "Multi URL Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1051, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1051", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3Guid, Text = "Media Picker 3", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1052, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1052", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3MultipleGuid, Text = "Multiple Media Picker 3", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1053, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1053", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3SingleImageGuid, Text = "Image Media Picker 3", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1054, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1054", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3MultipleImagesGuid, Text = "Multiple Image Media Picker 3", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1051, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1051", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3Guid, Text = "Media Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1052, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1052", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3MultipleGuid, Text = "Multiple Media Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1053, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1053", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3SingleImageGuid, Text = "Image Media Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1054, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1054", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3MultipleImagesGuid, Text = "Multiple Image Media Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); } @@ -350,8 +350,7 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1046, EditorAlias = Constants.PropertyEditors.Aliases.ContentPicker, DbType = "Nvarchar" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1047, EditorAlias = Constants.PropertyEditors.Aliases.MemberPicker, DbType = "Nvarchar" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1048, EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker, DbType = "Ntext" }); - _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1049, EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker, DbType = "Ntext", - Configuration = "{\"multiPicker\":1}" }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1049, EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker, DbType = "Ntext", Configuration = "{\"multiPicker\":1}" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1050, EditorAlias = Constants.PropertyEditors.Aliases.MultiUrlPicker, DbType = "Ntext" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 008c24fcbf..d9a90b74d8 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -2470,7 +2470,7 @@ namespace Umbraco.Tests.Services Assert.That(sut.GetValue("ddl"), Is.EqualTo("1234")); Assert.That(sut.GetValue("chklist"), Is.EqualTo("randomc")); Assert.That(sut.GetValue("contentPicker"), Is.EqualTo(Udi.Create(Constants.UdiEntityType.Document, new Guid("74ECA1D4-934E-436A-A7C7-36CC16D4095C")))); - Assert.That(sut.GetValue("mediaPicker"), Is.EqualTo(Udi.Create(Constants.UdiEntityType.Media, new Guid("44CB39C8-01E5-45EB-9CF8-E70AAF2D1691")))); + Assert.That(sut.GetValue("mediapicker3"), Is.EqualTo("[{\"key\": \"8f78ce9e-8fe0-4500-a52d-4c4f35566ba9\",\"mediaKey\": \"44CB39C8-01E5-45EB-9CF8-E70AAF2D1691\",\"crops\": [],\"focalPoint\": {\"left\": 0.5,\"top\": 0.5}}]")); Assert.That(sut.GetValue("memberPicker"), Is.EqualTo(Udi.Create(Constants.UdiEntityType.Member, new Guid("9A50A448-59C0-4D42-8F93-4F1D55B0F47D")))); Assert.That(sut.GetValue("multiUrlPicker"), Is.EqualTo("[{\"name\":\"https://test.com\",\"url\":\"https://test.com\"}]")); Assert.That(sut.GetValue("tags"), Is.EqualTo("this,is,tags")); diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs index 19a57d7775..d8902d2d62 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs @@ -130,7 +130,7 @@ namespace Umbraco.Tests.TestHelpers.Entities content.SetValue("ddl", "1234"); content.SetValue("chklist", "randomc"); content.SetValue("contentPicker", Udi.Create(Constants.UdiEntityType.Document, new Guid("74ECA1D4-934E-436A-A7C7-36CC16D4095C")).ToString()); - content.SetValue("mediaPicker", Udi.Create(Constants.UdiEntityType.Media, new Guid("44CB39C8-01E5-45EB-9CF8-E70AAF2D1691")).ToString()); + content.SetValue("mediaPicker3", "[{\"key\": \"8f78ce9e-8fe0-4500-a52d-4c4f35566ba9\",\"mediaKey\": \"44CB39C8-01E5-45EB-9CF8-E70AAF2D1691\",\"crops\": [],\"focalPoint\": {\"left\": 0.5,\"top\": 0.5}}]"); content.SetValue("memberPicker", Udi.Create(Constants.UdiEntityType.Member, new Guid("9A50A448-59C0-4D42-8F93-4F1D55B0F47D")).ToString()); content.SetValue("multiUrlPicker", "[{\"name\":\"https://test.com\",\"url\":\"https://test.com\"}]"); content.SetValue("tags", "this,is,tags"); diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs index 1b85787fee..9271486f2b 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs @@ -368,7 +368,7 @@ namespace Umbraco.Tests.TestHelpers.Entities contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.DropDownListFlexible, ValueStorageType.Integer) { Alias = "ddl", Name = "Dropdown List", Mandatory = false, SortOrder = 14, DataTypeId = -42 }); contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.CheckBoxList, ValueStorageType.Nvarchar) { Alias = "chklist", Name = "Checkbox List", Mandatory = false, SortOrder = 15, DataTypeId = -43 }); contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.ContentPicker, ValueStorageType.Integer) { Alias = "contentPicker", Name = "Content Picker", Mandatory = false, SortOrder = 16, DataTypeId = 1046 }); - contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.MediaPicker, ValueStorageType.Integer) { Alias = "mediaPicker", Name = "Media Picker", Mandatory = false, SortOrder = 17, DataTypeId = 1048 }); + contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.MediaPicker3, ValueStorageType.Integer) { Alias = "mediapicker3", Name = "Media Picker", Mandatory = false, SortOrder = 17, DataTypeId = 1051 }); contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.MemberPicker, ValueStorageType.Integer) { Alias = "memberPicker", Name = "Member Picker", Mandatory = false, SortOrder = 18, DataTypeId = 1047 }); contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.MultiUrlPicker, ValueStorageType.Nvarchar) { Alias = "multiUrlPicker", Name = "Multi URL Picker", Mandatory = false, SortOrder = 21, DataTypeId = 1050 }); contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.Tags, ValueStorageType.Ntext) { Alias = "tags", Name = "Tags", Mandatory = false, SortOrder = 22, DataTypeId = 1041 }); diff --git a/src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs index 526b4830c8..4ce376f543 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs @@ -14,7 +14,7 @@ namespace Umbraco.Web.PropertyEditors [DataEditor( Constants.PropertyEditors.Aliases.MediaPicker3, EditorType.PropertyValue, - "Media Picker v3", + "Media Picker", "mediapicker3", ValueType = ValueTypes.Json, Group = Constants.PropertyEditors.Groups.Media, diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs index 493fc96f76..499737f3b7 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs @@ -8,15 +8,17 @@ namespace Umbraco.Web.PropertyEditors { /// /// Represents a media picker property editor. + /// Marked as Deprecated as best to use the NEW Media Picker aka MediaPicker3 /// [DataEditor( Constants.PropertyEditors.Aliases.MediaPicker, EditorType.PropertyValue | EditorType.MacroParameter, - "Media Picker", + "(Obsolete) Media Picker", "mediapicker", ValueType = ValueTypes.Text, Group = Constants.PropertyEditors.Groups.Media, - Icon = Constants.Icons.MediaImage)] + Icon = Constants.Icons.MediaImage, + IsDeprecated = true)] public class MediaPickerPropertyEditor : DataEditor { From 38f5d8329dc4722aa32c55a9ea77b027765bbda0 Mon Sep 17 00:00:00 2001 From: Vlael Layug Date: Fri, 21 May 2021 23:55:04 +0800 Subject: [PATCH 182/289] added cdata on sectionMandatoryDesc for other languages --- src/Umbraco.Web.UI/Umbraco/config/lang/cy.xml | 4 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 4 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/de.xml | 4 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 4 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 4 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/es.xml | 4 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml | 4 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml | 4 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml | 4 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/cy.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/cy.xml index 274b7ef5b0..6f52c24034 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/cy.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/cy.xml @@ -1763,9 +1763,9 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang Enw Adran Mae Adran yn ofynnol - + @section, fel arall bydd gwall yn cael ei ddangos. - + ]]> Adeiladwr ymholiad Adeiladu ymholiad o eitemau wedi dychwelyd, mewn diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 8e33f102c7..b2b11fdef5 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1328,9 +1328,9 @@ Mange hilsner fra Umbraco robotten ]]> Sektionsnavn Sektionen er obligatorisk - + @section -definition. - + ]]> Query builder sider returneret, på Returner diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml index e417da263c..c4ff0812bd 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml @@ -1557,10 +1557,10 @@ Bereichsname Bereich ist notwendig - + @section Definition gleichen Namens enthalten, anderfalls tritt ein Fehler auf. - + ]]> Abfrage-Generator zurückgegebene Elemente, in Ich möchte diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index c76747c5a0..6ea1bd95af 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1582,9 +1582,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont ]]> Section Name Section is mandatory - + @section definition, otherwise an error is shown. - + ]]> Query builder items returned, in copy to clipboard diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index ba7586dde2..a6cd3d81a8 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1600,9 +1600,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont ]]> Section Name Section is mandatory - + @section definition, otherwise an error is shown. - + ]]> Query builder items returned, in copy to clipboard diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/es.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/es.xml index 96a6ec2ec0..c2b7468523 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/es.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/es.xml @@ -1113,9 +1113,9 @@ ]]> Nombre de sección Sección es obligatoria - + @section o se mostrará un error. - + ]]> Constructor de consultas elementos devueltos, en Quiero diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml index 86684678c9..d40e3b35d7 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml @@ -1527,9 +1527,9 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à ]]> Nom de la section La section est obligatoire - + @section, sinon une erreur est affichée. - + ]]> Générateur de requêtes éléments trouvés, en copier dans le clipboard diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml index 6a138fcb6b..4ac1a9ca04 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml @@ -1095,9 +1095,9 @@ Naciśnij przycisk instaluj, aby zainstalować bazę danych Umb ]]> Nazwa Sekcji Sekcja jest wymagana - + @section, w przeciwnym przypadku wystąpi błąd. - + ]]> Konstruktor zapytań Element zwrócony, w Chcę diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml index 77860523cd..ffc088522d 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml @@ -1490,9 +1490,9 @@ ]]> Название секции Секция обязательна - + @section, в противном случае генерируется ошибка. - + ]]> Генератор запросов элементов в результате, за Мне нужны diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml index bb73d43a3e..d59cf248fc 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml @@ -1622,9 +1622,9 @@ Web sitenizi yönetmek için, Umbraco'nun arka ofisini açın ve içerik eklemey Bölüm Adı Bölüm zorunludur - + @section tanımı içermelidir, aksi takdirde bir hata gösterilir. - + ]]> Sorgu oluşturucu öğe iade edildi, panoya kopyala From a845d2bffd76434e9af42b83ce820298cfe49f16 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 26 May 2021 16:18:10 +0200 Subject: [PATCH 183/289] Shows a warning for people tempted to switch to the new media picker --- .../src/views/prevalueeditors/obsoletemediapickernotice.html | 1 + src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/views/prevalueeditors/obsoletemediapickernotice.html diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/obsoletemediapickernotice.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/obsoletemediapickernotice.html new file mode 100644 index 0000000000..cc861dcb4b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/obsoletemediapickernotice.html @@ -0,0 +1 @@ +

    Important: switching from the (Obsolete) Media Picker to Media Picker will mean all data (references to previously selected media items) will be deleted and no longer available.

    \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs index b8b9476184..7266be9c26 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs @@ -8,6 +8,9 @@ namespace Umbraco.Web.PropertyEditors /// public class MediaPickerConfiguration : IIgnoreUserStartNodesConfig { + [ConfigurationField("notice", "You can NOT change the property editor", "obsoletemediapickernotice")] + public bool Notice { get; set; } + [ConfigurationField("multiPicker", "Pick multiple items", "boolean")] public bool Multiple { get; set; } From d81fac9753591415fd8418fdcdb89d5445957888 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 27 May 2021 10:42:13 +0200 Subject: [PATCH 184/289] Bump version to 8.14.0 --- src/SolutionInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 91918498f9..3619dc1371 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -19,4 +19,4 @@ using System.Resources; // these are FYI and changed automatically [assembly: AssemblyFileVersion("8.14.0")] -[assembly: AssemblyInformationalVersion("8.14.0-rc")] +[assembly: AssemblyInformationalVersion("8.14.0")] From 3440cbbac94bb29c500135a1e2c8093b341f397a Mon Sep 17 00:00:00 2001 From: patrickdemooij9 Date: Fri, 28 May 2021 01:14:07 +0200 Subject: [PATCH 185/289] Fixed 10144: Prevalue alias not set (#10163) * Fixed 10144: Prevalue alias not set * Remove console.log Co-authored-by: Nathan Woulfe --- .../src/common/services/datatypehelper.service.js | 5 +++-- .../datatypesettings/datatypesettings.controller.js | 3 ++- .../src/views/datatypes/datatype.edit.controller.js | 13 +------------ 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/datatypehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/datatypehelper.service.js index 3cde632d4b..f4317b51b7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/datatypehelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/datatypehelper.service.js @@ -18,7 +18,8 @@ function dataTypeHelper() { description: preVals[i].description, label: preVals[i].label, view: preVals[i].view, - value: preVals[i].value + value: preVals[i].value, + config: preVals[i].config }); } @@ -52,4 +53,4 @@ function dataTypeHelper() { return dataTypeHelperService; } -angular.module('umbraco.services').factory('dataTypeHelper', dataTypeHelper); \ No newline at end of file +angular.module('umbraco.services').factory('dataTypeHelper', dataTypeHelper); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.controller.js index 7faaddde77..4e1b3e868d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.controller.js @@ -76,7 +76,7 @@ // get pre values dataTypeResource.getPreValues(newDataType.selectedEditor).then(function(preValues) { - newDataType.preValues = preValues; + newDataType.preValues = dataTypeHelper.createPreValueProps(preValues); vm.dataType = newDataType; vm.loadingDataType = false; }); @@ -88,6 +88,7 @@ function getDataType() { vm.loadingDataType = true; dataTypeResource.getById($scope.model.id).then(function (dataType) { + dataType.preValues = dataTypeHelper.createPreValueProps(dataType.preValues); vm.dataType = dataType; vm.loadingDataType = false; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js index a2af8c8239..857351bb93 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js @@ -37,18 +37,7 @@ function DataTypeEditController($scope, $routeParams, appState, navigationServic //method used to configure the pre-values when we retrieve them from the server function createPreValueProps(preVals) { - vm.preValues = []; - for (var i = 0; i < preVals.length; i++) { - vm.preValues.push({ - hideLabel: preVals[i].hideLabel, - alias: preVals[i].key, - description: preVals[i].description, - label: preVals[i].label, - view: preVals[i].view, - value: preVals[i].value, - config: preVals[i].config - }); - } + vm.preValues = dataTypeHelper.createPreValueProps(preVals); } From 7cccbb3fbf7bc8c7be6ae3f01df0face2fa53d9a Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Fri, 28 May 2021 01:25:56 +0200 Subject: [PATCH 186/289] Replaced angular.extend with Utilities.extend (#10023) * Replace angular.extend with Utilities.extend * Replace more references of angular.extend with Utilities.extend * Replace angular.extend * Replace last reference of angular.extend except in Utilities itself * Use spread operator * Add spread operator * Use existing format with empty destination object * Configurate Babel to work with JS spread operator * Use default for loose property which is "false" * use alias for angular.extend - removes need for any other changes and makes multiple arguments easy Co-authored-by: Nathan Woulfe --- src/Umbraco.Web.UI.Client/.babelrc | 8 ++++++++ src/Umbraco.Web.UI.Client/package.json | 1 + .../directives/components/grid/grid.rte.directive.js | 2 +- .../directives/components/umbaceeditor.directive.js | 5 ++--- .../src/common/resources/content.resource.js | 2 +- .../src/common/resources/entity.resource.js | 4 ++-- .../src/common/resources/log.resource.js | 4 ++-- .../src/common/resources/logviewer.resource.js | 2 +- .../src/common/resources/media.resource.js | 4 ++-- .../src/common/resources/member.resource.js | 2 +- .../src/common/resources/relationtype.resource.js | 2 +- .../src/common/resources/users.resource.js | 2 +- .../src/common/services/keyboard.service.js | 2 +- .../common/services/searchresultformatter.service.js | 10 +++++----- .../src/common/services/tinymce.service.js | 7 +++---- src/Umbraco.Web.UI.Client/src/utilities.js | 3 ++- .../views/prevalueeditors/colorpicker.controller.js | 2 +- .../views/prevalueeditors/mediapicker.controller.js | 2 +- .../src/views/prevalueeditors/treepicker.controller.js | 2 +- .../contentpicker/contentpicker.controller.js | 4 ++-- .../datepicker/datepicker.controller.js | 2 +- .../dropdownFlexible/dropdownFlexible.controller.js | 2 +- .../src/views/propertyeditors/grid/grid.controller.js | 2 +- .../imagecropper/imagecropper.controller.js | 2 +- .../mediapicker/mediapicker.controller.js | 4 ++-- .../memberpicker/memberpicker.controller.js | 2 +- .../src/views/propertyeditors/rte/rte.controller.js | 4 ++-- 27 files changed, 48 insertions(+), 40 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/.babelrc b/src/Umbraco.Web.UI.Client/.babelrc index ae044dabd8..748cd6a810 100644 --- a/src/Umbraco.Web.UI.Client/.babelrc +++ b/src/Umbraco.Web.UI.Client/.babelrc @@ -6,5 +6,13 @@ "targets": "last 2 version, not dead, > 0.5%, not ie 11" } ] + ], + "plugins": [ + [ + "@babel/plugin-proposal-object-rest-spread", + { + "useBuiltIns": true + } + ] ] } diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 6514f2f217..045f788929 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -53,6 +53,7 @@ "devDependencies": { "@babel/core": "7.6.4", "@babel/preset-env": "7.6.3", + "@babel/plugin-proposal-object-rest-spread": "7.13.8", "autoprefixer": "9.6.5", "caniuse-lite": "^1.0.30001037", "cssnano": "4.1.10", diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js index e2b2e2c3df..7b690e5863 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js @@ -75,7 +75,7 @@ angular.module("umbraco.directives") maxImageSize: editorConfig.maxImageSize }; - angular.extend(baseLineConfigObj, standardConfig); + Utilities.extend(baseLineConfigObj, standardConfig); baseLineConfigObj.setup = function (editor) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbaceeditor.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbaceeditor.directive.js index 5e1f2489e6..ef463e6d95 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbaceeditor.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbaceeditor.directive.js @@ -146,8 +146,7 @@ * umbAceEditorConfig merged with user options via json in attribute or data binding * @type object */ - var opts = angular.extend({}, options, scope.umbAceEditor); - + var opts = Utilities.extend({}, options, scope.umbAceEditor); //load ace libraries here... @@ -273,7 +272,7 @@ return; } - opts = angular.extend({}, options, scope.umbAceEditor); + opts = Utilities.extend({}, options, scope.umbAceEditor); opts.callbacks = [opts.onLoad]; if (opts.onLoad !== options.onLoad) { diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index 368eab2339..b6e5183b30 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -776,7 +776,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; //change asc/desct diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index 838e8f1b80..0b060da34b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -439,7 +439,7 @@ function entityResource($q, $http, umbRequestHelper) { options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; //change asc/desct @@ -512,7 +512,7 @@ function entityResource($q, $http, umbRequestHelper) { options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; //change asc/desct diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js index bcdaddd22f..b1931af541 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js @@ -71,7 +71,7 @@ function logResource($q, $http, umbRequestHelper) { options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; @@ -140,7 +140,7 @@ function logResource($q, $http, umbRequestHelper) { options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/logviewer.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/logviewer.resource.js index d2d008640d..46ef8d2919 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/logviewer.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/logviewer.resource.js @@ -58,7 +58,7 @@ function logViewerResource($q, $http, umbRequestHelper) { } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js index 06eb2ed4d2..87ba054675 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js @@ -333,7 +333,7 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; //change asc/desct @@ -563,7 +563,7 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js index c45e173a98..05e746a69b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js @@ -40,7 +40,7 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; //change asc/desct diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js index 7c542c5e7b..96c7882b5a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js @@ -126,7 +126,7 @@ function relationTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index 91f00a36e3..0b69bec3f5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -226,7 +226,7 @@ options = {}; } //overwrite the defaults if there are any specified - angular.extend(defaults, options); + Utilities.extend(defaults, options); //now copy back to the options we will use options = defaults; //change asc/desct diff --git a/src/Umbraco.Web.UI.Client/src/common/services/keyboard.service.js b/src/Umbraco.Web.UI.Client/src/common/services/keyboard.service.js index 31375c5c56..b49e809eeb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/keyboard.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/keyboard.service.js @@ -264,7 +264,7 @@ function keyboardService($window, $timeout) { var elt; // Initialize opt object - opt = angular.extend({}, defaultOpt, opt); + opt = Utilities.extend(defaultOpt, opt); label = label.toLowerCase(); elt = opt.target; if(typeof opt.target === 'string'){ diff --git a/src/Umbraco.Web.UI.Client/src/common/services/searchresultformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/searchresultformatter.service.js index 9bf9f3762c..ca8fdc7fa7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/searchresultformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/searchresultformatter.service.js @@ -3,27 +3,27 @@ function searchResultFormatter(umbRequestHelper) { function configureDefaultResult(content, treeAlias, appAlias) { content.editorPath = appAlias + "/" + treeAlias + "/edit/" + content.id; - angular.extend(content.metaData, { treeAlias: treeAlias }); + Utilities.extend(content.metaData, { treeAlias: treeAlias }); } function configureContentResult(content, treeAlias, appAlias) { content.menuUrl = umbRequestHelper.getApiUrl("contentTreeBaseUrl", "GetMenu", [{ id: content.id }, { application: appAlias }]); content.editorPath = appAlias + "/" + treeAlias + "/edit/" + content.id; - angular.extend(content.metaData, { treeAlias: treeAlias }); + Utilities.extend(content.metaData, { treeAlias: treeAlias }); content.subTitle = content.metaData.Url; } function configureMemberResult(member, treeAlias, appAlias) { member.menuUrl = umbRequestHelper.getApiUrl("memberTreeBaseUrl", "GetMenu", [{ id: member.id }, { application: appAlias }]); member.editorPath = appAlias + "/" + treeAlias + "/edit/" + (member.key ? member.key : member.id); - angular.extend(member.metaData, { treeAlias: treeAlias }); + Utilities.extend(member.metaData, { treeAlias: treeAlias }); member.subTitle = member.metaData.Email; } function configureMediaResult(media, treeAlias, appAlias) { media.menuUrl = umbRequestHelper.getApiUrl("mediaTreeBaseUrl", "GetMenu", [{ id: media.id }, { application: appAlias }]); media.editorPath = appAlias + "/" + treeAlias + "/edit/" + media.id; - angular.extend(media.metaData, { treeAlias: treeAlias }); + Utilities.extend(media.metaData, { treeAlias: treeAlias }); } return { @@ -34,4 +34,4 @@ function searchResultFormatter(umbRequestHelper) { }; } -angular.module('umbraco.services').factory('searchResultFormatter', searchResultFormatter); \ No newline at end of file +angular.module('umbraco.services').factory('searchResultFormatter', searchResultFormatter); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index 9b5d240776..fbe342d44f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -463,8 +463,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s }; - angular.extend(config, pasteConfig); - + Utilities.extend(config, pasteConfig); if (tinyMceConfig.customConfig) { @@ -481,7 +480,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s //overwrite the baseline config item if it is an array, we want to concat the items in the array, otherwise //if it's an object it will overwrite the baseline if (Utilities.isArray(config[i]) && Utilities.isArray(tinyMceConfig.customConfig[i])) { - //concat it and below this concat'd array will overwrite the baseline in angular.extend + //concat it and below this concat'd array will overwrite the baseline in Utilities.extend tinyMceConfig.customConfig[i] = config[i].concat(tinyMceConfig.customConfig[i]); } } @@ -498,7 +497,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s } } - angular.extend(config, tinyMceConfig.customConfig); + Utilities.extend(config, tinyMceConfig.customConfig); } return config; diff --git a/src/Umbraco.Web.UI.Client/src/utilities.js b/src/Umbraco.Web.UI.Client/src/utilities.js index 01e18e4e1c..abbc287e0f 100644 --- a/src/Umbraco.Web.UI.Client/src/utilities.js +++ b/src/Umbraco.Web.UI.Client/src/utilities.js @@ -33,8 +33,9 @@ /** * Facade to angular.extend * Use this with Angular objects, for vanilla JS objects, use Object.assign() + * This is an alias as it to allow passing an unknown number of arguments */ - const extend = (dst, src) => angular.extend(dst, src); + const extend = angular.extend; /** * Equivalent to angular.isFunction diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/colorpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/colorpicker.controller.js index 9ad2c87ab4..c3f0314389 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/colorpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/colorpicker.controller.js @@ -7,7 +7,7 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.ColorPickerControl }; //map the user config - angular.extend(config, $scope.model.config); + Utilities.extend(config, $scope.model.config); //map back to the model $scope.model.config = config; diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediapicker.controller.js index 4258229042..65e5fa906f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediapicker.controller.js @@ -23,7 +23,7 @@ function mediaPickerController($scope, entityResource, iconHelper, editorService //combine the dialogOptions with any values returned from the server if ($scope.model.config) { - angular.extend(dialogOptions, $scope.model.config); + Utilities.extend(dialogOptions, $scope.model.config); } $scope.openTreePicker = function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treepicker.controller.js index 0359043da4..1b490d86d3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treepicker.controller.js @@ -21,7 +21,7 @@ angular.module('umbraco') //combine the config with any values returned from the server if ($scope.model.config) { - angular.extend(config, $scope.model.config); + Utilities.extend(config, $scope.model.config); } if ($scope.model.value) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index e818fe9a23..d8c7b3e76a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -130,7 +130,7 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso delete $scope.model.config.startNode; } //merge the server config on top of the default config, then set the server config to use the result - $scope.model.config = angular.extend(defaultConfig, $scope.model.config); + $scope.model.config = Utilities.extend(defaultConfig, $scope.model.config); // if the property is mandatory, set the minCount config to 1 (unless of course it is set to something already), // that way the minCount/maxCount validation handles the mandatory as well @@ -189,7 +189,7 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso //since most of the pre-value config's are used in the dialog options (i.e. maxNumber, minNumber, etc...) we'll merge the // pre-value config on to the dialog options - angular.extend(dialogOptions, $scope.model.config); + Utilities.extend(dialogOptions, $scope.model.config); dialogOptions.dataTypeKey = $scope.model.dataTypeKey; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js index af1dea167a..3ec7e191f9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js @@ -24,7 +24,7 @@ function dateTimePickerController($scope, angularHelper, dateHelper, validationM }; // map the user config - $scope.model.config = angular.extend(config, $scope.model.config); + $scope.model.config = Utilities.extend(config, $scope.model.config);; // ensure the format doesn't get overwritten with an empty string if ($scope.model.config.format === "" || $scope.model.config.format === undefined || $scope.model.config.format === null) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js index 4064df6a24..d62d521d17 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js @@ -8,7 +8,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.DropdownFlexibleCo }; //map the user config - angular.extend(config, $scope.model.config); + Utilities.extend(config, $scope.model.config); //map back to the model $scope.model.config = config; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index 15f5ceaa88..983644767d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -753,7 +753,7 @@ angular.module("umbraco") // allowed for this template based on the current config. _.each(found.sections, function (templateSection, index) { - angular.extend($scope.model.value.sections[index], Utilities.copy(templateSection)); + Utilities.extend($scope.model.value.sections[index], Utilities.copy(templateSection)); }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js index e9d9950bdd..cbaf843d35 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js @@ -43,7 +43,7 @@ angular.module('umbraco') function setModelValueWithSrc(src) { if (!$scope.model.value || !$scope.model.value.src) { //we are copying to not overwrite the original config - $scope.model.value = angular.extend(Utilities.copy($scope.model.config), { src: src }); + $scope.model.value = Utilities.extend(Utilities.copy($scope.model.config), { src: src }); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js index c6320a7cf2..bdd3251ca7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js @@ -116,7 +116,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl media.loading = true; entityResource.getById(media.udi, "Media") .then(function (mediaEntity) { - angular.extend(media, mediaEntity); + Utilities.extend(media, mediaEntity); media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); media.loading = false; }); @@ -226,7 +226,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl // we need to update all the media items vm.mediaItems.forEach(media => { if (media.id === model.mediaNode.id) { - angular.extend(media, mediaEntity); + Utilities.extend(media, mediaEntity); media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.controller.js index 315eb18ee4..0aa01a560b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.controller.js @@ -40,7 +40,7 @@ function memberPickerController($scope, entityResource, iconHelper, editorServic //since most of the pre-value config's are used in the dialog options (i.e. maxNumber, minNumber, etc...) we'll merge the // pre-value config on to the dialog options if ($scope.model.config) { - angular.extend(dialogOptions, $scope.model.config); + Utilities.extend(dialogOptions, $scope.model.config); } $scope.openMemberPicker = function () { 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 9faa012a4d..1b7a96309a 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 @@ -86,8 +86,8 @@ angular.module("umbraco") }); }; - - angular.extend(baseLineConfigObj, standardConfig); + + Utilities.extend(baseLineConfigObj, standardConfig); // We need to wait for DOM to have rendered before we can find the element by ID. $timeout(function () { From 2697431de68820986cb2a1b45d014398c0b1765f Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Fri, 28 May 2021 09:08:06 +0200 Subject: [PATCH 187/289] Fix issue with extend of object in keyboard service --- .../src/common/services/keyboard.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/keyboard.service.js b/src/Umbraco.Web.UI.Client/src/common/services/keyboard.service.js index b49e809eeb..66babe03b8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/keyboard.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/keyboard.service.js @@ -264,7 +264,7 @@ function keyboardService($window, $timeout) { var elt; // Initialize opt object - opt = Utilities.extend(defaultOpt, opt); + opt = Utilities.extend({}, defaultOpt, opt); label = label.toLowerCase(); elt = opt.target; if(typeof opt.target === 'string'){ From 1c630e3c5f5013a4b4a5b0218555066a9f6e72f9 Mon Sep 17 00:00:00 2001 From: Vlael Layug Date: Fri, 28 May 2021 00:07:30 +0800 Subject: [PATCH 188/289] inject clearAddUserForm() to inviteUser condition --- .../src/views/users/views/users/users.controller.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 70102c9418..0bc6e609db 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -245,6 +245,8 @@ $location.search("invite", null); } else if (state === "inviteUser") { + clearAddUserForm(); + $location.search("create", null); $location.search("invite", "true"); } From fc417e8ed811cf30c140c040ef07eecd9130c7ce Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 31 May 2021 09:18:12 +0200 Subject: [PATCH 189/289] Bump version to 8.11.3 --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index ca9d596415..e4becddab0 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.11.2")] -[assembly: AssemblyInformationalVersion("8.11.2")] +[assembly: AssemblyFileVersion("8.11.3")] +[assembly: AssemblyInformationalVersion("8.11.3")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 366a852a96..3b7286c293 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -347,9 +347,9 @@ False True - 8112 + 8113 / - http://localhost:8112 + http://localhost:8113 False False From 042e296d8f596f784c9c18333d507e45f02ff9d6 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 31 May 2021 09:21:44 +0200 Subject: [PATCH 190/289] Bump version to 8.12.3 --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 4162f47da6..cf8bde8057 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.12.2")] -[assembly: AssemblyInformationalVersion("8.12.2")] +[assembly: AssemblyFileVersion("8.12.3")] +[assembly: AssemblyInformationalVersion("8.12.3")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 2ad23e1b73..0454ba6c12 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -347,9 +347,9 @@ False True - 8122 + 8123 / - http://localhost:8122 + http://localhost:8123 False False From 0990599c630e2b372b7c3f7ed11ad47bea05d0de Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Mon, 31 May 2021 18:12:06 +0200 Subject: [PATCH 191/289] Support custom SVG icon in content app --- .../views/components/editor/umb-editor-navigation-item.html | 2 +- .../src/views/components/editor/umb-editor-navigation.html | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html index 0fc84c7fb8..9dd90b6b57 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html @@ -8,7 +8,7 @@ class="umb-sub-views-nav-item__action umb-outline umb-outline--thin" aria-haspopup="{{ vm.item.anchors && vm.item.anchors.length > 1 }}" aria-expanded="{{ vm.expanded }}"> - + {{ vm.item.name }}
    {{vm.item.badge.count}}
    !
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation.html index 08858b058b..be196fb31f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation.html @@ -23,8 +23,7 @@ text="{{moreButton.name}}" show-text="true" mode="tab" - css-class="umb-sub-views-nav-item__action umb-outline umb-outline--thin {{moreButton.active ? 'is-active' : ''}}" - > + css-class="umb-sub-views-nav-item__action umb-outline umb-outline--thin {{moreButton.active ? 'is-active' : ''}}"> From 3c2e7759fb1190e2a428a4fe2911b652cffa46fa Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Wed, 2 Jun 2021 03:00:01 +0200 Subject: [PATCH 192/289] Support custom SVG icon in block list editor (#10365) * Use custom SVG icon in block card * Adjust icons in block list editor views --- .../components/blockcard/umb-block-card.html | 8 +++- .../inlineblock/inlineblock.editor.html | 2 +- .../inlineblock/inlineblock.editor.less | 41 +++++++++++++++---- .../labelblock/labelblock.editor.html | 2 +- .../labelblock/labelblock.editor.less | 2 +- .../unsupportedblock.editor.html | 2 +- .../unsupportedblock.editor.less | 2 +- 7 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card.html b/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card.html index c1656c33ff..276cc2491a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card.html @@ -2,7 +2,13 @@
    - +
    + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html index 6cf717df81..c6b5e0a4fb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html @@ -5,7 +5,7 @@ ng-click="vm.openBlock(block)" ng-focus="block.focus"> - + {{block.label}}
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less index 9b408becce..5b155ac5ad 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less @@ -27,8 +27,8 @@ transition: transform 80ms ease-out; } - i { - font-size: 22px; + .icon { + font-size: 1.1rem; display: inline-block; vertical-align: middle; } @@ -48,17 +48,21 @@ ng-form.ng-invalid-val-server-match-content > .umb-block-list__block > .umb-block-list__block--content > div > & { > button { color: @formErrorText; + .show-validation-type-warning & { color: @formWarningText; } + span.caret { border-top-color: @formErrorText; + .show-validation-type-warning & { border-top-color: @formWarningText; } } } } + ng-form.ng-invalid-val-server-match-content > .umb-block-list__block:not(.--active) > .umb-block-list__block--content > div > & { > button { span.name { @@ -78,22 +82,41 @@ padding: 2px; line-height: 10px; background-color: @formErrorText; + .show-validation-type-warning & { background-color: @formWarningText; } + font-weight: 900; - animation-duration: 1.4s; animation-iteration-count: infinite; animation-name: blockelement-inlineblock-editor--badge-bounce; animation-timing-function: ease; + @keyframes blockelement-inlineblock-editor--badge-bounce { - 0% { transform: translateY(0); } - 20% { transform: translateY(-4px); } - 40% { transform: translateY(0); } - 55% { transform: translateY(-2px); } - 70% { transform: translateY(0); } - 100% { transform: translateY(0); } + 0% { + transform: translateY(0); + } + + 20% { + transform: translateY(-4px); + } + + 40% { + transform: translateY(0); + } + + 55% { + transform: translateY(-2px); + } + + 70% { + transform: translateY(0); + } + + 100% { + transform: translateY(0); + } } } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html index f51ea4607c..a5c4993505 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html @@ -3,6 +3,6 @@ ng-focus="block.focus" ng-class="{ '--active': block.active, '--error': parentForm.$invalid && valFormManager.isShowingValidation() }" val-server-property-class=""> - + {{block.label}} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less index 837fd3f564..8391923339 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less @@ -16,7 +16,7 @@ user-select: none; transition: border-color 120ms, background-color 120ms; - > i { + > .icon { font-size: 22px; margin-right: 10px; display: inline-block; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html index d5b0ddfd04..2252db3745 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html @@ -1,6 +1,6 @@
    - + {{block.label}}
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less index d3eddd0815..8b8fcef9e7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less @@ -18,7 +18,7 @@ background-color: @gray-11; color: @gray-6; - i { + .icon { font-size: 22px; margin-right: 5px; display: inline-block; From 57f777684890069a19ce1c27adf186da6ec4c856 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Wed, 2 Jun 2021 03:20:56 +0200 Subject: [PATCH 193/289] Add input id so click on property label set focus in input (#9477) Co-authored-by: Nathan Woulfe --- src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html b/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html index 1577af43de..f59491c693 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html @@ -5,6 +5,7 @@ ng-submit="vm.save()" novalidate val-form-manager> +

    - + ng-attr-style="{{control.editor.config.style}}" + localize="placeholder" + placeholder="@grid_placeholderWriteHere"> +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index 983644767d..21f6354c62 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -318,7 +318,6 @@ angular.module("umbraco") // Add items overlay menu // ********************************************* $scope.openEditorOverlay = function (event, area, index, key) { - const dialog = { view: "itempicker", filter: area.$allowedEditors.length > 15, From 57cc3d5da2d9f732d79d9a897ed5e7e452a09867 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Mon, 31 May 2021 19:09:20 +0200 Subject: [PATCH 198/289] Use custom SVG icon in user group filter --- .../src/views/users/views/users/users.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 57c87807fc..57c1128151 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -150,7 +150,7 @@ From 340273a6045e6466f41d5e14f335d6e990da9b75 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Mon, 7 Jun 2021 02:40:14 +0200 Subject: [PATCH 199/289] Support custom SVG icon in Nested Content (#10368) * Use custom SVG icon in Nested Content * Get icon once * Assign value instead of compare --- .../nestedcontent/nestedcontent.propertyeditor.html | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html index aaebb5d07e..22897e3ca2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html @@ -6,9 +6,9 @@
    -
    +
    - +
    @@ -18,7 +18,13 @@ ng-hide="vm.singleMode" umb-auto-focus="{{vm.focusOnNode && vm.currentNode.key === node.key ? 'true' : 'false'}}"> -
    +
    + + + +
    -
    From 31c0faa54e657978727e599a6d149c53de86563b Mon Sep 17 00:00:00 2001 From: patrickdemooij9 Date: Sat, 5 Jun 2021 22:44:13 +0200 Subject: [PATCH 200/289] Updated umbtable.directive.js for a working example I noticed that the example on https://our.umbraco.com/apidocs/v8/ui/#/api/umbraco.directives.directive:umbTable is actually not working. items should be replaced with vm.items. --- .../src/common/directives/components/umbtable.directive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js index 1554c136b6..c6f4c79ea2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js @@ -12,7 +12,7 @@
    Date: Mon, 7 Jun 2021 03:32:01 +0200 Subject: [PATCH 201/289] Update connect color in noUiSlider (#10349) Co-authored-by: Nathan Woulfe --- .../src/less/components/umb-range-slider.less | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less index 6c1980a6e4..42a13c7dda 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less @@ -15,6 +15,11 @@ height: 20px; top: -6px; } +.umb-range-slider .noUi-connect { + background-color: @purple-washed; + border: 1px solid @purple-l3; +} + .umb-range-slider .noUi-tooltip { padding: 2px 6px; } From 1e374105785d63ce4d4f21507b6533e7fa150150 Mon Sep 17 00:00:00 2001 From: inetzo Date: Mon, 7 Jun 2021 12:46:49 +0200 Subject: [PATCH 202/289] Convert date to datetime Datetime values where always converted to date(102) so sorting by time does not work. By converting to 120 (yyyy-mm-dd hh:mi:ss (24h)) the time is respected. --- src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 6f13afb24c..4d6b2eeea1 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -571,7 +571,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax public virtual string CreateDefaultConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} DEFAULT ({2}) FOR {3}"; public virtual string ConvertIntegerToOrderableString => "REPLACE(STR({0}, 8), SPACE(1), '0')"; - public virtual string ConvertDateToOrderableString => "CONVERT(nvarchar, {0}, 102)"; + public virtual string ConvertDateToOrderableString => "CONVERT(nvarchar, {0}, 120)"; public virtual string ConvertDecimalToOrderableString => "REPLACE(STR({0}, 20, 9), SPACE(1), '0')"; } } From c01b2c58073a5af2d5431721211c3399396d2a05 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 8 Jun 2021 07:51:08 +0200 Subject: [PATCH 203/289] Removed the "Umbraco Pro" category. --- .../src/views/packages/views/repo.controller.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.controller.js b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.controller.js index 4a2a3f29a7..90cfc9d2f7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function PackagesRepoController($scope, $route, $location, $timeout, ourPackageRepositoryResource, $q, packageResource, localStorageService, localizationService) { + function PackagesRepoController($scope, $timeout, ourPackageRepositoryResource, $q, packageResource, localStorageService, localizationService) { var vm = this; @@ -75,7 +75,9 @@ $q.all([ ourPackageRepositoryResource.getCategories() .then(function (cats) { - vm.categories = cats; + vm.categories = cats.filter(function (cat) { + return cat.name !== "Umbraco Pro"; + }); }), ourPackageRepositoryResource.getPopular(8) .then(function (pack) { From 97bc301bf8943c96119a5c762e79ce4359ed0673 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 8 Jun 2021 10:08:27 +0200 Subject: [PATCH 204/289] Adds the "works on Cloud" indicator to the package list and details screen in the back-office. --- .../src/less/components/umb-packages.less | 10 ++++++++++ .../src/views/packages/views/repo.html | 16 ++++++++++++++++ src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 1 + src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 1 + 4 files changed, 28 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less index 0045bed140..f660da8f8b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less @@ -157,6 +157,16 @@ color: @red !important; } +.umb-package-link .umb-package-cloud { + margin-top: 8px; + font-size: 11px; + height: 11px; // ensures vertical space is taken up even if "works on cloud" isn't visible +} + +.umb-package-link .umb-package-cloud .icon-cloud { + color: #2eadaf !important; +} + // Version .umb-package-version { display: inline-flex; diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html index 42b538f9ad..e92bd73535 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html +++ b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html @@ -62,6 +62,12 @@ {{package.likes}}
    +
    +
    + + Certified to work on Umbraco Cloud +
    +
    @@ -101,6 +107,12 @@ {{ package.likes }}
    +
    +
    + + Certified to work on Umbraco Cloud +
    +
    @@ -269,6 +281,10 @@
    {{vm.package.likes}}
    +
    +
    Certified to work on Umbraco CLoud
    +
    + diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index c76747c5a0..3ecafd1d67 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1288,6 +1288,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont All done, your browser will now refresh, please wait... Please click 'Finish' to complete installation and reload the page. Uploading package... + Certified to work on Umbraco Cloud Paste with full formatting (Not recommended) diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index ba7586dde2..ec56665a9c 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1294,6 +1294,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont All done, your browser will now refresh, please wait... Please click 'Finish' to complete installation and reload the page. Uploading package... + Certified to work on Umbraco Cloud Paste with full formatting (Not recommended) From e9c3c9fea6234bbe753b75108b8339e21bcc59b0 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Tue, 8 Jun 2021 10:19:26 +0200 Subject: [PATCH 205/289] Fix styling issues with listview sorting in overlay. (#10351) Remove margin bottom at input fields --- .../components/umb-list-view-settings.less | 7 +- .../src/less/listview.less | 104 +++++++++--------- 2 files changed, 56 insertions(+), 55 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-list-view-settings.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-list-view-settings.less index ba46c68a57..9eb00d4437 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-list-view-settings.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-list-view-settings.less @@ -66,11 +66,8 @@ padding-left: 15px; } - .ui-sortable-handle { - min-height: 37px; - display: flex; - width:0; - align-items: center; + input[type="text"] { + margin-bottom: 0; } } } diff --git a/src/Umbraco.Web.UI.Client/src/less/listview.less b/src/Umbraco.Web.UI.Client/src/less/listview.less index 582da12804..9321577c15 100644 --- a/src/Umbraco.Web.UI.Client/src/less/listview.less +++ b/src/Umbraco.Web.UI.Client/src/less/listview.less @@ -222,63 +222,67 @@ /* ---------- LAYOUTS ---------- */ .list-view-layout { - display: flex; - align-items: center; - padding: 10px 15px; - background: @gray-10; - margin-bottom: 1px; -} + display: flex; + align-items: center; + padding: 10px 15px; + background: @gray-10; + margin-bottom: 1px; -.list-view-layout__sort-handle { - font-size: 14px; - color: @gray-8; - margin-right: 15px; -} + &__sort-handle { + font-size: 14px; + color: @gray-8; + margin-right: 15px; + } -.list-view-layout__name { - flex: 5; - font-weight: bold; - margin-right: 15px; - display: flex; - align-content: center; - flex-wrap: wrap; - line-height: 1.2em; -} + &__name { + flex: 5; + font-weight: bold; + margin-right: 15px; + display: flex; + align-content: center; + flex-wrap: wrap; + line-height: 1.2em; + } -.list-view-layout__name-text { - margin-right: 3px; -} - -.list-view-layout__system { - font-size: 10px; - font-weight: normal; -} + &__name-text { + margin-right: 3px; + } -.list-view-layout__path { - flex: 10; - margin-right: 15px; -} + &__system { + font-size: 10px; + font-weight: normal; + } -.list-view-layout__icon-wrapper { - margin-right: 10px; -} + &__path { + flex: 10; + margin-right: 15px; + } -.list-view-layout__icon { - font-size: 18px; - vertical-align: middle; - border: 1px solid @gray-8; - background: @white; - padding: 0; - display: flex; - align-items: center; - justify-content: center; - width: 30px; - height: 30px; -} + &__icon-wrapper { + margin-right: 10px; + } -.list-view-layout__remove { - position: relative; - cursor: pointer; + &__icon { + font-size: 18px; + vertical-align: middle; + border: 1px solid @gray-8; + background: @white; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + } + + &__remove { + position: relative; + cursor: pointer; + } + + input[type="text"] { + margin-bottom: 0; + } } .list-view-add-layout { From dfae0adf3d5b6d4b2d3ffffed5711c1f7b1c7a20 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 8 Jun 2021 11:50:16 +0200 Subject: [PATCH 206/289] Amended phrase for Cloud confirmed packages from "certified" to "verified". --- src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html | 4 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html index e92bd73535..c59d2ee3e2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html +++ b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html @@ -110,7 +110,7 @@
    - Certified to work on Umbraco Cloud + Verified to work on Umbraco Cloud
    @@ -282,7 +282,7 @@
    -
    Certified to work on Umbraco CLoud
    +
    Verified to work on Umbraco CLoud
    diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 3ecafd1d67..727ca31dab 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1288,7 +1288,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont All done, your browser will now refresh, please wait... Please click 'Finish' to complete installation and reload the page. Uploading package... - Certified to work on Umbraco Cloud + Verified to work on Umbraco Cloud Paste with full formatting (Not recommended) diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index ec56665a9c..4fc6fb7cdb 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1294,7 +1294,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont All done, your browser will now refresh, please wait... Please click 'Finish' to complete installation and reload the page. Uploading package... - Certified to work on Umbraco Cloud + Verified to work on Umbraco Cloud Paste with full formatting (Not recommended) From 87eb224e5c5b6d036eefb0b4e32d3dd332464832 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 8 Jun 2021 11:55:58 +0200 Subject: [PATCH 207/289] Updated package listing to use umb-icon directive. --- .../src/views/packages/views/repo.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html index c59d2ee3e2..c29a914763 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html +++ b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html @@ -54,17 +54,17 @@
    - + {{package.downloads}} - + {{package.likes}}
    - + Certified to work on Umbraco Cloud
    @@ -99,17 +99,17 @@
    - - {{ package.downloads }} + + {{package.downloads}} - - {{ package.likes }} + + {{package.likes}}
    - + Verified to work on Umbraco Cloud
    From b5dad01fdcad3d769de89fd5df33928376c802f8 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 8 Jun 2021 12:09:55 +0200 Subject: [PATCH 208/289] Update localisation alias to match default phrase. --- src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html | 4 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html index c29a914763..18fd7c55f5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html +++ b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html @@ -65,7 +65,7 @@
    - Certified to work on Umbraco Cloud + Certified to work on Umbraco Cloud
    @@ -110,7 +110,7 @@
    - Verified to work on Umbraco Cloud + Verified to work on Umbraco Cloud
    diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 727ca31dab..6c684d2413 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1288,7 +1288,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont All done, your browser will now refresh, please wait... Please click 'Finish' to complete installation and reload the page. Uploading package... - Verified to work on Umbraco Cloud + Verified to work on Umbraco Cloud Paste with full formatting (Not recommended) diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 4fc6fb7cdb..f56024dacd 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1294,7 +1294,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont All done, your browser will now refresh, please wait... Please click 'Finish' to complete installation and reload the page. Uploading package... - Verified to work on Umbraco Cloud + Verified to work on Umbraco Cloud Paste with full formatting (Not recommended) From b06f902865812120174ddc9a0851edcc07088e62 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 8 Jun 2021 12:45:21 +0200 Subject: [PATCH 209/289] Update config.yml --- .github/ISSUE_TEMPLATE/config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d5418ad270..edee3cc9d9 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,8 @@ blank_issues_enabled: false contact_links: + - name: 📮 Features and ideas + url: https://github.com/umbraco/Umbraco-CMS/discussions/new?categories=features-and-ideas + about: Start a new discussion when you have ideas or feature requests, eventually discussions can turn into plans - name: ⁉️ Support Question url: https://our.umbraco.com about: This issue tracker is NOT meant for support questions. If you have a question, please join us on the forum. From ef20264df6c04123ee14536c68587c38d42a8d43 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 8 Jun 2021 12:47:44 +0200 Subject: [PATCH 210/289] Delete 02_feature_request.yml --- .github/ISSUE_TEMPLATE/02_feature_request.yml | 33 ------------------- 1 file changed, 33 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/02_feature_request.yml diff --git a/.github/ISSUE_TEMPLATE/02_feature_request.yml b/.github/ISSUE_TEMPLATE/02_feature_request.yml deleted file mode 100644 index 5d53b2f12e..0000000000 --- a/.github/ISSUE_TEMPLATE/02_feature_request.yml +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: 📮 Feature Request -description: Open a feature request, if you want to propose a new feature. -labels: type/feature -body: -- type: dropdown - id: version - attributes: - label: Umbraco version - description: Which major Umbraco version are you proposing a feature for? - options: - - v8 - - v9 - validations: - required: true -- type: textarea - id: summary - attributes: - label: Description - description: Write a brief desciption of your proposed new feature. - validations: - required: true -- type: textarea - attributes: - label: How can you help? - id: help - description: Umbraco''s core team has limited available time, but maybe you can help? - placeholder: > - If we can not work on your suggestion, please don't take it personally. Most likely, it's either: - - - We think your idea is valid, but we can't find the time to work on it. - - - Your idea might be better suited as a package, if it's not suitable for the majority of users. From b63ee6e7bf7baf7fef2770acbbf31fbc81781f6b Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 8 Jun 2021 13:00:40 +0200 Subject: [PATCH 211/289] Update config.yml --- .github/ISSUE_TEMPLATE/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index edee3cc9d9..ecf10b8854 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,7 +1,7 @@ blank_issues_enabled: false contact_links: - - name: 📮 Features and ideas - url: https://github.com/umbraco/Umbraco-CMS/discussions/new?categories=features-and-ideas + - name: 💡 Features and ideas + url: https://github.com/umbraco/Umbraco-CMS/discussions/new?category=features-and-ideas about: Start a new discussion when you have ideas or feature requests, eventually discussions can turn into plans - name: ⁉️ Support Question url: https://our.umbraco.com From 4e60e7dec17c308ddfdec042b459da6bb7572fa5 Mon Sep 17 00:00:00 2001 From: Nikcio Date: Fri, 28 May 2021 00:34:00 +0200 Subject: [PATCH 212/289] Translation added Added some danish translations and fixed a translation problem in the copy document. --- .../src/views/content/copy.html | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 179 ++++++++++++++++-- 2 files changed, 159 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/copy.html b/src/Umbraco.Web.UI.Client/src/views/content/copy.html index 111dacd7cb..399e0b8bf0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/copy.html @@ -11,7 +11,7 @@
    - {{source.name}} was copied to + {{source.name}} was copied to {{target.name}}
    diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index b2b11fdef5..b23d674072 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -31,6 +31,7 @@ Udgiv Afpublicér Genindlæs elementer + Fjern Genudgiv hele sitet Omdøb Gendan @@ -55,6 +56,7 @@ Sæt rettigheder Lås op Opret indholdsskabelon + Gensend Invitation Standard værdi @@ -141,18 +143,21 @@ Gem og send til udgivelse Gem listevisning Planlæg + Se side Forhåndsvisning Forhåndsvisning er deaktiveret fordi der ikke er nogen skabelon tildelt Vælg formattering Vis koder Indsæt tabel + Generer modeller og luk Gem og generer modeller Fortryd Genskab + Rul tilbage Slet tag Fortryd Bekræft - Flere publiseringsmuligheder + Flere publiseringsmuligheder Indsæt Indsæt og luk @@ -160,6 +165,7 @@ For Brugeren har slettet indholdet Brugeren har afpubliceret indholdet + Brugeren har afpubliceret indholdet for sprogene: %0% Brugeren har gemt og udgivet indholdet Brugeren har gemt og udgivet indholdet for sprogene: %0% Brugeren har gemt indholdet @@ -168,8 +174,10 @@ Brugeren har kopieret indholdet Brugeren har tilbagerullet indholdet til en tidligere tilstand Brugeren har sendt indholdet til udgivelse + Brugeren har sendt indholdet til udgivelse for sprogene: %0% Brugeren har sendt indholdet til oversættelse Brugeren har sorteret de underliggende sider + %0% Kopieret Udgivet Udgivet @@ -178,10 +186,13 @@ Gemt Slettet Afpubliceret + Afpubliceret Indhold tilbagerullet Sendt til udgivelse + Sendt til udgivelse Sendt til oversættelse Sorteret + Brugerdefineret Historik (alle sprog) @@ -241,6 +252,7 @@ Ingen dato valgt Sidetitel Dette medie har ikke noget link + Intet indhold kan tilføjes for dette element Egenskaber Dette dokument er udgivet, men ikke synligt da den overliggende side '%0%' ikke er udgivet! Dette sprog er udgivet, men ikke synligt, da den overliggende side '%0%' ikke er udgivet! @@ -263,6 +275,7 @@ Statistik Titel (valgfri) Alternativ tekst (valgfri) + Overskrift (valgfri) Type Hvilke varianter vil du udgive? Vælg hvilke varianter, der skal gemmes. @@ -272,6 +285,8 @@ Sidst redigeret Tidspunkt for seneste redigering Fjern fil + Klik her for at fjerne billedet fra medie filen + Klik her for at fjerne filen fra medie filen Link til dokument Medlem af grupper(ne) Ikke medlem af grupper(ne) @@ -283,6 +298,9 @@ Er du sikker på, at du vil slette alle elementer? Egenskaben %0% anvender editoren %1% som ikke er understøttet af Nested Content. Der er ikke konfigureret nogen indholdstyper for denne egenskab. + Tilføj element type + Vælg element type + Vælg gruppen, hvis værdier skal vises. Hvis dette er efterladt blankt vil den første gruppe på element typen bruges. %0% fra %1% Tilføj en ny tekstboks Fjern denne tekstboks @@ -301,6 +319,14 @@ Ikke-udgivne sprog Uændrede sprog Disse sprog er ikke blevet oprettet + Alle nye varianter vil blive gemt. + Hvilke varianter skal udgives? + Vælg, hvilke varianter skal gemmes. + Vælg varianter som skal sendes til gennemgang. + Sæt udgivnings tidspunkt... + Vælg varianterne som skal afpubliceres. Afpublicering af et krævet sprog vil afpublicere alle varianter. + De følgende varianter er krævet for at en udgivelse kan finde sted: + Vi er ikke klar til at udgive Klar til at udgive? Klar til at gemme? Send til godkendelse @@ -326,10 +352,12 @@ Maks filstørrelse er Medie rod Flytning af mediet fejlede + Overordnet og destinations mappe kan ikke være den samme Kopiering af mediet fejlede Oprettelse af mappen under parent med id %0% fejlede Omdøbning af mappen med id %0% fejlede Træk dine filer ind i dropzonen for, at uploade dem til mediebiblioteket. + Upload er ikke tiladt på denne lokation Opret et nyt medlem @@ -337,16 +365,16 @@ Medlemgrupper har ingen yderligere egenskaber til redigering. - Kopiering af indholdstypen fejlede - Flytning af indholdstypen fejlede + Kopiering af indholdstypen fejlede + Flytning af indholdstypen fejlede - Kopiering af medietypen fejlede - Flytning af medietypen fejlede - Auto vælg + Kopiering af medietypen fejlede + Flytning af medietypen fejlede + Auto vælg - Kopiering af medlemstypen fejlede + Kopiering af medlemstypen fejlede Hvor ønsker du at oprette den nye %0% @@ -359,6 +387,7 @@ Den valgte side i træet tillader ikke at sider oprettes under den. Rediger tilladelser for denne dokumenttype. Opret en ny dokumenttype + Dokumenttyper inde i Indstillinger sektionen, ved at ændre Tillad på rodniveau indestillingen under Permissions.]]> "media typer".]]> Det valgte medie i træet tillader ikke at medier oprettes under det. Rediger tilladelser for denne medietype. @@ -435,6 +464,9 @@ Luk denne dialog Er du sikker på at du vil slette Er du sikker på du vil deaktivere + Er du sikker på at du vil fjerne + %0%]]> + %0%]]> Er du sikker på at du vil forlade Umbraco? Er du sikker? Klip @@ -449,6 +481,7 @@ Indsæt makro Indsæt tabel Dette vil slette sproget + Ændring af kulturen for et sprog kan forsage en krævende opration og vil resultere i indholds cache og indeksering vil blive genlavet Sidst redigeret Link Internt link: @@ -513,6 +546,9 @@ Vælg konfiguration Vælg snippet Dette vil slette noden og alle dets sprog. Hvis du kun vil slette et sprog, så afpublicér det i stedet. + %0%]]> + %0% fra gruppen]]> + Ja, fjern Der er ingen ordbogselementer. @@ -574,6 +610,9 @@ #value eller ?key=value Indtast alias... Genererer alias... + Opret element + Rediger + Navn Opret brugerdefineret listevisning @@ -638,6 +677,7 @@ Denne egenskab er ugyldig + Valgmuligheder Om Handling Muligheder @@ -655,6 +695,7 @@ Ryd Luk Luk vindue + Luk vindue Kommentar Bekræft Proportioner @@ -663,6 +704,7 @@ Fortsæt Kopiér Opret + Beskær sektion Database Dato Standard @@ -788,6 +830,8 @@ Andet Artikler Videoer + installere + Avatar til Blå @@ -805,7 +849,7 @@ Vis genveje Brug listevisning Tillad på rodniveau - Lommentér/Udkommentér linjer + Kommentér/Udkommentér linjer Slet linje Kopiér linjer op Kopiér linjer ned @@ -1034,6 +1078,7 @@ Mange hilsner fra Umbraco robotten Bemærk: at dokumenter og medier som afhænger af denne pakke vil muligvis holde op med at virke, så vær forsigtig. Hvis i tvivl, kontakt personen som har udviklet pakken.]]> Pakke version + Opgraderer fra version Pakke allerede installeret Denne pakke kan ikke installeres, den kræver en minimum Umbraco version af Afinstallerer... @@ -1071,8 +1116,13 @@ Mange hilsner fra Umbraco robotten Hvis du ønsker at give adgang til enkelte medlemmer + Utilstrækkelige bruger adgang til a udgive alle under dokumenter Udgivelsen kunne ikke udgives da publiceringsdato er sat - + + Sortering udført Træk de forskellige sider op eller ned for at indstille hvordan de skal arrangeres, eller klik på kolonnehovederne for at sortere hele rækken af sider + Denne node har ingen under noder at sortere Validering Valideringsfejl skal rettes før elementet kan gemmes Fejlet Gemt + Gemt. For at se ændringerne skal du genindlæse din browser Utilstrækkelige brugerrettigheder, kunne ikke fuldføre handlingen Annulleret Handlingen blev annulleret af et 3. part tilføjelsesprogram @@ -1214,10 +1266,16 @@ Mange hilsner fra Umbraco robotten Udgivelse fejlede da overliggende side ikke er udgivet Indhold publiceret og nu synligt for besøgende + %0% dokumenter udgivet og synlige på hjemmesiden + %0% udgivet og synligt på hjemmesiden + %0% dokumenter udgivet for sprogene %1% og synlige på hjemmesiden Indhold gemt Husk at publicere for at gøre det synligt for besøgende + En planlægning for udgivelse er blevet opdateret + %0% gemt Send til Godkendelse Rettelser er blevet sendt til godkendelse + %0% rettelser er blevet sendt til godkendelse Medie gemt Medie gemt uden problemer Medlem gemt @@ -1244,6 +1302,8 @@ Mange hilsner fra Umbraco robotten Skabelon gemt Skabelon gemt uden fejl! Indhold fjernet fra udgivelse + Indhold variation %0% afpubliceret + Det krævet sprog '%0%' var afpubliceret. Alle sprog for dette indholds element er nu afpubliceret. Partial view gemt Partial view gemt uden fejl! Partial view ikke gemt @@ -1258,11 +1318,20 @@ Mange hilsner fra Umbraco robotten Brugergrupper er blevet indstillet Låste %0% brugere op %0% er nu låst op + Medlem blev exportet til fil + Der skete en fejl under exporteringen af medlemmet Brugeren %0% blev slettet Invitér bruger Invitationen blev gensendt til %0% + Kan ikke udgive dokumentet da det krævet '%0%' ikke er udgivet + Validering fejlede for sproget '%0%' Dokumenttypen blev eksporteret til en fil Der skete en fejl under eksport af en dokumenttype + Udgivelses datoen kan ikke ligge i fortiden + Kan ikke planlægge dokumentes udgivelse da det krævet '%0%' ikke er udgivet + Kan ikke planlægge dokumentes udgivelse da det krævet '%0%' har en senere udgivelses dato end et ikke krævet sprog + Afpubliceringsdatoen kan ikke ligge i fortiden + Afpubliceringsdatoen kan ikke være før udgivelsesdatoen Tilføj style @@ -1333,6 +1402,7 @@ Mange hilsner fra Umbraco robotten ]]> Query builder sider returneret, på + Kopier til udkilpsholder Returner alt indhold indhold af typen "%0%" @@ -1381,10 +1451,12 @@ Mange hilsner fra Umbraco robotten Grid layout Et layout er det overordnede arbejdsområde til dit grid - du vil typisk kun behøve ét eller to Tilføj grid layout + Rediger grid layout Juster dit layout ved at justere kolonnebredder og tilføj yderligere sektioner Rækkekonfigurationer Rækker er foruddefinerede celler, der arrangeres vandret Tilføj rækkekonfiguration + Rediger rækkekonfiguration Juster rækken ved at indstille cellebredder og tilføje yderligere celler Ingen yderligere konfiguration tilgængelig Kolonner @@ -1399,6 +1471,9 @@ Mange hilsner fra Umbraco robotten Vælg ekstra Vælg standard er tilføjet + Advarsel + Du sletter en rækkekonfiguration + Sletning af et rækkekonfigurations navn vil resultere i et tab af data for alle eksiterende indhold som bruger dens konfiguration. Maksimalt emner Efterlad blank eller sæt til 0 for ubegrænset @@ -1466,6 +1541,7 @@ Mange hilsner fra Umbraco robotten Element-type Er en Element-type En Element-type er tiltænkt brug i f.eks. Nested Content, ikke i indholdstræet. + En Dokumenttype kan ikke ændres til en Element-type efter den er blevet brugt til at oprette en eller flere indholds elementer. Dette benyttes ikke for en Element-type Du har lavet ændringer til denne egenskab. Er du sikker på at du vil kassere dem? Visning @@ -1509,21 +1585,27 @@ Mange hilsner fra Umbraco robotten Casing Kodning Felt som skal indsættes - Konvertér linieskift - Erstatter et linieskift med html-tag'et &lt;br&gt; + Konvertér linjeskift + Ja, konverter linjeskift + Erstatter et linjeskift med html-tag'et &lt;br&gt; Custom felter Ja, kun dato Format og kodning Formatér som dato + Formater værdien som en dato eller en dato med tid, i forhold til den aktive kultur HTML indkod Vil erstatte specielle karakterer med deres HTML jævnbyrdige. Denne tekst vil blive sat ind lige efter værdien af feltet Denne tekst vil blive sat ind lige før værdien af feltet Lowercase + Ændre udskrift Ingen + Udskrift eksempel Indsæt efter felt Indsæt før felt Rekursivt + Ja, lav det rekursivt + Separator Fjern paragraf-tags Fjerner eventuelle &lt;P&gt; omkring teksten Standard felter @@ -1609,6 +1691,8 @@ Mange hilsner fra Umbraco robotten Skift dit kodeord Skift billede Nyt kodeord + Minium %0% karakterer tilbage! + Der skal som minium være %0% specielle karakterer. er ikke blevet låst ude Kodeordet er ikke blevet ændret Gentag dit nye kodeord @@ -1643,8 +1727,10 @@ Mange hilsner fra Umbraco robotten Adgangskode Nulstil kodeord Dit kodeord er blevet ændret! + Kodeord ændret Bekræft venligst dit nye kodeord Indtast dit nye kodeord + Dit nye kodeord kan ikke være blankt! Nuværende kodeord ugyldig nuværende kodeord Dit nye kodeord må ikke være tomt! @@ -1664,6 +1750,7 @@ Mange hilsner fra Umbraco robotten Vælg brugergrupper Ingen startnode valgt Ingen startnoder valgt + Indhold startnode Begræns indholdstræet til en bestemt startnode Indhold startnoder Begræns indholdstræet til bestemte startnoder @@ -1677,7 +1764,7 @@ Mange hilsner fra Umbraco robotten er blevet inviteret En invitation er blevet sendt til den nye bruger med oplysninger om, hvordan man logger ind i Umbraco. Hej og velkommen til Umbraco! På bare 1 minut vil du være klar til at komme i gang, vi skal bare have dig til at oprette en adgangskode og tilføje et billede til din avatar. - Velkommen til Umbraco! Desværre er din invitation udløbet. Kontakt din administrator og bed om at gensende invitationen. + Velkommen til Umbraco! Desværre er din invitation udløbet. Kontakt din administrator og bed om at gensende invitationen. Hvis du uploader et billede af dig selv, gør du det nemt for andre brugere at genkende dig. Klik på cirklen ovenfor for at uploade et billede. Forfatter Skift @@ -1690,6 +1777,10 @@ Mange hilsner fra Umbraco robotten Tilbage til brugere Umbraco: Invitation

    Hej %0%, du er blevet inviteret af %1% til Umbraco backoffice.

    Besked fra %1%: %2%

    Klik på dette link for acceptere invitationen

    Hvis du ikke kan klikke på linket, så kopier og indsæt denne URL i dit browservindue

    %3%

    ]]>
    + Inviter + Gensender invitation... + Slet bruger + Er du sikker på du ønsker at slette denne brugers konto? Alle Aktiv Deaktiveret @@ -1701,6 +1792,7 @@ Mange hilsner fra Umbraco robotten Nyeste Ældste Sidst logget ind + Ingen brugere er blevet tilføjet Validering @@ -1709,7 +1801,9 @@ Mange hilsner fra Umbraco robotten Valider som URL ...eller indtast din egen validering Feltet er påkrævet + Indtast en selvvalgt validerings fejlbesked (valgfrit) Indtast et regulært udtryk + Indtast en selvvalgt validerings fejlbesked (valgfrit) Du skal tilføje mindst Du kan kun have Tilføj op til @@ -1724,14 +1818,18 @@ Mange hilsner fra Umbraco robotten Værdien kan ikke være tom Værdien kan ikke være tom Værdien er ugyldig, som ikke matcher det korrekte format + Selvvalgt validering %1% mere.]]> %1% for mange.]]> Slå URL tracker fra Slå URL tracker til + Kultur Original URL Viderestillet til + Viderestil URL håndtering + De følgende URLs viderestiller til dette indholds element Der er ikke lavet nogen viderestillinger Når en udgivet side bliver omdøbt eller flyttet, vil en viderestilling automatisk blive lavet til den nye side. Er du sikker på at du vil fjerne viderestillingen fra '%0%' til '%1%'? @@ -1806,12 +1904,34 @@ Mange hilsner fra Umbraco robotten Opsæt rettigheder på %0% Juster soterings rækkefølgen for %0% Opret indholds skabelon baseret på %0% + Åben kontext menu for Aktivt sprog Skift sprog til Opret ny mappe + Delvist View + Delvist View Macro + Medlem + Data type + Søg i viderestillings dashboardet + Søg i brugergruppe sektionen + Søg i bruger sektionen Opret element + Opret Rediger Navn + Tilføj ny række + Vis flere muligheder + Søg i Umbraco backoffice + Søg efter indholdsnoder, medienoder osv. i backoffice + Når autoudfyldnings resultaterne er klar, tryk op og ned pilene, eller benyt tab knappen og brug enter knappen til at vælge. + Vej + Fundet i + Har oversættelse + Mangler oversættelse + Ordbogs elementer + Udfør handling %0% på %1% noden + Tilføj billede overskrift + Søg i indholdstræet Referencer @@ -1823,12 +1943,17 @@ Mange hilsner fra Umbraco robotten Brugt i Medlems Typer Ingen referencer til Medlems Typer. Brugt af + Brugt i Dokumenter + Brugt i Medlemmer + Brugt i Medier Slet gemte søgning Log type + Gemte søgninger Gem søgning Indtast et navn for din søgebetingelse + Filter søgning Samlet resultat Dato Type @@ -1853,6 +1978,17 @@ Mange hilsner fra Umbraco robotten Find logs med Namespace Find logs med maskin navn Åben + Henter + Hver 2 sekunder + Hver 5 sekunder + Hver 10 sekunder + Hver 20 sekunder + Hver 30 sekunder + Henter hver 2s + Henter hver 5s + Henter hver 10s + Henter hver 20s + Henter hver 30s Kopier %0% @@ -1863,6 +1999,7 @@ Mange hilsner fra Umbraco robotten Åben egenskabshandlinger + Luk egenskabshandlinger Vælg elementtype @@ -1908,12 +2045,12 @@ Mange hilsner fra Umbraco robotten Tilføj %0% Feltet %0% bruger editor %1% som ikke er supporteret for blokke. - - Hvad er Indholdsskabeloner? - Indholdsskabeloner er foruddefineret indhold der kan vælges når der oprettes nye indholdselementer. - Hvordan opretter jeg en Indholdsskabelon? - - + Hvad er Indholdsskabeloner? + Indholdsskabeloner er foruddefineret indhold der kan vælges når der oprettes nye indholdselementer. + Hvordan opretter jeg en Indholdsskabelon? + + Der er to måder at oprette Indholdsskabeloner på:

    • Højreklik på en indholdsnode og vælg "Opret indholdsskabelon" for at oprette en ny Indholdsskabelon.
    • @@ -1922,9 +2059,9 @@ Mange hilsner fra Umbraco robotten

      Når indholdsskabelonen har fået et navn, kan redaktører begynde at bruge indholdsskabelonen som udgangspunkt for deres nye side.

      ]]> - Hvordan vedligeholder jeg Indholdsskabeloner? - Du kan redigere og slette Indholdsskabeloner fra "Indholdsskabeloner" i sektionen Indstillinger. Fold dokumenttypen som Indholdsskabelonen er baseret på ud og klik på den for at redigere eller slette den. - + Hvordan vedligeholder jeg Indholdsskabeloner? + Du kan redigere og slette Indholdsskabeloner fra "Indholdsskabeloner" i sektionen Indstillinger. Fold dokumenttypen som Indholdsskabelonen er baseret på ud og klik på den for at redigere eller slette den. + Afslut Afslut forhåndsvisning From 40c4c875c4673da125e8d04e4fc9ba617e130fdd Mon Sep 17 00:00:00 2001 From: Jan Skovgaard <1932158+BatJan@users.noreply.github.com> Date: Wed, 9 Jun 2021 01:02:37 +0200 Subject: [PATCH 213/289] Link picker: Make link input full width if anchor option is disabled (#10313) * Add missing focus styling * Add width modifier to link input * same result without adding new classes Co-authored-by: BatJan Co-authored-by: Jan Skovgaard Olsen Co-authored-by: Nathan Woulfe --- src/Umbraco.Web.UI.Client/src/less/property-editors.less | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index 328ba2229b..11d11c7e3a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -950,6 +950,12 @@ .umb-linkpicker__url { width: 50%; padding-right: 5px; + + // when the anchor input is hidden by config + // the URL input should be full-width + &:only-child { + width: 100%; + } } .umb-linkpicker__anchor { From b4e6222f327523ec2e5540c9d93065719a0b9685 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Thu, 10 Jun 2021 14:56:03 +0200 Subject: [PATCH 214/289] Fix up linq issues and lookup --- src/Umbraco.Web/Editors/ContentController.cs | 17 ++++++----------- .../BlockEditorPropertyEditor.cs | 6 +++--- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 38211cb0aa..fc473d729d 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -414,22 +414,17 @@ namespace Umbraco.Web.Editors { var result = new Dictionary(); - using (var scope = _scopeProvider.CreateScope()) - { - var contentTypes = Services.ContentTypeService.GetAll(contentTypeKeys).ToList(); + using var scope = _scopeProvider.CreateScope(autoComplete: true); + var contentTypes = Services.ContentTypeService.GetAll(contentTypeKeys).ToList(); - if (contentTypes.Any(contentType => contentType == null)) + foreach (var contentType in contentTypes) + { + if (contentType is null) { throw new HttpResponseException(HttpStatusCode.NotFound); } - foreach (var contentTypeKey in contentTypeKeys) - { - var contentType = contentTypes.First(c => c.Key == contentTypeKey); - result.Add(contentTypeKey, GetEmpty(contentType, parentId)); - } - - scope.Complete(); + result.Add(contentType.Key, GetEmpty(contentType, parentId)); } return result; diff --git a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs index 22f1a7dd1a..0f9fa5a1d2 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs @@ -156,15 +156,15 @@ namespace Umbraco.Web.PropertyEditors continue; } - if (!valEditors.ContainsKey(dataType.Id)) + if (!valEditors.TryGetValue(dataType.Id, out var valEditor)) { var tempConfig = dataType.Configuration; - var valEditor = propEditor.GetValueEditor(tempConfig); + valEditor = propEditor.GetValueEditor(tempConfig); valEditors.Add(dataType.Id, valEditor); } - var convValue = valEditors[dataType.Id].ToEditor(tempProp, dataTypeService); + var convValue = valEditor.ToEditor(tempProp, dataTypeService); // update the raw value since this is what will get serialized out row.RawPropertyValues[prop.Key] = convValue; From 6910399c3214490a31a2f7c4edd4be6669882b79 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Thu, 10 Jun 2021 15:33:29 +0200 Subject: [PATCH 215/289] Reverting optimization by passing data from a higher level - not really possible to achieve what we wanted --- .../PropertyEditors/BlockEditorPropertyEditor.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs index 0f9fa5a1d2..46b8c3f0ea 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs @@ -26,7 +26,6 @@ namespace Umbraco.Web.PropertyEditors private readonly Lazy _propertyEditors; private readonly IDataTypeService _dataTypeService; private readonly IContentTypeService _contentTypeService; - private readonly Lazy> _contentTypes; public BlockEditorPropertyEditor(ILogger logger, Lazy propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService localizedTextService) : base(logger) @@ -35,7 +34,6 @@ namespace Umbraco.Web.PropertyEditors _propertyEditors = propertyEditors; _dataTypeService = dataTypeService; _contentTypeService = contentTypeService; - _contentTypes = new Lazy>(() => contentTypeService.GetAll().ToDictionary(c => c.Key)); } // has to be lazy else circular dep in ctor @@ -43,7 +41,7 @@ namespace Umbraco.Web.PropertyEditors #region Value Editor - protected override IDataValueEditor CreateValueEditor() => new BlockEditorPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService, _localizedTextService, Logger, _contentTypes); + protected override IDataValueEditor CreateValueEditor() => new BlockEditorPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService, _localizedTextService, Logger); internal class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference { @@ -52,14 +50,14 @@ namespace Umbraco.Web.PropertyEditors private readonly ILogger _logger; private readonly BlockEditorValues _blockEditorValues; - public BlockEditorPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService textService, ILogger logger, Lazy> contentTypes) + public BlockEditorPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService textService, ILogger logger) : base(attribute) { _propertyEditors = propertyEditors; _dataTypeService = dataTypeService; _logger = logger; - _blockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypes, _logger); + _blockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypeService, _logger); Validators.Add(new BlockEditorValidator(_blockEditorValues, propertyEditors, dataTypeService, textService, contentTypeService)); Validators.Add(new MinMaxValidator(_blockEditorValues, textService)); } @@ -338,9 +336,9 @@ namespace Umbraco.Web.PropertyEditors private readonly BlockEditorDataConverter _dataConverter; private readonly ILogger _logger; - public BlockEditorValues(BlockEditorDataConverter dataConverter, Lazy> contentTypes, ILogger logger) + public BlockEditorValues(BlockEditorDataConverter dataConverter, IContentTypeService contentTypeService, ILogger logger) { - _contentTypes = contentTypes; + _contentTypes = new Lazy>(() => contentTypeService.GetAll().ToDictionary(c => c.Key)); _dataConverter = dataConverter; _logger = logger; } From 990fc118cbd6df423be2107a75970a834ade1f76 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Thu, 10 Jun 2021 15:36:39 +0200 Subject: [PATCH 216/289] Optimization by returning the same instance --- src/Umbraco.Core/PropertyEditors/DataEditor.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/PropertyEditors/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index add523ecf6..d927d10052 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -19,6 +19,7 @@ namespace Umbraco.Core.PropertyEditors public class DataEditor : IDataEditor { private IDictionary _defaultConfiguration; + private IDataValueEditor _reusableEditor; /// /// Initializes a new instance of the class. @@ -90,7 +91,8 @@ namespace Umbraco.Core.PropertyEditors /// simple enough for now. /// // TODO: point of that one? shouldn't we always configure? - public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? CreateValueEditor(); + public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? (_reusableEditor ?? (_reusableEditor = CreateValueEditor())); + /// /// From e70c3628513ca720c4b4098f60a0a3a43df399f2 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 14 Jun 2021 12:20:06 +0100 Subject: [PATCH 217/289] Run `npm update caniuse-lite` --- src/Umbraco.Web.UI.Client/package-lock.json | 205 +++++--------------- src/Umbraco.Web.UI.Client/package.json | 4 +- 2 files changed, 55 insertions(+), 154 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 3f53638fc6..b2d811df89 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -1836,8 +1836,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", - "dev": true, - "optional": true + "dev": true }, "base64id": { "version": "1.0.0", @@ -2084,7 +2083,6 @@ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", "dev": true, - "optional": true, "requires": { "p-finally": "^1.0.0" } @@ -2126,7 +2124,6 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha1-oWCRFxcQPAdBDO9j71Gzl8Alr5w=", "dev": true, - "optional": true, "requires": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" @@ -2136,15 +2133,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, - "optional": true + "dev": true }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", "dev": true, - "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -2160,7 +2155,6 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", "dev": true, - "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -2301,7 +2295,6 @@ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz", "integrity": "sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==", "dev": true, - "optional": true, "requires": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" @@ -2327,8 +2320,7 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true, - "optional": true + "dev": true }, "buffer-equal": { "version": "1.0.0", @@ -2503,9 +2495,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001168", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001168.tgz", - "integrity": "sha512-P2zmX7swIXKu+GMMR01TWa4csIKELTNnZKc+f1CjebmZJQtTAEXmpQSoKVJVVcvPGAA0TEYTOUp3VehavZSFPQ==", + "version": "1.0.30001237", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001237.tgz", + "integrity": "sha512-pDHgRndit6p1NR2GhzMbQ6CkRrp4VKuSsqbcLeOQppYPKOYkKT/6ZvZDvKJUqcmtyWIAHuZq3SVS2vc1egCZzw==", "dev": true }, "caseless": { @@ -2519,7 +2511,6 @@ "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", "integrity": "sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==", "dev": true, - "optional": true, "requires": { "get-proxy": "^2.0.0", "isurl": "^1.0.0-alpha5", @@ -2953,7 +2944,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", "dev": true, - "optional": true, "requires": { "graceful-readlink": ">= 1.0.0" } @@ -3048,7 +3038,6 @@ "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", "dev": true, - "optional": true, "requires": { "ini": "^1.3.4", "proto-list": "~1.2.1" @@ -3104,7 +3093,6 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", "dev": true, - "optional": true, "requires": { "safe-buffer": "5.1.2" } @@ -3548,7 +3536,6 @@ "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.0.tgz", "integrity": "sha1-eu3YVCflqS2s/lVnSnxQXpbQH50=", "dev": true, - "optional": true, "requires": { "decompress-tar": "^4.0.0", "decompress-tarbz2": "^4.0.0", @@ -3565,7 +3552,6 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", "dev": true, - "optional": true, "requires": { "pify": "^3.0.0" }, @@ -3574,8 +3560,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true + "dev": true } } } @@ -3586,7 +3571,6 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", "dev": true, - "optional": true, "requires": { "mimic-response": "^1.0.0" } @@ -3596,7 +3580,6 @@ "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", "dev": true, - "optional": true, "requires": { "file-type": "^5.2.0", "is-stream": "^1.1.0", @@ -3607,8 +3590,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3617,7 +3599,6 @@ "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", "dev": true, - "optional": true, "requires": { "decompress-tar": "^4.1.0", "file-type": "^6.1.0", @@ -3630,8 +3611,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", - "dev": true, - "optional": true + "dev": true } } }, @@ -3640,7 +3620,6 @@ "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", "dev": true, - "optional": true, "requires": { "decompress-tar": "^4.1.1", "file-type": "^5.2.0", @@ -3651,8 +3630,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3661,7 +3639,6 @@ "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", "dev": true, - "optional": true, "requires": { "file-type": "^3.8.0", "get-stream": "^2.2.0", @@ -3673,15 +3650,13 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", - "dev": true, - "optional": true + "dev": true }, "get-stream": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", "dev": true, - "optional": true, "requires": { "object-assign": "^4.0.1", "pinkie-promise": "^2.0.0" @@ -3691,8 +3666,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3974,8 +3948,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3992,8 +3965,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true, - "optional": true + "dev": true }, "duplexify": { "version": "3.7.1", @@ -4596,7 +4568,6 @@ "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", "dev": true, - "optional": true, "requires": { "cross-spawn": "^5.0.1", "get-stream": "^3.0.0", @@ -4612,7 +4583,6 @@ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", "dev": true, - "optional": true, "requires": { "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", @@ -4752,7 +4722,6 @@ "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", "dev": true, - "optional": true, "requires": { "mime-db": "^1.28.0" } @@ -4762,7 +4731,6 @@ "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", "dev": true, - "optional": true, "requires": { "ext-list": "^2.0.0", "sort-keys-length": "^1.0.0" @@ -4997,7 +4965,6 @@ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", "dev": true, - "optional": true, "requires": { "pend": "~1.2.0" } @@ -5036,15 +5003,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", - "dev": true, - "optional": true + "dev": true }, "filenamify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz", "integrity": "sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==", "dev": true, - "optional": true, "requires": { "filename-reserved-regex": "^2.0.0", "strip-outer": "^1.0.0", @@ -5405,8 +5370,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha1-a+Dem+mYzhavivwkSXue6bfM2a0=", - "dev": true, - "optional": true + "dev": true }, "fs-mkdirp-stream": { "version": "1.0.0", @@ -5453,8 +5417,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -5475,14 +5438,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5497,20 +5458,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -5627,8 +5585,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -5640,7 +5597,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5655,7 +5611,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5663,14 +5618,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5689,7 +5642,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5770,8 +5722,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -5783,7 +5734,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5869,8 +5819,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -5906,7 +5855,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5926,7 +5874,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5970,14 +5917,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -6004,7 +5949,6 @@ "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", "integrity": "sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==", "dev": true, - "optional": true, "requires": { "npm-conf": "^1.1.0" } @@ -6013,15 +5957,13 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true, - "optional": true + "dev": true }, "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true, - "optional": true + "dev": true }, "get-value": { "version": "2.0.6", @@ -6336,8 +6278,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", - "dev": true, - "optional": true + "dev": true }, "growly": { "version": "1.3.0", @@ -7108,8 +7049,7 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", - "dev": true, - "optional": true + "dev": true }, "has-symbols": { "version": "1.0.0", @@ -7122,7 +7062,6 @@ "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", "dev": true, - "optional": true, "requires": { "has-symbol-support-x": "^1.4.1" } @@ -7322,8 +7261,7 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true, - "optional": true + "dev": true }, "ignore": { "version": "4.0.6", @@ -7453,7 +7391,6 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", "dev": true, - "optional": true, "requires": { "repeating": "^2.0.0" } @@ -7781,7 +7718,6 @@ "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7834,8 +7770,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", - "dev": true, - "optional": true + "dev": true }, "is-negated-glob": { "version": "1.0.0", @@ -7873,15 +7808,13 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", - "dev": true, - "optional": true + "dev": true }, "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true, - "optional": true + "dev": true }, "is-plain-object": { "version": "2.0.4", @@ -7951,15 +7884,13 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", - "dev": true, - "optional": true + "dev": true }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true, - "optional": true + "dev": true }, "is-svg": { "version": "3.0.0", @@ -8056,7 +7987,6 @@ "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", "dev": true, - "optional": true, "requires": { "has-to-string-tag-x": "^1.2.0", "is-object": "^1.0.1" @@ -8969,8 +8899,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", "integrity": "sha1-b54wtHCE2XGnyCD/FabFFnt0wm8=", - "dev": true, - "optional": true + "dev": true }, "lpad-align": { "version": "1.1.2", @@ -9040,8 +8969,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true, - "optional": true + "dev": true }, "map-visit": { "version": "1.0.0", @@ -9211,8 +9139,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "optional": true + "dev": true }, "minimatch": { "version": "3.0.4", @@ -9435,9 +9362,9 @@ "dev": true }, "nouislider": { - "version": "14.6.3", - "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-14.6.3.tgz", - "integrity": "sha512-/3tAqsWY2JYW9vd7bC14bFRA1P9A+pRHOtKmoMsyfnB0fQcd1UFx2pdY1Ey5wAUzTnXTesmYaEo/ecLVETijIQ==" + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-14.6.4.tgz", + "integrity": "sha512-PVCGYl+aC7/nVEbW61ypJWfuW3UCpvctz/luxpt4byxxli1FFyjBX9NIiy4Yak9AaO6a5BkPGfFYMCW4eg3eeQ==" }, "now-and-later": { "version": "2.0.1", @@ -12547,7 +12474,6 @@ "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", "dev": true, - "optional": true, "requires": { "config-chain": "^1.1.11", "pify": "^3.0.0" @@ -12557,8 +12483,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -12567,7 +12492,6 @@ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "dev": true, - "optional": true, "requires": { "path-key": "^2.0.0" } @@ -12929,8 +12853,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true, - "optional": true + "dev": true }, "p-is-promise": { "version": "1.1.0", @@ -12967,7 +12890,6 @@ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", "integrity": "sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=", "dev": true, - "optional": true, "requires": { "p-finally": "^1.0.0" } @@ -13158,8 +13080,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", - "dev": true, - "optional": true + "dev": true }, "performance-now": { "version": "2.1.0", @@ -13667,8 +13588,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", - "dev": true, - "optional": true + "dev": true }, "prr": { "version": "1.0.1", @@ -14024,7 +13944,6 @@ "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "dev": true, - "optional": true, "requires": { "is-finite": "^1.0.0" } @@ -14386,7 +14305,6 @@ "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", "dev": true, - "optional": true, "requires": { "commander": "~2.8.1" } @@ -14789,7 +14707,6 @@ "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", "dev": true, - "optional": true, "requires": { "is-plain-obj": "^1.0.0" } @@ -14799,7 +14716,6 @@ "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", "dev": true, - "optional": true, "requires": { "sort-keys": "^1.0.0" } @@ -15130,7 +15046,6 @@ "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", "dev": true, - "optional": true, "requires": { "is-natural-number": "^4.0.1" } @@ -15139,8 +15054,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true, - "optional": true + "dev": true }, "strip-indent": { "version": "1.0.1", @@ -15163,7 +15077,6 @@ "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", "integrity": "sha1-sv0qv2YEudHmATBXGV34Nrip1jE=", "dev": true, - "optional": true, "requires": { "escape-string-regexp": "^1.0.2" } @@ -15289,7 +15202,6 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", "integrity": "sha1-jqVdqzeXIlPZqa+Q/c1VmuQ1xVU=", "dev": true, - "optional": true, "requires": { "bl": "^1.0.0", "buffer-alloc": "^1.2.0", @@ -15304,15 +15216,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, - "optional": true + "dev": true }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", "dev": true, - "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -15328,7 +15238,6 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", "dev": true, - "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -15339,15 +15248,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", - "dev": true, - "optional": true + "dev": true }, "tempfile": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-2.0.0.tgz", "integrity": "sha1-awRGhWqbERTRhW/8vlCczLCXcmU=", "dev": true, - "optional": true, "requires": { "temp-dir": "^1.0.0", "uuid": "^3.0.1" @@ -15442,8 +15349,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", - "dev": true, - "optional": true + "dev": true }, "timers-ext": { "version": "0.1.7", @@ -15500,8 +15406,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", "integrity": "sha1-STvUj2LXxD/N7TE6A9ytsuEhOoA=", - "dev": true, - "optional": true + "dev": true }, "to-fast-properties": { "version": "2.0.0", @@ -15605,7 +15510,6 @@ "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", "dev": true, - "optional": true, "requires": { "escape-string-regexp": "^1.0.2" } @@ -15742,7 +15646,6 @@ "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz", "integrity": "sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg==", "dev": true, - "optional": true, "requires": { "buffer": "^5.2.1", "through": "^2.3.8" @@ -15943,8 +15846,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", - "dev": true, - "optional": true + "dev": true }, "use": { "version": "3.1.1", @@ -16440,7 +16342,6 @@ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", "dev": true, - "optional": true, "requires": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 6514f2f217..d0f1c1cb99 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -54,7 +54,7 @@ "@babel/core": "7.6.4", "@babel/preset-env": "7.6.3", "autoprefixer": "9.6.5", - "caniuse-lite": "^1.0.30001037", + "caniuse-lite": "^1.0.30001237", "cssnano": "4.1.10", "fs": "0.0.2", "gulp": "4.0.2", @@ -77,8 +77,8 @@ "jasmine-core": "3.5.0", "jsdom": "16.4.0", "karma": "4.4.1", - "karma-jsdom-launcher": "^8.0.2", "karma-jasmine": "2.0.1", + "karma-jsdom-launcher": "^8.0.2", "karma-junit-reporter": "2.0.1", "karma-spec-reporter": "0.0.32", "less": "3.10.3", From b97f6dcb2fad9d57f26295d2dc79e4c3bd621f57 Mon Sep 17 00:00:00 2001 From: Lucas Michaelsen <69140700+lucasmichaelsengorm@users.noreply.github.com> Date: Tue, 15 Jun 2021 01:09:26 +0200 Subject: [PATCH 218/289] HTML Symantic error UL > LI >LI changes to UL > LI > UL > LI (#10455) * HTML Symantic error UL > LI >LI changes to UL > LI > UL > LI - ScreenReader reads propperly * ensurce render if condition is meet. If any child render a subset of unorder list withs is list-items --- .../src/views/components/tree/umb-tree.html | 71 ++++++++++--------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree.html b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree.html index 59d67ee2e1..e50707f2d4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree.html @@ -13,26 +13,28 @@ - +
    - - + +
      + + +
    @@ -51,26 +53,27 @@ - + - - +
      + + +
    From efeda47e30e395635e5219a0ca85fc7a14d9cfdb Mon Sep 17 00:00:00 2001 From: patrickdemooij9 Date: Tue, 15 Jun 2021 01:22:18 +0200 Subject: [PATCH 219/289] 10430: Fix typo (#10440) * 10430: Fix typo * 10430: ng-disabled instead of disabled --- .../mediapicker3/umb-media-picker3-property-editor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html index aa9f50b7df..cb6d9e5e26 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html @@ -42,7 +42,7 @@ id="{{vm.model.alias}}" type="button" class="btn-reset umb-media-card-grid__create-button umb-outline" - disbled="!vm.allowAdd" + ng-disabled="!vm.allowAdd" ng-click="vm.addMediaAt(vm.model.value.length, $event)">
    From 348d1676cd7b82a70f82496619ad34722c0bdadf Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sat, 29 May 2021 07:29:00 +0200 Subject: [PATCH 220/289] Handle only single content type event to avoid reloading the content and media cache multiple times after a deployment containing more than one changed document type. --- .../Cache/DistributedCacheBinderTests.cs | 16 +++++- .../Cache/DistributedCacheBinder.cs | 54 +++++++++++++++++-- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs b/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs index e446e049b6..00a33c0b6c 100644 --- a/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs +++ b/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; +using Umbraco.Core.Services.Changes; using Umbraco.Tests.Testing; using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web; @@ -148,7 +149,6 @@ namespace Umbraco.Tests.Cache { // works because that event definition maps to an empty handler new EventDefinition>(null, Current.Services.ContentTypeService, new SaveEventArgs(Enumerable.Empty()), "Saved"), - }; var umbracoContextFactory = new UmbracoContextFactory( @@ -166,5 +166,19 @@ namespace Umbraco.Tests.Cache var refreshers = new DistributedCacheBinder(null, umbracoContextFactory, null); refreshers.HandleEvents(definitions); } + + [Test] + public void OnlyHandlesOnContentTypeEvent() + { + var definitions = new IEventDefinition[] + { + new EventDefinition.EventArgs>(null, Current.Services.ContentTypeService, new ContentTypeChange.EventArgs(Enumerable.Empty>()), "Changed"), + new EventDefinition>(null, Current.Services.ContentTypeService, new SaveEventArgs(Enumerable.Empty()), "Saved"), + new EventDefinition.EventArgs>(null, Current.Services.ContentTypeService, new ContentTypeChange.EventArgs(Enumerable.Empty>()), "Changed"), + new EventDefinition>(null, Current.Services.ContentTypeService, new SaveEventArgs(Enumerable.Empty()), "Saved"), + }; + var result = DistributedCacheBinder.GetReducedEventList(definitions); + Assert.AreEqual(1, result.Count()); + } } } diff --git a/src/Umbraco.Web/Cache/DistributedCacheBinder.cs b/src/Umbraco.Web/Cache/DistributedCacheBinder.cs index 3ee24a23bf..e3a5a01d54 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheBinder.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheBinder.cs @@ -61,11 +61,15 @@ namespace Umbraco.Web.Cache /// public void HandleEvents(IEnumerable events) { - // ensure we run with an UmbracoContext, because this may run in a background task, - // yet developers may be using the 'current' UmbracoContext in the event handlers + // Ensure we run with an UmbracoContext, because this may run in a background task, + // yet developers may be using the 'current' UmbracoContext in the event handlers. using (_umbracoContextFactory.EnsureUmbracoContext()) { - foreach (var e in events) + // When it comes to content types types, a change to any single one will trigger a reload of the content and media caches. + // As far as I (AB) can tell, there's no type specific logic here, they all clear caches for all content types, and trigger a reload of all content and media. + // We also have events registered for Changed and Saved, which do the same thing, so really only need one of these. + // Hence if we have more than one document or media types, we can and should only handle one of the events for one, to avoid repeated cache reloads. + foreach (var e in GetReducedEventList(events)) { var handler = FindHandler(e); if (handler == null) @@ -80,5 +84,49 @@ namespace Umbraco.Web.Cache } } } + + // Internal for tests + internal static IEnumerable GetReducedEventList(IEnumerable events) + { + var reducedEvents = new List(); + + var gotDoumentType = false; + var gotMediaType = false; + var gotMemberType = false; + + foreach (var evt in events) + { + if (evt.Sender.ToString().Contains(nameof(Core.Services.Implement.ContentTypeService))) + { + if (gotDoumentType == false) + { + reducedEvents.Add(evt); + gotDoumentType = true; + } + } + else if (evt.Sender.ToString().Contains(nameof(Core.Services.Implement.MediaTypeService))) + { + if (gotMediaType == false) + { + reducedEvents.Add(evt); + gotMediaType = true; + } + } + else if (evt.Sender.ToString().Contains(nameof(Core.Services.Implement.MemberTypeService))) + { + if (gotMemberType == false) + { + reducedEvents.Add(evt); + gotMemberType = true; + } + } + else + { + reducedEvents.Add(evt); + } + } + + return reducedEvents; + } } } From dc334c10154820cf0958e901dd6c00906084727a Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 16 Jun 2021 10:00:29 +0100 Subject: [PATCH 221/289] Automated install user with Environment Variables & unattended.user.json (#9930) * Try to update admin user unattended This will fail because we're not in install runtime state * Create a new user instead of trying to update the default admin * Create a new user instead of trying to update the default admin * Use same logic from NewInstallStep to modify the SuperUser aka -1 * Add back stuff after merge conflict from v8/dev * Add event to be raised * Trying to wire up events * Remove commented out code - just need to figure out why event is not hit/triggered * Read Appsettings as opposed to ENV variables * Use a JSON file that deletes itself as storing secrets in web.config will be accidently committed * Remove component based event - Component were only initialized after DB creation * Move UnattendedInstall down after _factory * Remove commented out code * Fixed issue where upgrader UI would show up - needed to recheck the Runtimelevel after UnattenedInstall * Apply suggestions from code review - Thanks Marc :) Co-authored-by: Marc Goodson Co-authored-by: Mole Co-authored-by: Marc Goodson --- .../Events/UnattendedInstallEventArgs.cs | 9 ++ src/Umbraco.Core/Runtime/CoreRuntime.cs | 137 +++++++++++++++++- src/Umbraco.Core/Umbraco.Core.csproj | 1 + 3 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 src/Umbraco.Core/Events/UnattendedInstallEventArgs.cs diff --git a/src/Umbraco.Core/Events/UnattendedInstallEventArgs.cs b/src/Umbraco.Core/Events/UnattendedInstallEventArgs.cs new file mode 100644 index 0000000000..3029126dea --- /dev/null +++ b/src/Umbraco.Core/Events/UnattendedInstallEventArgs.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Core.Events +{ + /// + /// Used to notify that an Unattended install has completed + /// + public class UnattendedInstallEventArgs : System.ComponentModel.CancelEventArgs + { + } +} diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index 25bb5d3151..4345469f54 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -1,13 +1,17 @@ -using System; +using Newtonsoft.Json; +using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; +using System.Runtime.Serialization; using System.Threading; using System.Web; using System.Web.Hosting; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; +using Umbraco.Core.Events; using Umbraco.Core.Exceptions; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -16,6 +20,9 @@ using Umbraco.Core.Migrations.Install; using Umbraco.Core.Migrations.Upgrade; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Mappers; +using Umbraco.Core.Scoping; +using Umbraco.Core.Security; +using Umbraco.Core.Services; using Umbraco.Core.Sync; namespace Umbraco.Core.Runtime @@ -119,6 +126,9 @@ namespace Umbraco.Core.Runtime try { + // Setup event listener + UnattendedInstalled += CoreRuntime_UnattendedInstalled; + // throws if not full-trust new AspNetHostingPermission(AspNetHostingPermissionLevel.Unrestricted).Demand(); @@ -162,8 +172,7 @@ namespace Umbraco.Core.Runtime // run handlers RuntimeOptions.DoRuntimeEssentials(composition, appCaches, typeLoader, databaseFactory); - // determines if unattended install is enabled and performs it if required - DoUnattendedInstall(databaseFactory); + // register runtime-level services // there should be none, really - this is here "just in case" @@ -190,6 +199,13 @@ namespace Umbraco.Core.Runtime // create the factory _factory = Current.Factory = composition.CreateFactory(); + // determines if unattended install is enabled and performs it if required + DoUnattendedInstall(databaseFactory); + + // determine our runtime level (AFTER UNATTENDED INSTALL) + // TODO: Feels kinda weird to call this again + DetermineRuntimeLevel(databaseFactory, ProfilingLogger); + // if level is Run and reason is UpgradeMigrations, that means we need to perform an unattended upgrade if (_state.Reason == RuntimeLevelReason.UpgradeMigrations && _state.Level == RuntimeLevel.Run) { @@ -203,8 +219,6 @@ namespace Umbraco.Core.Runtime // create & initialize the components _components = _factory.GetInstance(); _components.Initialize(); - - } catch (Exception e) { @@ -242,6 +256,93 @@ namespace Umbraco.Core.Runtime return _factory; } + private void CoreRuntime_UnattendedInstalled(IRuntime sender, UnattendedInstallEventArgs e) + { + var unattendedName = Environment.GetEnvironmentVariable("UnattendedUserName"); + var unattendedEmail = Environment.GetEnvironmentVariable("UnattendedUserEmail"); + var unattendedPassword = Environment.GetEnvironmentVariable("UnattendedUserPassword"); + + var fileExists = false; + var filePath = IOHelper.MapPath("~/App_Data/unattended.user.json"); + + // No values store in ENV vars - try fallback file of /app_data/unattended.user.json + if (unattendedName.IsNullOrWhiteSpace() + || unattendedEmail.IsNullOrWhiteSpace() + || unattendedPassword.IsNullOrWhiteSpace()) + { + + fileExists = File.Exists(filePath); + if (fileExists == false) + { + return; + } + + // Attempt to deserialize JSON + try + { + var fileContents = File.ReadAllText(filePath); + var credentials = JsonConvert.DeserializeObject(fileContents); + + unattendedName = credentials.Name; + unattendedEmail = credentials.Email; + unattendedPassword = credentials.Password; + } + catch (Exception ex) + { + + throw; + } + } + + // ENV Variables & JSON still empty + if (unattendedName.IsNullOrWhiteSpace() + || unattendedEmail.IsNullOrWhiteSpace() + || unattendedPassword.IsNullOrWhiteSpace()) + { + return; + } + + + // Update user details + var currentProvider = MembershipProviderExtensions.GetUsersMembershipProvider(); + var admin = Current.Services.UserService.GetUserById(Constants.Security.SuperUserId); + if (admin == null) + { + throw new InvalidOperationException("Could not find the super user!"); + } + + var membershipUser = currentProvider.GetUser(Constants.Security.SuperUserId, true); + if (membershipUser == null) + { + throw new InvalidOperationException($"No user found in membership provider with id of {Constants.Security.SuperUserId}."); + } + + try + { + var success = membershipUser.ChangePassword("default", unattendedPassword.Trim()); + if (success == false) + { + throw new FormatException("Password must be at least " + currentProvider.MinRequiredPasswordLength + " characters long and contain at least " + currentProvider.MinRequiredNonAlphanumericCharacters + " symbols"); + } + } + catch (Exception) + { + throw new FormatException("Password must be at least " + currentProvider.MinRequiredPasswordLength + " characters long and contain at least " + currentProvider.MinRequiredNonAlphanumericCharacters + " symbols"); + } + + admin.Email = unattendedEmail.Trim(); + admin.Name = unattendedName.Trim(); + admin.Username = unattendedEmail.Trim(); + + Current.Services.UserService.Save(admin); + + // Delete JSON file if it existed to tidy + if (fileExists) + { + File.Delete(filePath); + } + } + private void DoUnattendedInstall(IUmbracoDatabaseFactory databaseFactory) { // unattended install is not enabled @@ -285,6 +386,11 @@ namespace Umbraco.Core.Runtime var creator = new DatabaseSchemaCreator(database, Logger); creator.InitializeDatabaseSchema(); database.CompleteTransaction(); + + // Emit an event that unattended install completed + // Then this event can be listened for and create an unattended user + UnattendedInstalled?.Invoke(this, new UnattendedInstallEventArgs()); + Logger.Info("Unattended install completed."); } catch (Exception ex) @@ -397,6 +503,7 @@ namespace Umbraco.Core.Runtime public virtual void Terminate() { _components?.Terminate(); + UnattendedInstalled -= CoreRuntime_UnattendedInstalled; } /// @@ -404,7 +511,7 @@ namespace Umbraco.Core.Runtime /// public virtual void Compose(Composition composition) { - // nothing + // Nothing } #region Getters @@ -465,5 +572,23 @@ namespace Umbraco.Core.Runtime } #endregion + + /// + /// Event to be used to notify when the Unattended Install has finished + /// + public static event TypedEventHandler UnattendedInstalled; + + [DataContract] + public class UnattendedUserConfig + { + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "email")] + public string Email { get; set; } + + [DataMember(Name = "password")] + public string Password { get; set; } + } } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 0a453ad75f..14444f5d59 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -131,6 +131,7 @@ + From 585f7bb57135ab31e36b782b78e0e151d8503625 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 17 Jun 2021 09:40:07 +0200 Subject: [PATCH 222/289] Make dashboards support deep linking (#10283) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ronald Barendse Co-authored-by: Niels Lyngsø --- .../src/views/common/dashboard.controller.js | 59 +++++++++++++------ 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dashboard.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dashboard.controller.js index e788e6fc9b..d7d5153956 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dashboard.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dashboard.controller.js @@ -7,38 +7,59 @@ * Controls the dashboards of the application * */ - -function DashboardController($scope, $routeParams, dashboardResource, localizationService) { + +function DashboardController($scope, $q, $routeParams, $location, dashboardResource, localizationService) { + const DASHBOARD_QUERY_PARAM = 'dashboard'; $scope.page = {}; $scope.page.nameLocked = true; $scope.page.loading = true; $scope.dashboard = {}; - localizationService.localize("sections_" + $routeParams.section).then(function(name){ - $scope.dashboard.name = name; - }); - - dashboardResource.getDashboard($routeParams.section).then(function(tabs){ - $scope.dashboard.tabs = tabs; - - // set first tab to active - if($scope.dashboard.tabs && $scope.dashboard.tabs.length > 0) { - $scope.dashboard.tabs[0].active = true; - } + var promises = []; + + promises.push(localizationService.localize("sections_" + $routeParams.section).then(function (name) { + $scope.dashboard.name = name; + })); + + promises.push(dashboardResource.getDashboard($routeParams.section).then(function (tabs) { + $scope.dashboard.tabs = tabs; + + if ($scope.dashboard.tabs && $scope.dashboard.tabs.length > 0) { + initActiveTab(); + } + })); + + $q.all(promises).then(function () { $scope.page.loading = false; }); - $scope.changeTab = function(tab) { - $scope.dashboard.tabs.forEach(function(tab) { - tab.active = false; - }); + $scope.changeTab = function (tab) { + if ($scope.dashboard.tabs && $scope.dashboard.tabs.length > 0) { + $scope.dashboard.tabs.forEach(function (tab) { + tab.active = false; + }); + } + tab.active = true; + $location.search(DASHBOARD_QUERY_PARAM, tab.alias); }; + function initActiveTab() { + // Check the query parameter for a dashboard alias + const dashboardAlias = $location.search()[DASHBOARD_QUERY_PARAM]; + const dashboardIndex = $scope.dashboard.tabs.findIndex(tab => tab.alias === dashboardAlias); + + // Set the first dashboard to active if there is no query parameter or we can't find a matching dashboard for the alias + const activeIndex = dashboardIndex !== -1 ? dashboardIndex : 0; + + const tab = $scope.dashboard.tabs[activeIndex]; + + tab.active = true; + $location.search(DASHBOARD_QUERY_PARAM, tab.alias); + } } - -//register it +// Register it angular.module('umbraco').controller("Umbraco.DashboardController", DashboardController); From f303fa0cc3c0209695e7b718f95274386591e8be Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Fri, 18 Jun 2021 09:51:23 +0200 Subject: [PATCH 223/289] Make sure we can build in VS2022 --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index f3652a1273..d3e8a355c9 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -319,7 +319,7 @@ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v12.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v14.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v15.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v16.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v17.0 @@ -329,8 +329,7 @@ $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v14.0\Web\Microsoft.Web.Publishing.Tasks.dll $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v15.0\Web\Microsoft.Web.Publishing.Tasks.dll $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v16.0\Web\Microsoft.Web.Publishing.Tasks.dll - - $(ProgramFiles32)\Microsoft Visual Studio\2019\Preview\MSBuild\Microsoft\VisualStudio\v16.0\Web\Microsoft.Web.Publishing.Tasks.dll + $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v17.0\Web\Microsoft.Web.Publishing.Tasks.dll @@ -436,4 +435,4 @@ - \ No newline at end of file + From 2c7413693b9edd1bd34475179f033c356c0fd666 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Fri, 18 Jun 2021 11:03:21 +0200 Subject: [PATCH 224/289] Prefix the new media types to stop them from clashing when people make document types with that alias (#10483) --- src/Umbraco.Core/Constants-Conventions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index c8233c8d34..da2d2cd0ec 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -121,22 +121,22 @@ namespace Umbraco.Core /// /// MediaType alias for a video. /// - public const string Video = "Video"; + public const string Video = "umbracoMediaVideo"; /// /// MediaType alias for an audio. /// - public const string Audio = "Audio"; + public const string Audio = "umbracoMediaAudio"; /// /// MediaType alias for an article. /// - public const string Article = "Article"; + public const string Article = "umbracoMediaArticle"; /// /// MediaType alias for vector graphics. /// - public const string VectorGraphics = "VectorGraphics"; + public const string VectorGraphics = "umbracoMediaVectorGraphics"; /// /// MediaType alias indicating allowing auto-selection. From 64ebbae5b8bfe37113d95231ec85685cf56cbb22 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Fri, 18 Jun 2021 11:37:32 +0200 Subject: [PATCH 225/289] Keep the old constants as they were and add new "Name" constants, in case people were already using the old constants to query by alias --- src/Umbraco.Core/Constants-Conventions.cs | 20 +++++++++++++++++++ .../Migrations/Install/DatabaseDataCreator.cs | 8 ++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index da2d2cd0ec..98df39202c 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -118,6 +118,26 @@ namespace Umbraco.Core /// public const string Image = "Image"; + /// + /// MediaType name for a video. + /// + public const string VideoName = "Video"; + + /// + /// MediaType name for an audio. + /// + public const string AudioName = "Audio"; + + /// + /// MediaType name for an article. + /// + public const string ArticleName = "Article"; + + /// + /// MediaType name for vector graphics. + /// + public const string VectorGraphicsName = "VectorGraphics"; + /// /// MediaType alias for a video. /// diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 6a56141491..a8d089dd05 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -130,10 +130,10 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1031, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1031", SortOrder = 2, UniqueId = new Guid("f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d"), Text = Constants.Conventions.MediaTypes.Folder, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1032, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1032", SortOrder = 2, UniqueId = new Guid("cc07b313-0843-4aa8-bbda-871c8da728c8"), Text = Constants.Conventions.MediaTypes.Image, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1033, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1033", SortOrder = 2, UniqueId = new Guid("4c52d8ab-54e6-40cd-999c-7a5f24903e4d"), Text = Constants.Conventions.MediaTypes.File, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1034, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1034", SortOrder = 2, UniqueId = new Guid("f6c515bb-653c-4bdc-821c-987729ebe327"), Text = Constants.Conventions.MediaTypes.Video, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1035, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1035", SortOrder = 2, UniqueId = new Guid("a5ddeee0-8fd8-4cee-a658-6f1fcdb00de3"), Text = Constants.Conventions.MediaTypes.Audio, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1036, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1036", SortOrder = 2, UniqueId = new Guid("a43e3414-9599-4230-a7d3-943a21b20122"), Text = Constants.Conventions.MediaTypes.Article, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1037, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1037", SortOrder = 2, UniqueId = new Guid("c4b1efcf-a9d5-41c4-9621-e9d273b52a9c"), Text = "Vector Graphics (SVG)", NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1034, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1034", SortOrder = 2, UniqueId = new Guid("f6c515bb-653c-4bdc-821c-987729ebe327"), Text = Constants.Conventions.MediaTypes.VideoName, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1035, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1035", SortOrder = 2, UniqueId = new Guid("a5ddeee0-8fd8-4cee-a658-6f1fcdb00de3"), Text = Constants.Conventions.MediaTypes.AudioName, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1036, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1036", SortOrder = 2, UniqueId = new Guid("a43e3414-9599-4230-a7d3-943a21b20122"), Text = Constants.Conventions.MediaTypes.ArticleName, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1037, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1037", SortOrder = 2, UniqueId = new Guid("c4b1efcf-a9d5-41c4-9621-e9d273b52a9c"), Text = Constants.Conventions.MediaTypes.VectorGraphicsName, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Tags, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Tags}", SortOrder = 2, UniqueId = new Guid("b6b73142-b9c1-4bf8-a16d-e1c23320b549"), Text = "Tags", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.ImageCropper, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.ImageCropper}", SortOrder = 2, UniqueId = new Guid("1df9f033-e6d4-451f-b8d2-e0cbc50a836f"), Text = "Image Cropper", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1044, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1044", SortOrder = 0, UniqueId = new Guid("d59be02f-1df9-4228-aa1e-01917d806cda"), Text = Constants.Conventions.MemberTypes.DefaultAlias, NodeObjectType = Constants.ObjectTypes.MemberType, CreateDate = DateTime.Now }); From 70590ce9934d821292979da6f22efb8e0160ec81 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Fri, 18 Jun 2021 11:42:09 +0200 Subject: [PATCH 226/289] Invert the previous change so querying with these constants will give the same results as in 8.14.0 --- src/Umbraco.Core/Constants-Conventions.cs | 16 ++++++++-------- .../Migrations/Install/DatabaseDataCreator.cs | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index 98df39202c..37267a5e22 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -121,42 +121,42 @@ namespace Umbraco.Core /// /// MediaType name for a video. /// - public const string VideoName = "Video"; + public const string Video = "Video"; /// /// MediaType name for an audio. /// - public const string AudioName = "Audio"; + public const string Audio = "Audio"; /// /// MediaType name for an article. /// - public const string ArticleName = "Article"; + public const string Article = "Article"; /// /// MediaType name for vector graphics. /// - public const string VectorGraphicsName = "VectorGraphics"; + public const string VectorGraphics = "VectorGraphics"; /// /// MediaType alias for a video. /// - public const string Video = "umbracoMediaVideo"; + public const string VideoAlias = "umbracoMediaVideo"; /// /// MediaType alias for an audio. /// - public const string Audio = "umbracoMediaAudio"; + public const string AudioAlias = "umbracoMediaAudio"; /// /// MediaType alias for an article. /// - public const string Article = "umbracoMediaArticle"; + public const string ArticleAlias = "umbracoMediaArticle"; /// /// MediaType alias for vector graphics. /// - public const string VectorGraphics = "umbracoMediaVectorGraphics"; + public const string VectorGraphicsAlias = "umbracoMediaVectorGraphics"; /// /// MediaType alias indicating allowing auto-selection. diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index a8d089dd05..55ac3a4ec9 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -130,10 +130,10 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1031, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1031", SortOrder = 2, UniqueId = new Guid("f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d"), Text = Constants.Conventions.MediaTypes.Folder, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1032, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1032", SortOrder = 2, UniqueId = new Guid("cc07b313-0843-4aa8-bbda-871c8da728c8"), Text = Constants.Conventions.MediaTypes.Image, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1033, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1033", SortOrder = 2, UniqueId = new Guid("4c52d8ab-54e6-40cd-999c-7a5f24903e4d"), Text = Constants.Conventions.MediaTypes.File, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1034, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1034", SortOrder = 2, UniqueId = new Guid("f6c515bb-653c-4bdc-821c-987729ebe327"), Text = Constants.Conventions.MediaTypes.VideoName, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1035, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1035", SortOrder = 2, UniqueId = new Guid("a5ddeee0-8fd8-4cee-a658-6f1fcdb00de3"), Text = Constants.Conventions.MediaTypes.AudioName, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1036, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1036", SortOrder = 2, UniqueId = new Guid("a43e3414-9599-4230-a7d3-943a21b20122"), Text = Constants.Conventions.MediaTypes.ArticleName, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1037, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1037", SortOrder = 2, UniqueId = new Guid("c4b1efcf-a9d5-41c4-9621-e9d273b52a9c"), Text = Constants.Conventions.MediaTypes.VectorGraphicsName, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1034, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1034", SortOrder = 2, UniqueId = new Guid("f6c515bb-653c-4bdc-821c-987729ebe327"), Text = Constants.Conventions.MediaTypes.Video, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1035, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1035", SortOrder = 2, UniqueId = new Guid("a5ddeee0-8fd8-4cee-a658-6f1fcdb00de3"), Text = Constants.Conventions.MediaTypes.Audio, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1036, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1036", SortOrder = 2, UniqueId = new Guid("a43e3414-9599-4230-a7d3-943a21b20122"), Text = Constants.Conventions.MediaTypes.Article, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1037, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1037", SortOrder = 2, UniqueId = new Guid("c4b1efcf-a9d5-41c4-9621-e9d273b52a9c"), Text = Constants.Conventions.MediaTypes.VectorGraphics, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Tags, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Tags}", SortOrder = 2, UniqueId = new Guid("b6b73142-b9c1-4bf8-a16d-e1c23320b549"), Text = "Tags", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.ImageCropper, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.ImageCropper}", SortOrder = 2, UniqueId = new Guid("1df9f033-e6d4-451f-b8d2-e0cbc50a836f"), Text = "Image Cropper", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1044, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1044", SortOrder = 0, UniqueId = new Guid("d59be02f-1df9-4228-aa1e-01917d806cda"), Text = Constants.Conventions.MemberTypes.DefaultAlias, NodeObjectType = Constants.ObjectTypes.MemberType, CreateDate = DateTime.Now }); @@ -174,10 +174,10 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Constants.Conventions.MediaTypes.Folder, Icon = Constants.Icons.MediaFolder, Thumbnail = Constants.Icons.MediaFolder, IsContainer = false, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Constants.Conventions.MediaTypes.Image, Icon = Constants.Icons.MediaImage, Thumbnail = Constants.Icons.MediaImage, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Constants.Conventions.MediaTypes.File, Icon = Constants.Icons.MediaFile, Thumbnail = Constants.Icons.MediaFile, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 540, NodeId = 1034, Alias = Constants.Conventions.MediaTypes.Video, Icon = Constants.Icons.MediaVideo, Thumbnail = Constants.Icons.MediaVideo, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 541, NodeId = 1035, Alias = Constants.Conventions.MediaTypes.Audio, Icon = Constants.Icons.MediaAudio, Thumbnail = Constants.Icons.MediaAudio, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 542, NodeId = 1036, Alias = Constants.Conventions.MediaTypes.Article, Icon = Constants.Icons.MediaArticle, Thumbnail = Constants.Icons.MediaArticle, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 543, NodeId = 1037, Alias = Constants.Conventions.MediaTypes.VectorGraphics, Icon = Constants.Icons.MediaVectorGraphics, Thumbnail = Constants.Icons.MediaVectorGraphics, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 540, NodeId = 1034, Alias = Constants.Conventions.MediaTypes.VideoAlias, Icon = Constants.Icons.MediaVideo, Thumbnail = Constants.Icons.MediaVideo, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 541, NodeId = 1035, Alias = Constants.Conventions.MediaTypes.AudioAlias, Icon = Constants.Icons.MediaAudio, Thumbnail = Constants.Icons.MediaAudio, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 542, NodeId = 1036, Alias = Constants.Conventions.MediaTypes.ArticleAlias, Icon = Constants.Icons.MediaArticle, Thumbnail = Constants.Icons.MediaArticle, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 543, NodeId = 1037, Alias = Constants.Conventions.MediaTypes.VectorGraphicsAlias, Icon = Constants.Icons.MediaVectorGraphics, Thumbnail = Constants.Icons.MediaVectorGraphics, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Constants.Conventions.MemberTypes.DefaultAlias, Icon = Constants.Icons.Member, Thumbnail = Constants.Icons.Member, Variations = (byte) ContentVariation.Nothing }); } From e95b57ca1f992665eb951e8327125c9bbc3abd09 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Fri, 18 Jun 2021 11:44:26 +0200 Subject: [PATCH 227/289] Fixed friendly name of Vector graphics --- src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 55ac3a4ec9..9e6884efe0 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -133,7 +133,7 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1034, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1034", SortOrder = 2, UniqueId = new Guid("f6c515bb-653c-4bdc-821c-987729ebe327"), Text = Constants.Conventions.MediaTypes.Video, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1035, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1035", SortOrder = 2, UniqueId = new Guid("a5ddeee0-8fd8-4cee-a658-6f1fcdb00de3"), Text = Constants.Conventions.MediaTypes.Audio, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1036, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1036", SortOrder = 2, UniqueId = new Guid("a43e3414-9599-4230-a7d3-943a21b20122"), Text = Constants.Conventions.MediaTypes.Article, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1037, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1037", SortOrder = 2, UniqueId = new Guid("c4b1efcf-a9d5-41c4-9621-e9d273b52a9c"), Text = Constants.Conventions.MediaTypes.VectorGraphics, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1037, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1037", SortOrder = 2, UniqueId = new Guid("c4b1efcf-a9d5-41c4-9621-e9d273b52a9c"), Text = "Vector Graphics (SVG)", NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Tags, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Tags}", SortOrder = 2, UniqueId = new Guid("b6b73142-b9c1-4bf8-a16d-e1c23320b549"), Text = "Tags", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.ImageCropper, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.ImageCropper}", SortOrder = 2, UniqueId = new Guid("1df9f033-e6d4-451f-b8d2-e0cbc50a836f"), Text = "Image Cropper", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1044, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1044", SortOrder = 0, UniqueId = new Guid("d59be02f-1df9-4228-aa1e-01917d806cda"), Text = Constants.Conventions.MemberTypes.DefaultAlias, NodeObjectType = Constants.ObjectTypes.MemberType, CreateDate = DateTime.Now }); From 499d0ff5c113b0841e2514d255dcc8b7f47b1745 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Fri, 18 Jun 2021 12:32:01 +0200 Subject: [PATCH 228/289] Fix hardcoded aliases --- src/Umbraco.Tests/Services/MediaServiceTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Tests/Services/MediaServiceTests.cs b/src/Umbraco.Tests/Services/MediaServiceTests.cs index d5cec11211..a0d74c0144 100644 --- a/src/Umbraco.Tests/Services/MediaServiceTests.cs +++ b/src/Umbraco.Tests/Services/MediaServiceTests.cs @@ -165,7 +165,7 @@ namespace Umbraco.Tests.Services var mediaService = ServiceContext.MediaService; var mediaType = MockedContentTypes.CreateNewMediaType(); ServiceContext.MediaTypeService.Save(mediaType); - var media = mediaService.CreateMedia(string.Empty, -1, "video"); + var media = mediaService.CreateMedia(string.Empty, -1, Constants.Conventions.MediaTypes.VideoAlias); // Act & Assert Assert.Throws(() => mediaService.Save(media)); @@ -177,7 +177,7 @@ namespace Umbraco.Tests.Services var mediaService = ServiceContext.MediaService; var mediaType = MockedContentTypes.CreateNewMediaType(); ServiceContext.MediaTypeService.Save(mediaType); - var media = mediaService.CreateMedia("Test", -1, "video"); + var media = mediaService.CreateMedia("Test", -1, Constants.Conventions.MediaTypes.VideoAlias); mediaService.Save(media); From 3645539fe296c424f1d8afdcb7085261e868d2d7 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 14 Jun 2021 12:20:06 +0100 Subject: [PATCH 229/289] Run `npm update caniuse-lite` (cherry picked from commit e70c3628513ca720c4b4098f60a0a3a43df399f2) --- src/Umbraco.Web.UI.Client/package-lock.json | 205 +++++--------------- src/Umbraco.Web.UI.Client/package.json | 4 +- 2 files changed, 55 insertions(+), 154 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 3f53638fc6..b2d811df89 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -1836,8 +1836,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", - "dev": true, - "optional": true + "dev": true }, "base64id": { "version": "1.0.0", @@ -2084,7 +2083,6 @@ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", "dev": true, - "optional": true, "requires": { "p-finally": "^1.0.0" } @@ -2126,7 +2124,6 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha1-oWCRFxcQPAdBDO9j71Gzl8Alr5w=", "dev": true, - "optional": true, "requires": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" @@ -2136,15 +2133,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, - "optional": true + "dev": true }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", "dev": true, - "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -2160,7 +2155,6 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", "dev": true, - "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -2301,7 +2295,6 @@ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz", "integrity": "sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==", "dev": true, - "optional": true, "requires": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" @@ -2327,8 +2320,7 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true, - "optional": true + "dev": true }, "buffer-equal": { "version": "1.0.0", @@ -2503,9 +2495,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001168", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001168.tgz", - "integrity": "sha512-P2zmX7swIXKu+GMMR01TWa4csIKELTNnZKc+f1CjebmZJQtTAEXmpQSoKVJVVcvPGAA0TEYTOUp3VehavZSFPQ==", + "version": "1.0.30001237", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001237.tgz", + "integrity": "sha512-pDHgRndit6p1NR2GhzMbQ6CkRrp4VKuSsqbcLeOQppYPKOYkKT/6ZvZDvKJUqcmtyWIAHuZq3SVS2vc1egCZzw==", "dev": true }, "caseless": { @@ -2519,7 +2511,6 @@ "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", "integrity": "sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==", "dev": true, - "optional": true, "requires": { "get-proxy": "^2.0.0", "isurl": "^1.0.0-alpha5", @@ -2953,7 +2944,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", "dev": true, - "optional": true, "requires": { "graceful-readlink": ">= 1.0.0" } @@ -3048,7 +3038,6 @@ "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", "dev": true, - "optional": true, "requires": { "ini": "^1.3.4", "proto-list": "~1.2.1" @@ -3104,7 +3093,6 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", "dev": true, - "optional": true, "requires": { "safe-buffer": "5.1.2" } @@ -3548,7 +3536,6 @@ "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.0.tgz", "integrity": "sha1-eu3YVCflqS2s/lVnSnxQXpbQH50=", "dev": true, - "optional": true, "requires": { "decompress-tar": "^4.0.0", "decompress-tarbz2": "^4.0.0", @@ -3565,7 +3552,6 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", "dev": true, - "optional": true, "requires": { "pify": "^3.0.0" }, @@ -3574,8 +3560,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true + "dev": true } } } @@ -3586,7 +3571,6 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", "dev": true, - "optional": true, "requires": { "mimic-response": "^1.0.0" } @@ -3596,7 +3580,6 @@ "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", "dev": true, - "optional": true, "requires": { "file-type": "^5.2.0", "is-stream": "^1.1.0", @@ -3607,8 +3590,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3617,7 +3599,6 @@ "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", "dev": true, - "optional": true, "requires": { "decompress-tar": "^4.1.0", "file-type": "^6.1.0", @@ -3630,8 +3611,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", - "dev": true, - "optional": true + "dev": true } } }, @@ -3640,7 +3620,6 @@ "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", "dev": true, - "optional": true, "requires": { "decompress-tar": "^4.1.1", "file-type": "^5.2.0", @@ -3651,8 +3630,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3661,7 +3639,6 @@ "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", "dev": true, - "optional": true, "requires": { "file-type": "^3.8.0", "get-stream": "^2.2.0", @@ -3673,15 +3650,13 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", - "dev": true, - "optional": true + "dev": true }, "get-stream": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", "dev": true, - "optional": true, "requires": { "object-assign": "^4.0.1", "pinkie-promise": "^2.0.0" @@ -3691,8 +3666,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3974,8 +3948,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3992,8 +3965,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true, - "optional": true + "dev": true }, "duplexify": { "version": "3.7.1", @@ -4596,7 +4568,6 @@ "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", "dev": true, - "optional": true, "requires": { "cross-spawn": "^5.0.1", "get-stream": "^3.0.0", @@ -4612,7 +4583,6 @@ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", "dev": true, - "optional": true, "requires": { "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", @@ -4752,7 +4722,6 @@ "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", "dev": true, - "optional": true, "requires": { "mime-db": "^1.28.0" } @@ -4762,7 +4731,6 @@ "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", "dev": true, - "optional": true, "requires": { "ext-list": "^2.0.0", "sort-keys-length": "^1.0.0" @@ -4997,7 +4965,6 @@ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", "dev": true, - "optional": true, "requires": { "pend": "~1.2.0" } @@ -5036,15 +5003,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", - "dev": true, - "optional": true + "dev": true }, "filenamify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz", "integrity": "sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==", "dev": true, - "optional": true, "requires": { "filename-reserved-regex": "^2.0.0", "strip-outer": "^1.0.0", @@ -5405,8 +5370,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha1-a+Dem+mYzhavivwkSXue6bfM2a0=", - "dev": true, - "optional": true + "dev": true }, "fs-mkdirp-stream": { "version": "1.0.0", @@ -5453,8 +5417,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -5475,14 +5438,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5497,20 +5458,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -5627,8 +5585,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -5640,7 +5597,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5655,7 +5611,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5663,14 +5618,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5689,7 +5642,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5770,8 +5722,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -5783,7 +5734,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5869,8 +5819,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -5906,7 +5855,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5926,7 +5874,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5970,14 +5917,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -6004,7 +5949,6 @@ "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", "integrity": "sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==", "dev": true, - "optional": true, "requires": { "npm-conf": "^1.1.0" } @@ -6013,15 +5957,13 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true, - "optional": true + "dev": true }, "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true, - "optional": true + "dev": true }, "get-value": { "version": "2.0.6", @@ -6336,8 +6278,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", - "dev": true, - "optional": true + "dev": true }, "growly": { "version": "1.3.0", @@ -7108,8 +7049,7 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", - "dev": true, - "optional": true + "dev": true }, "has-symbols": { "version": "1.0.0", @@ -7122,7 +7062,6 @@ "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", "dev": true, - "optional": true, "requires": { "has-symbol-support-x": "^1.4.1" } @@ -7322,8 +7261,7 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true, - "optional": true + "dev": true }, "ignore": { "version": "4.0.6", @@ -7453,7 +7391,6 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", "dev": true, - "optional": true, "requires": { "repeating": "^2.0.0" } @@ -7781,7 +7718,6 @@ "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7834,8 +7770,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", - "dev": true, - "optional": true + "dev": true }, "is-negated-glob": { "version": "1.0.0", @@ -7873,15 +7808,13 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", - "dev": true, - "optional": true + "dev": true }, "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true, - "optional": true + "dev": true }, "is-plain-object": { "version": "2.0.4", @@ -7951,15 +7884,13 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", - "dev": true, - "optional": true + "dev": true }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true, - "optional": true + "dev": true }, "is-svg": { "version": "3.0.0", @@ -8056,7 +7987,6 @@ "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", "dev": true, - "optional": true, "requires": { "has-to-string-tag-x": "^1.2.0", "is-object": "^1.0.1" @@ -8969,8 +8899,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", "integrity": "sha1-b54wtHCE2XGnyCD/FabFFnt0wm8=", - "dev": true, - "optional": true + "dev": true }, "lpad-align": { "version": "1.1.2", @@ -9040,8 +8969,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true, - "optional": true + "dev": true }, "map-visit": { "version": "1.0.0", @@ -9211,8 +9139,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "optional": true + "dev": true }, "minimatch": { "version": "3.0.4", @@ -9435,9 +9362,9 @@ "dev": true }, "nouislider": { - "version": "14.6.3", - "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-14.6.3.tgz", - "integrity": "sha512-/3tAqsWY2JYW9vd7bC14bFRA1P9A+pRHOtKmoMsyfnB0fQcd1UFx2pdY1Ey5wAUzTnXTesmYaEo/ecLVETijIQ==" + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-14.6.4.tgz", + "integrity": "sha512-PVCGYl+aC7/nVEbW61ypJWfuW3UCpvctz/luxpt4byxxli1FFyjBX9NIiy4Yak9AaO6a5BkPGfFYMCW4eg3eeQ==" }, "now-and-later": { "version": "2.0.1", @@ -12547,7 +12474,6 @@ "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", "dev": true, - "optional": true, "requires": { "config-chain": "^1.1.11", "pify": "^3.0.0" @@ -12557,8 +12483,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -12567,7 +12492,6 @@ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "dev": true, - "optional": true, "requires": { "path-key": "^2.0.0" } @@ -12929,8 +12853,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true, - "optional": true + "dev": true }, "p-is-promise": { "version": "1.1.0", @@ -12967,7 +12890,6 @@ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", "integrity": "sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=", "dev": true, - "optional": true, "requires": { "p-finally": "^1.0.0" } @@ -13158,8 +13080,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", - "dev": true, - "optional": true + "dev": true }, "performance-now": { "version": "2.1.0", @@ -13667,8 +13588,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", - "dev": true, - "optional": true + "dev": true }, "prr": { "version": "1.0.1", @@ -14024,7 +13944,6 @@ "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "dev": true, - "optional": true, "requires": { "is-finite": "^1.0.0" } @@ -14386,7 +14305,6 @@ "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", "dev": true, - "optional": true, "requires": { "commander": "~2.8.1" } @@ -14789,7 +14707,6 @@ "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", "dev": true, - "optional": true, "requires": { "is-plain-obj": "^1.0.0" } @@ -14799,7 +14716,6 @@ "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", "dev": true, - "optional": true, "requires": { "sort-keys": "^1.0.0" } @@ -15130,7 +15046,6 @@ "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", "dev": true, - "optional": true, "requires": { "is-natural-number": "^4.0.1" } @@ -15139,8 +15054,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true, - "optional": true + "dev": true }, "strip-indent": { "version": "1.0.1", @@ -15163,7 +15077,6 @@ "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", "integrity": "sha1-sv0qv2YEudHmATBXGV34Nrip1jE=", "dev": true, - "optional": true, "requires": { "escape-string-regexp": "^1.0.2" } @@ -15289,7 +15202,6 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", "integrity": "sha1-jqVdqzeXIlPZqa+Q/c1VmuQ1xVU=", "dev": true, - "optional": true, "requires": { "bl": "^1.0.0", "buffer-alloc": "^1.2.0", @@ -15304,15 +15216,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, - "optional": true + "dev": true }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", "dev": true, - "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -15328,7 +15238,6 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", "dev": true, - "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -15339,15 +15248,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", - "dev": true, - "optional": true + "dev": true }, "tempfile": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-2.0.0.tgz", "integrity": "sha1-awRGhWqbERTRhW/8vlCczLCXcmU=", "dev": true, - "optional": true, "requires": { "temp-dir": "^1.0.0", "uuid": "^3.0.1" @@ -15442,8 +15349,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", - "dev": true, - "optional": true + "dev": true }, "timers-ext": { "version": "0.1.7", @@ -15500,8 +15406,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", "integrity": "sha1-STvUj2LXxD/N7TE6A9ytsuEhOoA=", - "dev": true, - "optional": true + "dev": true }, "to-fast-properties": { "version": "2.0.0", @@ -15605,7 +15510,6 @@ "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", "dev": true, - "optional": true, "requires": { "escape-string-regexp": "^1.0.2" } @@ -15742,7 +15646,6 @@ "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz", "integrity": "sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg==", "dev": true, - "optional": true, "requires": { "buffer": "^5.2.1", "through": "^2.3.8" @@ -15943,8 +15846,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", - "dev": true, - "optional": true + "dev": true }, "use": { "version": "3.1.1", @@ -16440,7 +16342,6 @@ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", "dev": true, - "optional": true, "requires": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 045f788929..45411fad26 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -55,7 +55,7 @@ "@babel/preset-env": "7.6.3", "@babel/plugin-proposal-object-rest-spread": "7.13.8", "autoprefixer": "9.6.5", - "caniuse-lite": "^1.0.30001037", + "caniuse-lite": "^1.0.30001237", "cssnano": "4.1.10", "fs": "0.0.2", "gulp": "4.0.2", @@ -78,8 +78,8 @@ "jasmine-core": "3.5.0", "jsdom": "16.4.0", "karma": "4.4.1", - "karma-jsdom-launcher": "^8.0.2", "karma-jasmine": "2.0.1", + "karma-jsdom-launcher": "^8.0.2", "karma-junit-reporter": "2.0.1", "karma-spec-reporter": "0.0.32", "less": "3.10.3", From 0bc2ff17d77c76a985788979c3017d5d57369812 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Fri, 18 Jun 2021 16:14:01 +0200 Subject: [PATCH 230/289] =?UTF-8?q?Yeah=20okay,=20#h5is=20I=20broke=20the?= =?UTF-8?q?=20build=20=F0=9F=A4=A6=E2=80=8D=E2=99=82=EF=B8=8F=20-=20fixed?= =?UTF-8?q?=20here=20for=20this=20PR,=20will=20also=20fix=20in=20the=20con?= =?UTF-8?q?trib=20branch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index d3e8a355c9..180837bc80 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -319,6 +319,7 @@ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v12.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v14.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v16.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v17.0 From 3265fbb4734bd65059aa7f49cfa64b29db04513c Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Fri, 18 Jun 2021 16:15:32 +0200 Subject: [PATCH 231/289] Fix the build --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index d3e8a355c9..180837bc80 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -319,6 +319,7 @@ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v12.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v14.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v16.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v17.0 From 32a1870d80fceff655feae9868107ddde91e407b Mon Sep 17 00:00:00 2001 From: Christian Tricarico Date: Sun, 20 Jun 2021 02:32:10 +0200 Subject: [PATCH 232/289] Backoffice Italian localization improvements (#10371) * Italian translations in localization xml. * Italian translations in localization xml. * restore previous package-lock version --- src/Umbraco.Web.UI/Umbraco/config/lang/it.xml | 1674 +++++++++-------- 1 file changed, 886 insertions(+), 788 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml index 2f89060ac6..63b7a2fff8 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml @@ -1,401 +1,437 @@ - - The Umbraco community - https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - - - Gestisci hostnames - Audit Trail - Sfoglia - Copia - Crea - Crea pacchetto - Cancella - Disabilita - Svuota il cestino - Esporta il tipo di documento - Importa il tipo di documento - Importa il pacchetto - Modifica in Area di Lavoro - Uscita - Sposta - Notifiche - Accesso pubblico - Pubblica - Aggiorna nodi - Ripubblica intero sito - Permessi - Annulla ultima modifica - Invia per la pubblicazione - Invia per la traduzione - Ordina - Invia la pubblicazione - Traduci - Annulla pubblicazione - Aggiorna - - - Aggiungi nuovo dominio - Dominio - - - - - Hostname non valido - Modifica il dominio corrente - - - Visualizzazione per - - - Grassetto - Cancella rientro paragrafo - Inserisci dal file - Inserisci intestazione grafica - Modifica Html - Inserisci rientro paragrafo - Corsivo - Centra - Allinea testo a sinistra - Allinea testo a destra - Inserisci Link - Inserisci local link (ancora) - Elenco puntato - Elenco numerato - Inserisci macro - Inserisci immagine - Modifica relazioni - Salva - Salva e pubblica - Salva e invia per approvazione - Anteprima - - Scegli lo stile - Mostra gli stili - Inserisci tabella - - - Informazioni su questa pagina - Link alternativo - - Links alternativi - Clicca per modificare questo elemento - Creato da - Creato il - Tipo di documento - Modifica - Attivo fino al - - - Ultima pubblicazione - Link ai media - Tipo di media - Gruppo di membri - Ruolo - Tipologia Membro - - Titolo della Pagina - - - Pubblicato - Stato della pubblicazione - Pubblicato il - Rimuovi data - Ordinamento dei nodi aggiornato - - Statistiche - Titolo (opzionale) - Tipo - Non pubblicare - Ultima modifica - Rimuovi il file - Link al documento - - - - Crea al - Scegli il tipo ed il titolo - - - - - - hai aperto una nuova finestra - Riavvia - Visita - Benvenuto - - - Rimani - Scarta le modifiche - Hai delle modifiche non salvate - Sei sicuro di voler lasciare questa pagina? - hai delle modifiche non salvate - - - Fatto - Elimianto %0% elemento - Elimianto %0% elementi - Eliminato %0% su %1% elemento - Eliminato %0% su %1% elementi - Pubblicato %0% elemento - Pubblicato %0% elementi - Pubblicato %0% su %1% elemento - Pubblicato %0% su %1% elementi - %0% elemento non pubblicato - %0% elementi non pubblicati - Elementi non pubblicati - %0% su %1% - Elementi non pubblicati - %0% su %1% - Spostato %0% elemento - Spsotato %0% elementi - Spostato %0% su %1% elemento - Spostato %0% su %1% elementi - Copiato %0% elemento - Copiato %0% elementi - Copiato %0% su %1% elemento - Copiato %0% su %1% elementi - - - Titolo del Link - Link - Nome - Gestione alias Hostnames - Chiudi questa finestra - - - - - Taglia - Modifica elemento Dictionary - Modifica il linguaggio - Inserisci il link locale - Inserisci carattere - - - Inserisci link - Inserisci macro - Inserisci tabella - Ultima modifica - Link - - - - - Incolla - Modifica il Permesso per - - - - regexlib.com ha attualmente qualche problema, di cui non abbiamo il controllo. Siamo spiacevoli dell'inconveniente.]]> - - Elimina Macro - Campo obbligatorio - - - - Numero di colonne - Numero di righe - - Seleziona elemento - Visualizza gli elementi in cache - - - - - - - - - - - - Rendering controllo - Bottoni - Abilita impostazioni avanzate per - Abilita menu contestuale - Dimensione massima delle immagini inserite - Fogli di stile collegati - Visualizza etichetta - Larghezza e altezza - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Info - Azione - Aggiungi - Alias - - Bordo - o - Annulla - - Scegli - Chiudi - Chiudi la finestra - Commento - Conferma - Blocca le proporzioni - Continua - Copia - Crea - Base di dati - Data - Default - Elimina - Eliminato - Elimina... - Design - Dimensioni - - Scarica - Modifica - Modificato - Elementi - Email - Errore - Trova - Cartella - Altezza - Guida - Icona - Importa - - Inserisci - Installa - Giustificato - Lingua - Layout - Caricamento - Bloccato - Login - Log off - Logout - Macro - Sposta - Nome - Nuovo - Successivo - No - di - Ok - Apri - o - Password - Percorso - - Precedente - - - Cestino - Rimangono - Rinomina - Rinnova - Riprova - Permessi - Cerca - Server - Mostra - Mostra la pagina inviata - Dimensione - Ordina - Invia - Tipo - - Su - Aggiorna - Aggiornamento - Carica - URL - Utente - - Valore - Vedi - Benvenuto... - Larghezza - Si - Riordina - Ho finito di ordinare - - - Colore di sfondo - Grassetto - Colore del testo - Carattere - Testo - - - Pagina - - - - - - - installa per installare il database Umbraco %0% ]]> - Avanti per proseguire.]]> - Database non trovato! Perfavore, controlla che le informazioni della stringa di connessione nel file "web.config" siano corrette.

    -

    Per procedere, edita il file "web.config" (utilizzando Visual Studio o l'editor di testo che preferisci), scorri in basso, aggiungi la stringa di connessione per il database chiamato "umbracoDbDSN" e salva il file.

    Clicca il tasto riprova quando hai finito.
    Maggiori dettagli per la modifica del file web.config qui.

    ]]>
    - - Premi il tasto aggiorna per aggiornare il database ad Umbraco %0%

    Non preoccuparti, il contenuto non verrà perso e tutto continuerà a funzionare dopo l'aggiornamento!

    ]]>
    - Premi il tasto Avanti per continuare.]]> - Avanti per continuare la configurazione.]]> - La password predefinita per l'utente di default deve essere cambiata!]]> - L'utente di default è stato disabilitato o non ha accesso ad Umbraco!

    Non è necessario eseguire altre operazioni. Clicca il tasto Avanti per continuare.]]> - La password è stata modificata con successo

    Non è necessario eseguire altre operazioni. Clicca il tasto Avanti per continuare.]]> - - - - - - - - Le impostazioni dei permessi sono perfette!

    Puoi eseguire Umbraco senza problemi, ma potresti non poter installare i pacchetti che sono consigliati per sfruttare tutti i vantaggi offerti da Umbraco.]]>
    - - - video tutorial su come impostare i permessi delle cartelle per Umbraco o leggi la versione testuale.]]> - Le impostazioni dei permessi potrebbero avere dei problemi!

    Puoi eseguire Umbraco, ma potresti non essere in grado di creare cartelle o installare pacchetti che sono raccomandati per sfruttare tutti i vantaggi di Umbraco.]]>
    - Le impostazioni dei permessi non sono corrette per Umbraco!

    Per eseguire Umbraco, devi aggiornare le impostazioni dei permessi.]]>
    - La configurazione dei permessi è perfetta!

    Sei pronto per avviare Umbraco e installare i pacchetti!]]>
    - - - - - - Guarda come) Puoi anche installare eventuali Runway in un secondo momento. Vai nella sezione Developer e scegli Pacchetti.]]> - - Runway è installato - + The Umbraco community + https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files + + + Gestisci hostnames + Audit Trail + Sfoglia + Copia + Crea + Crea pacchetto + Cancella + Disabilita + Svuota il cestino + Esporta il tipo di documento + Importa il tipo di documento + Importa il pacchetto + Modifica in Area di Lavoro + Uscita + Sposta + Notifiche + Accesso pubblico + Pubblica + Aggiorna nodi + Ripubblica intero sito + Permessi + Annulla ultima modifica + Invia per la pubblicazione + Invia per la traduzione + Ordina + Invia la pubblicazione + Traduci + Annulla pubblicazione + Aggiorna + Rimuovi + Ripristina + Crea Content Template + Crea gruppo + + + Aggiungi nuovo dominio + Dominio + + + + + Hostname non valido + Modifica il dominio corrente + + + Visualizzazione per + Contenuto pubblicato + Contenuto salvato + + + Grassetto + Cancella rientro paragrafo + Inserisci dal file + Inserisci intestazione grafica + Modifica Html + Inserisci rientro paragrafo + Corsivo + Centra + Allinea testo a sinistra + Allinea testo a destra + Inserisci Link + Inserisci local link (ancora) + Elenco puntato + Elenco numerato + Inserisci macro + Inserisci immagine + Modifica relazioni + Salva + Salva e pubblica + Salva e invia per approvazione + Anteprima + + Scegli lo stile + Mostra gli stili + Inserisci tabella + Altre azioni + Pubblica con i discendenti + Pianifica + Seleziona + Annulla selezione + + + Informazioni su questa pagina + Link alternativo + + Links alternativi + Clicca per modificare questo elemento + Creato da + Creato il + Tipo di documento + Modifica + Attivo fino al + + + Ultima pubblicazione + Link ai media + Tipo di media + Gruppo di membri + Ruolo + Tipologia Membro + + Titolo della Pagina + + + Pubblicato + Stato della pubblicazione + Pubblicato il + Rimuovi data + Ordinamento dei nodi aggiornato + + Statistiche + Titolo (opzionale) + Tipo + Non pubblicare + Ultima modifica + Rimuovi il file + Link al documento + Elementi + Pubblicato + Seleziona da data e l'ora in cui pubblicare/depubblicare il contenuto. + Imposta data + Depubblicato il + + + + Crea un elemento sotto + Scegli il tipo ed il titolo + Cartella + + + + + + hai aperto una nuova finestra + Riavvia + Visita + Benvenuto + + + Rimani + Scarta le modifiche + Hai delle modifiche non salvate + Sei sicuro di voler lasciare questa pagina? - hai delle modifiche non salvate + + + Fatto + Elimianto %0% elemento + Elimianto %0% elementi + Eliminato %0% su %1% elemento + Eliminato %0% su %1% elementi + Pubblicato %0% elemento + Pubblicato %0% elementi + Pubblicato %0% su %1% elemento + Pubblicato %0% su %1% elementi + %0% elemento non pubblicato + %0% elementi non pubblicati + Elementi non pubblicati - %0% su %1% + Elementi non pubblicati - %0% su %1% + Spostato %0% elemento + Spsotato %0% elementi + Spostato %0% su %1% elemento + Spostato %0% su %1% elementi + Copiato %0% elemento + Copiato %0% elementi + Copiato %0% su %1% elemento + Copiato %0% su %1% elementi + + + Titolo del Link + Link + Nome + Gestione alias Hostnames + Chiudi questa finestra + + + + + Taglia + Modifica elemento Dictionary + Modifica il linguaggio + Inserisci il link locale + Inserisci carattere + + + Inserisci link + Inserisci macro + Inserisci tabella + Ultima modifica + Link + + + + + Incolla + Modifica il Permesso per + + + + regexlib.com ha attualmente qualche problema, di cui non abbiamo il controllo. Siamo spiacevoli dell'inconveniente.]]> + + Elimina Macro + Campo obbligatorio + + + + Numero di colonne + Numero di righe + + Seleziona elemento + Visualizza gli elementi in cache + Seleziona contenuto + + + + + + + + + + + + Rendering controllo + Bottoni + Abilita impostazioni avanzate per + Abilita menu contestuale + Dimensione massima delle immagini inserite + Fogli di stile collegati + Visualizza etichetta + Larghezza e altezza + + + + + + + + + + + + + + + + + + + + + + + + + + + Questa proprietà non è valida + + + Info + Azione + Aggiungi + Alias + + Bordo + o + Annulla + + Scegli + Chiudi + Chiudi la finestra + Commento + Conferma + Blocca le proporzioni + Continua + Copia + Crea + Base di dati + Data + Default + Elimina + Eliminato + Elimina... + Design + Dimensioni + + Scarica + Modifica + Modificato + Elementi + Email + Errore + Trova + Cartella + Altezza + Guida + Icona + Importa + + Inserisci + Installa + Giustificato + Lingua + Layout + Caricamento + Bloccato + Login + Log off + Logout + Macro + Sposta + Nome + Nuovo + Successivo + No + di + Ok + Apri + o + Password + Percorso + + Precedente + + + Cestino + Rimangono + Rinomina + Rinnova + Riprova + Permessi + Cerca + Server + Mostra + Mostra la pagina inviata + Dimensione + Ordina + Conferma + Tipo + Digita per cercare... + Su + Aggiorna + Aggiornamento + Carica + URL + Utente + + Valore + Vedi + Benvenuto... + Larghezza + Si + Riordina + Ho finito di ordinare + Richiesto + Contenuti + Azioni + Cerca solo in questa cartella + Pianifica pubblicazione + selezionato + Annulla + Cambia password + Cronologia + Generale + Rimuovi + Gruppi + + + Colore di sfondo + Grassetto + Colore del testo + Carattere + Testo + + + Pagina + + + + + + + installa per installare il database Umbraco %0% ]]> + Avanti per proseguire.]]> + + Database non trovato! Perfavore, controlla che le informazioni della stringa di connessione nel file "web.config" siano corrette.

    +

    Per procedere, edita il file "web.config" (utilizzando Visual Studio o l'editor di testo che preferisci), scorri in basso, aggiungi la stringa di connessione per il database chiamato "umbracoDbDSN" e salva il file.

    Clicca il tasto riprova quando hai finito.
    Maggiori dettagli per la modifica del file web.config qui.

    ]]> +
    + + Premi il tasto aggiorna per aggiornare il database ad Umbraco %0%

    Non preoccuparti, il contenuto non verrà perso e tutto continuerà a funzionare dopo l'aggiornamento!

    ]]>
    + Premi il tasto Avanti per continuare.]]> + Avanti per continuare la configurazione.]]> + La password predefinita per l'utente di default deve essere cambiata!]]> + L'utente di default è stato disabilitato o non ha accesso ad Umbraco!

    Non è necessario eseguire altre operazioni. Clicca il tasto Avanti per continuare.]]> + La password è stata modificata con successo

    Non è necessario eseguire altre operazioni. Clicca il tasto Avanti per continuare.]]> + + + + + + + + Le impostazioni dei permessi sono perfette!

    Puoi eseguire Umbraco senza problemi, ma potresti non poter installare i pacchetti che sono consigliati per sfruttare tutti i vantaggi offerti da Umbraco.]]>
    + + + video tutorial su come impostare i permessi delle cartelle per Umbraco o leggi la versione testuale.]]> + Le impostazioni dei permessi potrebbero avere dei problemi!

    Puoi eseguire Umbraco, ma potresti non essere in grado di creare cartelle o installare pacchetti che sono raccomandati per sfruttare tutti i vantaggi di Umbraco.]]>
    + Le impostazioni dei permessi non sono corrette per Umbraco!

    Per eseguire Umbraco, devi aggiornare le impostazioni dei permessi.]]>
    + La configurazione dei permessi è perfetta!

    Sei pronto per avviare Umbraco e installare i pacchetti!]]>
    + + + + + + Guarda come) Puoi anche installare eventuali Runway in un secondo momento. Vai nella sezione Developer e scegli Pacchetti.]]> + + Runway è installato + + Questa è la lista dei nostri moduli raccomandati, seleziona quali vorresti installare, o vedi l'intera lista di moduli - ]]> - Raccommandato solo per utenti esperti - Vorrei iniziare da un sito semplice - + + Raccommandato solo per utenti esperti + Vorrei iniziare da un sito semplice + + "Runway" è un semplice sito web contenente alcuni tipi di documento e alcuni templates di base. L'installer configurerà Runway per te automaticamente, ma tu potrai facilmente modificarlo, estenderlo o eliminarlo. Non è necessario installarlo e potrai usare Umbraco anche senza di esso, ma @@ -406,63 +442,85 @@ Inclusi in Runway: Home page, pagina Guida introduttiva, pagina Installazione moduli
    Moduli opzionali: Top Navigation, Sitemap, Contatti, Gallery. - ]]>
    - Cosa è Runway - Passo 1/5 Accettazione licenza - Passo 2/5: Configurazione database - Passo 3/5: Controllo permessi dei file - Passo 4/5: Controllo impostazioni sicurezza - Passo 5/5: Umbraco è pronto per iniziare - Grazie per aver scelto Umbraco - Naviga per il tuo nuovo sito -Hai installato Runway, quindi perché non dare uno sguardo al vostro nuovo sito web.]]> - Ulteriori informazioni e assistenza -Fatti aiutare dalla nostra community, consulta la documentazione o guarda alcuni video gratuiti su come costruire un semplice sito web, come usare i pacchetti e una guida rapida alla terminologia Umbraco]]> - - /web.config e aggiornare la chiave AppSetting UmbracoConfigurationStatus impostando il valore '%0%'.]]> - iniziare immediatamente cliccando sul bottone "Avvia Umbraco".
    Se sei nuovo a Umbraco, si possono trovare un sacco di risorse sulle nostre pagine Getting Started.]]>
    - Avvia Umbraco -Per gestire il tuo sito web, è sufficiente aprire il backoffice di Umbraco e iniziare ad aggiungere i contenuti, aggiornando i modelli e i fogli di stile o aggiungere nuove funzionalità]]> - Connessione al database non riuscita. - Umbraco Versione 3 - Umbraco Versione 4 - Guarda - Umbraco %0% per una nuova installazione o per l'aggiornamento dalla versione 3.0. + ]]> + + Cosa è Runway + Passo 1/5 Accettazione licenza + Passo 2/5: Configurazione database + Passo 3/5: Controllo permessi dei file + Passo 4/5: Controllo impostazioni sicurezza + Passo 5/5: Umbraco è pronto per iniziare + Grazie per aver scelto Umbraco + + Naviga per il tuo nuovo sito +Hai installato Runway, quindi perché non dare uno sguardo al vostro nuovo sito web.]]> + + + Ulteriori informazioni e assistenza +Fatti aiutare dalla nostra community, consulta la documentazione o guarda alcuni video gratuiti su come costruire un semplice sito web, come usare i pacchetti e una guida rapida alla terminologia Umbraco]]> + + + /web.config e aggiornare la chiave AppSetting UmbracoConfigurationStatus impostando il valore '%0%'.]]> + iniziare immediatamente cliccando sul bottone "Avvia Umbraco".
    Se sei nuovo a Umbraco, si possono trovare un sacco di risorse sulle nostre pagine Getting Started.]]>
    + + Avvia Umbraco +Per gestire il tuo sito web, è sufficiente aprire il backoffice di Umbraco e iniziare ad aggiungere i contenuti, aggiornando i modelli e i fogli di stile o aggiungere nuove funzionalità]]> + + Connessione al database non riuscita. + Umbraco Versione 3 + Umbraco Versione 4 + Guarda + + Umbraco %0% per una nuova installazione o per l'aggiornamento dalla versione 3.0.

    - Clicca "avanti" per avviare la procedura.]]>
    - - - Codice cultura - Nome cultura - - - - Riconnetti adesso per salvare il tuo lavoro - - - © 2001 - %0%
    umbraco.com

    ]]>
    - - - Dashboard - Sezioni - Contenuto - - - Scegli la pagina sopra... - - Seleziona dove il documento %0% deve essere copiato - - Seleziona dove il documento %0% deve essere spostato - - - - - - - - - - "avanti" per avviare la procedura.]]> + + + + Codice cultura + Nome cultura + + + + Riconnetti adesso per salvare il tuo lavoro + + + © 2001 - %0%
    umbraco.com

    ]]>
    + Buona domenica + Buon lunedì + Buon martedì + Buon mercoledì + Buon giovedì + Buon venerdì + Buon sabato + Mostra password + Nascondi password + Password dimenticata? + Una email verrà inviata all'indirizzo specificato con un link per il reset della password + Ritorna alla finestra di login + + + Dashboard + Sezioni + Contenuto + + + Scegli la pagina sopra... + + Seleziona dove il documento %0% deve essere copiato + + Seleziona dove il documento %0% deve essere spostato + + + + + + + + + + + - Salve %0%

    + ]]> +
    + + Salve %0%

    Questa è un'email automatica per informare che l'azione '%1%' è stata eseguita sulla pagina '%2%' @@ -501,257 +561,266 @@ Per gestire il tuo sito web, è sufficiente aprire il backoffice di Umbraco e in

    Buona giornata!

    Grazie da Umbraco -

    ]]>
    - [%0%] Notifica per %1% eseguita su %2% - Notifiche - - - ]]> + + [%0%] Notifica per %1% eseguita su %2% + Notifiche + + + + e selezionando il pacchetto. I pacchetti Umbraco generalmente hanno l'estensione ".umb" o ".zip". - ]]> - Autore - Documentazione - Meta dati pacchetto - Nome del pacchetto - Il pacchetto non contiene tutti gli elementi -
    - E' possibile rimuovere questo pacchetto dal sistema cliccando "rimuovi pacchetto" in basso.]]>
    - Opzioni pacchetto - Pacchetto leggimi - Pacchetto repository - Conferma eliminazione - - - Disinstalla pacchetto - + ]]> + + Autore + Documentazione + Meta dati pacchetto + Nome del pacchetto + Il pacchetto non contiene tutti gli elementi + +
    + E' possibile rimuovere questo pacchetto dal sistema cliccando "rimuovi pacchetto" in basso.]]> +
    + Opzioni pacchetto + Pacchetto leggimi + Pacchetto repository + Conferma eliminazione + + + Disinstalla pacchetto + + Avviso: tutti i documenti, i media, etc a seconda degli elementi che rimuoverai, smetteranno di funzionare, e potrebbero portare a un'instabilità del sistema, - perciò disinstalla con cautela. In caso di dubbio contattare l'autore del pacchetto.]]> - Versione del pacchetto - - - - - - - - - - usando i gruppi di membri di Umbraco.]]> - Devi creare un gruppo di membri prima di utilizzare l'autenticazione basata sui ruoli - - - - - - - - - - - - - - - - - - - - - - - - ok per pubblicare %0% e rendere questo contenuto accessibile al pubblico.

    Puoi pubblicare questa pagina e tutte le sue sottopagine selezionando pubblica tutti i figli qui sotto.]]>
    - - - - - - - - - - - - - - - - Il testo in rosso non verrà mostrato nella versione selezionata, quello in verde verrà aggiunto]]> - - - - - - - - - - - Concierge - Contenuto - Courier - Sviluppo - Configurazione guidata Umbraco - Media - Membri - Newsletters - Impostazioni - Statistiche - Traduzione - Utenti - - - Tipo di contenuto master abilitato - Questo tipo di contenuto usa - - - - - Tipo - Foglio di stile - Tab - Titolo tab - Tabs - - - Ordinamento - Data creazione - - - - - - - - - Tipo di dati: %1%]]> - - Tipo di documento salvato - Tab creata - Tab eliminata - Tab con id: %0% eliminata - Contenuto non pubblicare - - - - Tipo di dato salvato - - - - - - - - - - - - - - - Tipo utente salvato - - - - - - Partial view salvata - Partial view salvata senza errori! - Partial view non salvata - Errore durante il salvataggio del file. - - - - - - - - - - - Anteprima - Stili - - - - - - - - - Master template - - Template - - - Image - Macro - Seleziona il tipo di contenuto - Seleziona un layout - Aggiungi una riga - Aggiungi contenuto - Elimina contenuto - Impostazioni applicati - Questo contenuto non è consentito qui - Questo contenuto è consentito qui - Clicca per incorporare - Clicca per inserire l'immagine - Didascalia dell'immagine... - Scrivi qui... - I Grid Layout - I layout sono l'area globale di lavoro per il grid editor, di solito ti serve solo uno o due layout differenti - Aggiungi un Grid Layout - Sistema il layout impostando la larghezza della colonna ed aggiungendo ulteriori sezioni - Configurazioni della riga - Le righe sono le colonne predefinite disposte orizzontalmente - Aggiungi configurazione della riga - Sistema la riga impostando la larghezza della colonna ed aggiungendo ulteriori colonne - Colonne - Totale combinazioni delle colonne nel grid layout - Impostazioni - Configura le impostazioni che possono essere cambiate dai editori - Stili - Configura i stili che possono essere cambiati dai editori - Permetti tutti i editor - Permetti tutte le configurazioni della riga - - - - - - Scegli il campo - Converte le interruzioni di linea - - Campi Personalizzati - - - - - - - Minuscolo - Nessuno - - - Ricorsivo - - - Campi Standard - Maiuscolo - - - - - - - - Dettagli - Scarica xml DTD - Campi - Includi le sottopagine - + + Versione del pacchetto + + + + + + + + + + usando i gruppi di membri di Umbraco.]]> + Devi creare un gruppo di membri prima di utilizzare l'autenticazione basata sui ruoli + + + + + + + + + + + + + + + + + + + + + + + + ok per pubblicare %0% e rendere questo contenuto accessibile al pubblico.

    Puoi pubblicare questa pagina e tutte le sue sottopagine selezionando pubblica tutti i figli qui sotto.]]>
    + + + + + + + + + + + + + + + + Il testo in rosso non verrà mostrato nella versione selezionata, quello in verde verrà aggiunto]]> + + + + + + + + + + + Concierge + Contenuto + Courier + Sviluppo + Configurazione guidata Umbraco + Media + Membri + Newsletters + Impostazioni + Statistiche + Traduzione + Utenti + + + Tipo di contenuto master abilitato + Questo tipo di contenuto usa + + + + + Tipo + Foglio di stile + Tab + Titolo tab + Tabs + + + Ordinamento + Data creazione + + + + + + + + + Tipo di dati: %1%]]> + + Tipo di documento salvato + Tab creata + Tab eliminata + Tab con id: %0% eliminata + Contenuto non pubblicare + + + + Tipo di dato salvato + + + + + + + + + + + + + + + Tipo utente salvato + + + + + + Partial view salvata + Partial view salvata senza errori! + Partial view non salvata + Errore durante il salvataggio del file. + + + + + + + + + + + Anteprima + Stili + + + + + + + + + Master template + + Template + Data creazione + + + Immagine + Macro + Seleziona il tipo di contenuto + Seleziona un layout + Aggiungi una riga + Aggiungi contenuto + Elimina contenuto + Impostazioni applicati + Questo contenuto non è consentito qui + Questo contenuto è consentito qui + Clicca per incorporare + Clicca per inserire l'immagine + Didascalia dell'immagine... + Scrivi qui... + I Grid Layout + I layout sono l'area globale di lavoro per il grid editor, di solito ti serve solo uno o due layout differenti + Aggiungi un Grid Layout + Sistema il layout impostando la larghezza della colonna ed aggiungendo ulteriori sezioni + Configurazioni della riga + Le righe sono le colonne predefinite disposte orizzontalmente + Aggiungi configurazione della riga + Sistema la riga impostando la larghezza della colonna ed aggiungendo ulteriori colonne + Colonne + Totale combinazioni delle colonne nel grid layout + Impostazioni + Configura le impostazioni che possono essere cambiate dai editori + Stili + Configura i stili che possono essere cambiati dai editori + Permetti tutti i editor + Permetti tutte le configurazioni della riga + + + + + + Scegli il campo + Converte le interruzioni di linea + + Campi Personalizzati + + + + + + + Minuscolo + Nessuno + + + Ricorsivo + + + Campi Standard + Maiuscolo + + + + + + + + Dettagli + Scarica xml DTD + Campi + Includi le sottopagine + + - - - - - - - - - - Traduttore - - - - Cache Browser - Cestino - Pacchetti creati - Tipi di dato - Dizionario - Pacchetti installati - Installare skin - Installare starter kit - Lingue - Installa un pacchetto locale - Macros - Tipi di media - Membri - Gruppi di Membri - Ruoli - Tipologia Membri - Tipi di documento - Pacchetti - Pacchetti - Installa dal repository - Installa Runway - Moduli Runway - Files di scripting - Scripts - Fogli di stile - Templates - Permessi Utente - Tipi di Utente - Utenti - - - - - - - - - Amministratore - Campo Categoria - Cambia la tua password - - Conferma la nuova password - Contenuto del canale - Campo Descrizione - Disabilita l'utente - Tipo di Documento - Editor - Campo Eccezione - Lingua - Login - - Sezioni - Modifica la tua password - - Password - - - Password attuale - - - - - - - - - - - Username - - - - Autore - + ]]> + + + + + + + + + + + Traduttore + + + + Cache Browser + Cestino + Pacchetti creati + Tipi di dato + Dizionario + Pacchetti installati + Installare skin + Installare starter kit + Lingue + Installa un pacchetto locale + Macros + Tipi di media + Membri + Gruppi di Membri + Ruoli + Tipologia Membri + Tipi di documento + Pacchetti + Pacchetti + Installa dal repository + Installa Runway + Moduli Runway + Files di scripting + Scripts + Fogli di stile + Templates + Permessi Utente + Tipi di Utente + Utenti + Contenuti + + + + + + + + + Amministratore + Campo Categoria + Cambia la tua password + + Conferma la nuova password + Contenuto del canale + Campo Descrizione + Disabilita l'utente + Tipo di Documento + Editor + Campo Eccezione + Lingua + Login + + Sezioni + Modifica la tua password + + Password + + + Password attuale + + + + + + + + + + + Username + + + + Autore + Il tuo profilo + La tua storia recente + Crea utente + Crea nuovi utenti e dai loro accesso ad Umbraco. Quando un nuovo utente viene creato viene generata una password che potrai condividere con l'utente. + Aggiungi gruppi per assegnare accessi e permessi + Torna agli utenti + Gestione utenti + + + Devi aggiungere almeno + elementi + + + Digita per cercare... + Inserisci la tua email + Inserisci la tua password + Inserisci un nome... + Inserisci una email... + + + o clicca qui per scegliere i file + Trascina i tuoi file all'interno di quest'area + + + Contenuti + Info + Elementi +
    From ed79bd65f2d9f4217df4dee35279d7d675e01b6a Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 21 Jun 2021 13:38:49 +0200 Subject: [PATCH 233/289] Fix certified to verified refactoring --- .../src/views/packages/views/repo.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html index 18fd7c55f5..d467c513ec 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html +++ b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html @@ -65,7 +65,7 @@
    - Certified to work on Umbraco Cloud + Verified to work on Umbraco Cloud
    @@ -116,7 +116,7 @@
    - + @@ -282,7 +282,7 @@
    -
    Verified to work on Umbraco CLoud
    +
    Verified to work on Umbraco CLoud
    From 809bb3b5d3462ebb43dd8c8929de497442b22b73 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 21 Jun 2021 14:01:50 +0200 Subject: [PATCH 234/289] Fixes #10423 Don't obsolete the v2 pickers, renamed them and they're able to be created again, without config changes --- src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs | 4 ++-- .../views/prevalueeditors/obsoletemediapickernotice.html | 2 +- .../PropertyEditors/MediaPickerPropertyEditor.cs | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 6a56141491..d78e0a90d7 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -141,8 +141,8 @@ namespace Umbraco.Core.Migrations.Install //New UDI pickers with newer Ids _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1046, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1046", SortOrder = 2, UniqueId = new Guid("FD1E0DA5-5606-4862-B679-5D0CF3A52A59"), Text = "Content Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1047, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1047", SortOrder = 2, UniqueId = new Guid("1EA2E01F-EBD8-4CE1-8D71-6B1149E63548"), Text = "Member Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "(Obsolete) Media Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = new Guid("9DBBCBBB-2327-434A-B355-AF1B84E5010A"), Text = "(Obsolete) Multiple Media Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "Media Picker (legacy)", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = new Guid("9DBBCBBB-2327-434A-B355-AF1B84E5010A"), Text = "Multiple Media Picker (legacy)", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1050, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1050", SortOrder = 2, UniqueId = new Guid("B4E3535A-1753-47E2-8568-602CF8CFEE6F"), Text = "Multi URL Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1051, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1051", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3Guid, Text = "Media Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/obsoletemediapickernotice.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/obsoletemediapickernotice.html index cc861dcb4b..617ffd80d6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/obsoletemediapickernotice.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/obsoletemediapickernotice.html @@ -1 +1 @@ -

    Important: switching from the (Obsolete) Media Picker to Media Picker will mean all data (references to previously selected media items) will be deleted and no longer available.

    \ No newline at end of file +

    Important: switching from "Media Picker (legacy)" to "Media Picker" is not supported and doing so will mean all data (references to previously selected media items) will no longer be available on existing content items.

    diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs index 499737f3b7..1784b5fd0d 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs @@ -8,17 +8,17 @@ namespace Umbraco.Web.PropertyEditors { /// /// Represents a media picker property editor. - /// Marked as Deprecated as best to use the NEW Media Picker aka MediaPicker3 + /// Nameed "(legacy)" as it's best to use the NEW Media Picker aka MediaPicker3 /// [DataEditor( Constants.PropertyEditors.Aliases.MediaPicker, EditorType.PropertyValue | EditorType.MacroParameter, - "(Obsolete) Media Picker", + "Media Picker (legacy)", "mediapicker", ValueType = ValueTypes.Text, Group = Constants.PropertyEditors.Groups.Media, Icon = Constants.Icons.MediaImage, - IsDeprecated = true)] + IsDeprecated = false)] public class MediaPickerPropertyEditor : DataEditor { From 7d53659fac2c9544e3dd7999a92b852fea840c04 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 21 Jun 2021 17:27:34 +0100 Subject: [PATCH 235/289] Update src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs --- src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs index 1784b5fd0d..a33bed89fa 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs @@ -8,7 +8,7 @@ namespace Umbraco.Web.PropertyEditors { /// /// Represents a media picker property editor. - /// Nameed "(legacy)" as it's best to use the NEW Media Picker aka MediaPicker3 + /// Named "(legacy)" as it's best to use the NEW Media Picker aka MediaPicker3 /// [DataEditor( Constants.PropertyEditors.Aliases.MediaPicker, From 79fc804a9b4344ccf92f9bb595d2e658dfc35379 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 21 Jun 2021 17:47:02 +0100 Subject: [PATCH 236/289] Merge pull request #10500 from umbraco/v8/fix/10423 Don't obsolete the v2 pickers, renamed them and they're able to be created again, without config changes (cherry picked from commit 69383d3dead2143848c711051990809d1a42a314) --- src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs | 4 ++-- .../views/prevalueeditors/obsoletemediapickernotice.html | 2 +- .../PropertyEditors/MediaPickerPropertyEditor.cs | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 6a56141491..d78e0a90d7 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -141,8 +141,8 @@ namespace Umbraco.Core.Migrations.Install //New UDI pickers with newer Ids _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1046, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1046", SortOrder = 2, UniqueId = new Guid("FD1E0DA5-5606-4862-B679-5D0CF3A52A59"), Text = "Content Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1047, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1047", SortOrder = 2, UniqueId = new Guid("1EA2E01F-EBD8-4CE1-8D71-6B1149E63548"), Text = "Member Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "(Obsolete) Media Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = new Guid("9DBBCBBB-2327-434A-B355-AF1B84E5010A"), Text = "(Obsolete) Multiple Media Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "Media Picker (legacy)", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = new Guid("9DBBCBBB-2327-434A-B355-AF1B84E5010A"), Text = "Multiple Media Picker (legacy)", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1050, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1050", SortOrder = 2, UniqueId = new Guid("B4E3535A-1753-47E2-8568-602CF8CFEE6F"), Text = "Multi URL Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1051, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1051", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3Guid, Text = "Media Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/obsoletemediapickernotice.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/obsoletemediapickernotice.html index cc861dcb4b..617ffd80d6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/obsoletemediapickernotice.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/obsoletemediapickernotice.html @@ -1 +1 @@ -

    Important: switching from the (Obsolete) Media Picker to Media Picker will mean all data (references to previously selected media items) will be deleted and no longer available.

    \ No newline at end of file +

    Important: switching from "Media Picker (legacy)" to "Media Picker" is not supported and doing so will mean all data (references to previously selected media items) will no longer be available on existing content items.

    diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs index 499737f3b7..a33bed89fa 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs @@ -8,17 +8,17 @@ namespace Umbraco.Web.PropertyEditors { /// /// Represents a media picker property editor. - /// Marked as Deprecated as best to use the NEW Media Picker aka MediaPicker3 + /// Named "(legacy)" as it's best to use the NEW Media Picker aka MediaPicker3 /// [DataEditor( Constants.PropertyEditors.Aliases.MediaPicker, EditorType.PropertyValue | EditorType.MacroParameter, - "(Obsolete) Media Picker", + "Media Picker (legacy)", "mediapicker", ValueType = ValueTypes.Text, Group = Constants.PropertyEditors.Groups.Media, Icon = Constants.Icons.MediaImage, - IsDeprecated = true)] + IsDeprecated = false)] public class MediaPickerPropertyEditor : DataEditor { From ee348be8a5dd57b28c3b16bb41ef1c8e4f9f4cf1 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Mon, 21 Jun 2021 17:59:01 +0100 Subject: [PATCH 237/289] Merge pull request #10496 from umbraco/v8/fix/10483 Fixes clashing aliases when new mediatypes had been installed (cherry picked from commit f69cee87f94f46ec014ca36d213552cd4813e6fc) --- src/Umbraco.Core/Constants-Conventions.cs | 28 ++- .../Migrations/Install/DatabaseDataCreator.cs | 8 +- .../Services/MediaServiceTests.cs | 4 +- src/Umbraco.Web.UI.Client/package-lock.json | 205 +++++------------- src/Umbraco.Web.UI.Client/package.json | 4 +- 5 files changed, 85 insertions(+), 164 deletions(-) diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index c8233c8d34..37267a5e22 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -119,25 +119,45 @@ namespace Umbraco.Core public const string Image = "Image"; /// - /// MediaType alias for a video. + /// MediaType name for a video. /// public const string Video = "Video"; /// - /// MediaType alias for an audio. + /// MediaType name for an audio. /// public const string Audio = "Audio"; /// - /// MediaType alias for an article. + /// MediaType name for an article. /// public const string Article = "Article"; /// - /// MediaType alias for vector graphics. + /// MediaType name for vector graphics. /// public const string VectorGraphics = "VectorGraphics"; + /// + /// MediaType alias for a video. + /// + public const string VideoAlias = "umbracoMediaVideo"; + + /// + /// MediaType alias for an audio. + /// + public const string AudioAlias = "umbracoMediaAudio"; + + /// + /// MediaType alias for an article. + /// + public const string ArticleAlias = "umbracoMediaArticle"; + + /// + /// MediaType alias for vector graphics. + /// + public const string VectorGraphicsAlias = "umbracoMediaVectorGraphics"; + /// /// MediaType alias indicating allowing auto-selection. /// diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index d78e0a90d7..d1957f501c 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -174,10 +174,10 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Constants.Conventions.MediaTypes.Folder, Icon = Constants.Icons.MediaFolder, Thumbnail = Constants.Icons.MediaFolder, IsContainer = false, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Constants.Conventions.MediaTypes.Image, Icon = Constants.Icons.MediaImage, Thumbnail = Constants.Icons.MediaImage, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Constants.Conventions.MediaTypes.File, Icon = Constants.Icons.MediaFile, Thumbnail = Constants.Icons.MediaFile, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 540, NodeId = 1034, Alias = Constants.Conventions.MediaTypes.Video, Icon = Constants.Icons.MediaVideo, Thumbnail = Constants.Icons.MediaVideo, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 541, NodeId = 1035, Alias = Constants.Conventions.MediaTypes.Audio, Icon = Constants.Icons.MediaAudio, Thumbnail = Constants.Icons.MediaAudio, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 542, NodeId = 1036, Alias = Constants.Conventions.MediaTypes.Article, Icon = Constants.Icons.MediaArticle, Thumbnail = Constants.Icons.MediaArticle, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 543, NodeId = 1037, Alias = Constants.Conventions.MediaTypes.VectorGraphics, Icon = Constants.Icons.MediaVectorGraphics, Thumbnail = Constants.Icons.MediaVectorGraphics, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 540, NodeId = 1034, Alias = Constants.Conventions.MediaTypes.VideoAlias, Icon = Constants.Icons.MediaVideo, Thumbnail = Constants.Icons.MediaVideo, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 541, NodeId = 1035, Alias = Constants.Conventions.MediaTypes.AudioAlias, Icon = Constants.Icons.MediaAudio, Thumbnail = Constants.Icons.MediaAudio, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 542, NodeId = 1036, Alias = Constants.Conventions.MediaTypes.ArticleAlias, Icon = Constants.Icons.MediaArticle, Thumbnail = Constants.Icons.MediaArticle, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 543, NodeId = 1037, Alias = Constants.Conventions.MediaTypes.VectorGraphicsAlias, Icon = Constants.Icons.MediaVectorGraphics, Thumbnail = Constants.Icons.MediaVectorGraphics, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Constants.Conventions.MemberTypes.DefaultAlias, Icon = Constants.Icons.Member, Thumbnail = Constants.Icons.Member, Variations = (byte) ContentVariation.Nothing }); } diff --git a/src/Umbraco.Tests/Services/MediaServiceTests.cs b/src/Umbraco.Tests/Services/MediaServiceTests.cs index d5cec11211..a0d74c0144 100644 --- a/src/Umbraco.Tests/Services/MediaServiceTests.cs +++ b/src/Umbraco.Tests/Services/MediaServiceTests.cs @@ -165,7 +165,7 @@ namespace Umbraco.Tests.Services var mediaService = ServiceContext.MediaService; var mediaType = MockedContentTypes.CreateNewMediaType(); ServiceContext.MediaTypeService.Save(mediaType); - var media = mediaService.CreateMedia(string.Empty, -1, "video"); + var media = mediaService.CreateMedia(string.Empty, -1, Constants.Conventions.MediaTypes.VideoAlias); // Act & Assert Assert.Throws(() => mediaService.Save(media)); @@ -177,7 +177,7 @@ namespace Umbraco.Tests.Services var mediaService = ServiceContext.MediaService; var mediaType = MockedContentTypes.CreateNewMediaType(); ServiceContext.MediaTypeService.Save(mediaType); - var media = mediaService.CreateMedia("Test", -1, "video"); + var media = mediaService.CreateMedia("Test", -1, Constants.Conventions.MediaTypes.VideoAlias); mediaService.Save(media); diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 3f53638fc6..b2d811df89 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -1836,8 +1836,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", - "dev": true, - "optional": true + "dev": true }, "base64id": { "version": "1.0.0", @@ -2084,7 +2083,6 @@ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", "dev": true, - "optional": true, "requires": { "p-finally": "^1.0.0" } @@ -2126,7 +2124,6 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha1-oWCRFxcQPAdBDO9j71Gzl8Alr5w=", "dev": true, - "optional": true, "requires": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" @@ -2136,15 +2133,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, - "optional": true + "dev": true }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", "dev": true, - "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -2160,7 +2155,6 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", "dev": true, - "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -2301,7 +2295,6 @@ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz", "integrity": "sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==", "dev": true, - "optional": true, "requires": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" @@ -2327,8 +2320,7 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true, - "optional": true + "dev": true }, "buffer-equal": { "version": "1.0.0", @@ -2503,9 +2495,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001168", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001168.tgz", - "integrity": "sha512-P2zmX7swIXKu+GMMR01TWa4csIKELTNnZKc+f1CjebmZJQtTAEXmpQSoKVJVVcvPGAA0TEYTOUp3VehavZSFPQ==", + "version": "1.0.30001237", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001237.tgz", + "integrity": "sha512-pDHgRndit6p1NR2GhzMbQ6CkRrp4VKuSsqbcLeOQppYPKOYkKT/6ZvZDvKJUqcmtyWIAHuZq3SVS2vc1egCZzw==", "dev": true }, "caseless": { @@ -2519,7 +2511,6 @@ "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", "integrity": "sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==", "dev": true, - "optional": true, "requires": { "get-proxy": "^2.0.0", "isurl": "^1.0.0-alpha5", @@ -2953,7 +2944,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", "dev": true, - "optional": true, "requires": { "graceful-readlink": ">= 1.0.0" } @@ -3048,7 +3038,6 @@ "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", "dev": true, - "optional": true, "requires": { "ini": "^1.3.4", "proto-list": "~1.2.1" @@ -3104,7 +3093,6 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", "dev": true, - "optional": true, "requires": { "safe-buffer": "5.1.2" } @@ -3548,7 +3536,6 @@ "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.0.tgz", "integrity": "sha1-eu3YVCflqS2s/lVnSnxQXpbQH50=", "dev": true, - "optional": true, "requires": { "decompress-tar": "^4.0.0", "decompress-tarbz2": "^4.0.0", @@ -3565,7 +3552,6 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", "dev": true, - "optional": true, "requires": { "pify": "^3.0.0" }, @@ -3574,8 +3560,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true + "dev": true } } } @@ -3586,7 +3571,6 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", "dev": true, - "optional": true, "requires": { "mimic-response": "^1.0.0" } @@ -3596,7 +3580,6 @@ "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", "dev": true, - "optional": true, "requires": { "file-type": "^5.2.0", "is-stream": "^1.1.0", @@ -3607,8 +3590,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3617,7 +3599,6 @@ "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", "dev": true, - "optional": true, "requires": { "decompress-tar": "^4.1.0", "file-type": "^6.1.0", @@ -3630,8 +3611,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", - "dev": true, - "optional": true + "dev": true } } }, @@ -3640,7 +3620,6 @@ "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", "dev": true, - "optional": true, "requires": { "decompress-tar": "^4.1.1", "file-type": "^5.2.0", @@ -3651,8 +3630,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3661,7 +3639,6 @@ "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", "dev": true, - "optional": true, "requires": { "file-type": "^3.8.0", "get-stream": "^2.2.0", @@ -3673,15 +3650,13 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", - "dev": true, - "optional": true + "dev": true }, "get-stream": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", "dev": true, - "optional": true, "requires": { "object-assign": "^4.0.1", "pinkie-promise": "^2.0.0" @@ -3691,8 +3666,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3974,8 +3948,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3992,8 +3965,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true, - "optional": true + "dev": true }, "duplexify": { "version": "3.7.1", @@ -4596,7 +4568,6 @@ "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", "dev": true, - "optional": true, "requires": { "cross-spawn": "^5.0.1", "get-stream": "^3.0.0", @@ -4612,7 +4583,6 @@ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", "dev": true, - "optional": true, "requires": { "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", @@ -4752,7 +4722,6 @@ "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", "dev": true, - "optional": true, "requires": { "mime-db": "^1.28.0" } @@ -4762,7 +4731,6 @@ "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", "dev": true, - "optional": true, "requires": { "ext-list": "^2.0.0", "sort-keys-length": "^1.0.0" @@ -4997,7 +4965,6 @@ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", "dev": true, - "optional": true, "requires": { "pend": "~1.2.0" } @@ -5036,15 +5003,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", - "dev": true, - "optional": true + "dev": true }, "filenamify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz", "integrity": "sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==", "dev": true, - "optional": true, "requires": { "filename-reserved-regex": "^2.0.0", "strip-outer": "^1.0.0", @@ -5405,8 +5370,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha1-a+Dem+mYzhavivwkSXue6bfM2a0=", - "dev": true, - "optional": true + "dev": true }, "fs-mkdirp-stream": { "version": "1.0.0", @@ -5453,8 +5417,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -5475,14 +5438,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5497,20 +5458,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -5627,8 +5585,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -5640,7 +5597,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5655,7 +5611,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5663,14 +5618,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5689,7 +5642,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5770,8 +5722,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -5783,7 +5734,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5869,8 +5819,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -5906,7 +5855,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5926,7 +5874,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5970,14 +5917,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -6004,7 +5949,6 @@ "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", "integrity": "sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==", "dev": true, - "optional": true, "requires": { "npm-conf": "^1.1.0" } @@ -6013,15 +5957,13 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true, - "optional": true + "dev": true }, "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true, - "optional": true + "dev": true }, "get-value": { "version": "2.0.6", @@ -6336,8 +6278,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", - "dev": true, - "optional": true + "dev": true }, "growly": { "version": "1.3.0", @@ -7108,8 +7049,7 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", - "dev": true, - "optional": true + "dev": true }, "has-symbols": { "version": "1.0.0", @@ -7122,7 +7062,6 @@ "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", "dev": true, - "optional": true, "requires": { "has-symbol-support-x": "^1.4.1" } @@ -7322,8 +7261,7 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true, - "optional": true + "dev": true }, "ignore": { "version": "4.0.6", @@ -7453,7 +7391,6 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", "dev": true, - "optional": true, "requires": { "repeating": "^2.0.0" } @@ -7781,7 +7718,6 @@ "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7834,8 +7770,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", - "dev": true, - "optional": true + "dev": true }, "is-negated-glob": { "version": "1.0.0", @@ -7873,15 +7808,13 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", - "dev": true, - "optional": true + "dev": true }, "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true, - "optional": true + "dev": true }, "is-plain-object": { "version": "2.0.4", @@ -7951,15 +7884,13 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", - "dev": true, - "optional": true + "dev": true }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true, - "optional": true + "dev": true }, "is-svg": { "version": "3.0.0", @@ -8056,7 +7987,6 @@ "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", "dev": true, - "optional": true, "requires": { "has-to-string-tag-x": "^1.2.0", "is-object": "^1.0.1" @@ -8969,8 +8899,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", "integrity": "sha1-b54wtHCE2XGnyCD/FabFFnt0wm8=", - "dev": true, - "optional": true + "dev": true }, "lpad-align": { "version": "1.1.2", @@ -9040,8 +8969,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true, - "optional": true + "dev": true }, "map-visit": { "version": "1.0.0", @@ -9211,8 +9139,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "optional": true + "dev": true }, "minimatch": { "version": "3.0.4", @@ -9435,9 +9362,9 @@ "dev": true }, "nouislider": { - "version": "14.6.3", - "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-14.6.3.tgz", - "integrity": "sha512-/3tAqsWY2JYW9vd7bC14bFRA1P9A+pRHOtKmoMsyfnB0fQcd1UFx2pdY1Ey5wAUzTnXTesmYaEo/ecLVETijIQ==" + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-14.6.4.tgz", + "integrity": "sha512-PVCGYl+aC7/nVEbW61ypJWfuW3UCpvctz/luxpt4byxxli1FFyjBX9NIiy4Yak9AaO6a5BkPGfFYMCW4eg3eeQ==" }, "now-and-later": { "version": "2.0.1", @@ -12547,7 +12474,6 @@ "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", "dev": true, - "optional": true, "requires": { "config-chain": "^1.1.11", "pify": "^3.0.0" @@ -12557,8 +12483,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -12567,7 +12492,6 @@ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "dev": true, - "optional": true, "requires": { "path-key": "^2.0.0" } @@ -12929,8 +12853,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true, - "optional": true + "dev": true }, "p-is-promise": { "version": "1.1.0", @@ -12967,7 +12890,6 @@ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", "integrity": "sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=", "dev": true, - "optional": true, "requires": { "p-finally": "^1.0.0" } @@ -13158,8 +13080,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", - "dev": true, - "optional": true + "dev": true }, "performance-now": { "version": "2.1.0", @@ -13667,8 +13588,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", - "dev": true, - "optional": true + "dev": true }, "prr": { "version": "1.0.1", @@ -14024,7 +13944,6 @@ "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "dev": true, - "optional": true, "requires": { "is-finite": "^1.0.0" } @@ -14386,7 +14305,6 @@ "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", "dev": true, - "optional": true, "requires": { "commander": "~2.8.1" } @@ -14789,7 +14707,6 @@ "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", "dev": true, - "optional": true, "requires": { "is-plain-obj": "^1.0.0" } @@ -14799,7 +14716,6 @@ "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", "dev": true, - "optional": true, "requires": { "sort-keys": "^1.0.0" } @@ -15130,7 +15046,6 @@ "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", "dev": true, - "optional": true, "requires": { "is-natural-number": "^4.0.1" } @@ -15139,8 +15054,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true, - "optional": true + "dev": true }, "strip-indent": { "version": "1.0.1", @@ -15163,7 +15077,6 @@ "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", "integrity": "sha1-sv0qv2YEudHmATBXGV34Nrip1jE=", "dev": true, - "optional": true, "requires": { "escape-string-regexp": "^1.0.2" } @@ -15289,7 +15202,6 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", "integrity": "sha1-jqVdqzeXIlPZqa+Q/c1VmuQ1xVU=", "dev": true, - "optional": true, "requires": { "bl": "^1.0.0", "buffer-alloc": "^1.2.0", @@ -15304,15 +15216,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, - "optional": true + "dev": true }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=", "dev": true, - "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -15328,7 +15238,6 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=", "dev": true, - "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -15339,15 +15248,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", - "dev": true, - "optional": true + "dev": true }, "tempfile": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-2.0.0.tgz", "integrity": "sha1-awRGhWqbERTRhW/8vlCczLCXcmU=", "dev": true, - "optional": true, "requires": { "temp-dir": "^1.0.0", "uuid": "^3.0.1" @@ -15442,8 +15349,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", - "dev": true, - "optional": true + "dev": true }, "timers-ext": { "version": "0.1.7", @@ -15500,8 +15406,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", "integrity": "sha1-STvUj2LXxD/N7TE6A9ytsuEhOoA=", - "dev": true, - "optional": true + "dev": true }, "to-fast-properties": { "version": "2.0.0", @@ -15605,7 +15510,6 @@ "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", "dev": true, - "optional": true, "requires": { "escape-string-regexp": "^1.0.2" } @@ -15742,7 +15646,6 @@ "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz", "integrity": "sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg==", "dev": true, - "optional": true, "requires": { "buffer": "^5.2.1", "through": "^2.3.8" @@ -15943,8 +15846,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", - "dev": true, - "optional": true + "dev": true }, "use": { "version": "3.1.1", @@ -16440,7 +16342,6 @@ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", "dev": true, - "optional": true, "requires": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 6514f2f217..d0f1c1cb99 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -54,7 +54,7 @@ "@babel/core": "7.6.4", "@babel/preset-env": "7.6.3", "autoprefixer": "9.6.5", - "caniuse-lite": "^1.0.30001037", + "caniuse-lite": "^1.0.30001237", "cssnano": "4.1.10", "fs": "0.0.2", "gulp": "4.0.2", @@ -77,8 +77,8 @@ "jasmine-core": "3.5.0", "jsdom": "16.4.0", "karma": "4.4.1", - "karma-jsdom-launcher": "^8.0.2", "karma-jasmine": "2.0.1", + "karma-jsdom-launcher": "^8.0.2", "karma-junit-reporter": "2.0.1", "karma-spec-reporter": "0.0.32", "less": "3.10.3", From 5f94cb83ed37fdbcbf3eae731518bc0dcb249474 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Mon, 7 Jun 2021 02:28:47 +0200 Subject: [PATCH 238/289] Support custom SVG icon in grid editor row configuration (#10361) * Use custom svg icon in grid editor row configuration * Use umb-icon in grid editor placeholder * Use control icon in grid editor placeholder * use umb-icon in itempicker to ensure svg icons are displayed Co-authored-by: Nathan Woulfe (cherry picked from commit f0e13e87d50598a98645820a8935f5857a1f9a93) --- .../src/less/components/umb-form-check.less | 20 ++++++++++++------- .../src/less/components/umb-grid.less | 6 +++--- .../grid/dialogs/rowconfig.html | 2 +- .../grid/editors/embed.controller.js | 4 +++- .../propertyeditors/grid/editors/embed.html | 2 +- .../grid/editors/macro.controller.js | 2 ++ .../propertyeditors/grid/editors/macro.html | 4 ++-- .../grid/editors/media.controller.js | 2 ++ .../propertyeditors/grid/editors/media.html | 3 ++- .../grid/editors/textstring.html | 8 ++++++-- .../propertyeditors/grid/grid.controller.js | 1 - 11 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less index a96c59de84..9be50b877a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less @@ -14,13 +14,12 @@ margin-left: 30px; position: relative; } - - &.-small-text{ + &.-small-text { font-size: 13px; } - &.-bold{ + &.-bold { font-weight: 700; } @@ -38,12 +37,15 @@ &:hover ~ .umb-form-check__state .umb-form-check__check { border-color: @inputBorderFocus; } + &:checked ~ .umb-form-check__state .umb-form-check__check { border-color: @ui-option-type; } + &[type='checkbox']:checked ~ .umb-form-check__state .umb-form-check__check { background-color: @ui-option-type; } + &:checked:hover ~ .umb-form-check__state .umb-form-check__check { &::before { background: @ui-option-type-hover; @@ -80,16 +82,22 @@ border: 2px solid @inputBorderTabFocus; margin: -1px; } + .tabbing-active &.umb-form-check--checkbox &__input:focus ~ .umb-form-check__state .umb-form-check__check { outline: 2px solid @inputBorderTabFocus; } + .tabbing-active &.umb-form-check--checkbox &__input:checked:focus ~ .umb-form-check__state .umb-form-check__check { border-color: @white; } - // add spacing between when flexed/inline, equal to the width of the input .flex & + & { - margin-left:@checkboxWidth; + margin-left: @checkboxWidth; + } + + .icon, + .umb-icon { + font-size: 1.2rem; } &__state { @@ -98,10 +106,8 @@ width: 20px; position: absolute; top: 0; - } - &__check { display: flex; position: relative; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less index 281284a5ca..033fe8fc0e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less @@ -404,12 +404,12 @@ box-sizing: border-box; } -.umb-grid .umb-editor-placeholder i { +.umb-grid .umb-editor-placeholder .icon { color: @gray-8; font-size: 85px; - line-height: 85px; + line-height: 1; display: block; - margin-bottom: 10px; + margin: 10px auto; } .umb-grid .umb-editor-preview { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html index 5cf0676526..312c158351 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html @@ -109,7 +109,7 @@ - {{editor.name}} + ({{editor.alias}}) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.controller.js index dbc847738c..909b2a555f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.controller.js @@ -3,7 +3,9 @@ angular.module("umbraco") function ($scope, $timeout, $sce, editorService) { function onInit() { - + + $scope.control.icon = $scope.control.icon || 'icon-movie-alt'; + var embedPreview = Utilities.isObject($scope.control.value) && $scope.control.value.preview ? $scope.control.value.preview : $scope.control.value; $scope.trustedValue = embedPreview ? $sce.trustAsHtml(embedPreview) : null; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.html index 14741d6ca6..9673109816 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.html @@ -1,7 +1,7 @@
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.controller.js index 5e66684ac5..486bdab044 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.controller.js @@ -2,6 +2,8 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.Grid.MacroController", function ($scope, $timeout, editorService, macroResource, macroService, localizationService, $routeParams) { + $scope.control.icon = $scope.control.icon || 'icon-settings-alt'; + localizationService.localize("grid_clickToInsertMacro").then(function(label) { $scope.title = label; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.html index c07d29d89c..300ec91bcc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.html @@ -2,9 +2,9 @@
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index 15f5ceaa88..3de340a083 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -318,7 +318,6 @@ angular.module("umbraco") // Add items overlay menu // ********************************************* $scope.openEditorOverlay = function (event, area, index, key) { - const dialog = { view: "itempicker", filter: area.$allowedEditors.length > 15, From d0220c01f97da78e10042321f8ad7e4748cd24ac Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Mon, 31 May 2021 19:09:20 +0200 Subject: [PATCH 239/289] Use custom SVG icon in user group filter (cherry picked from commit 57cc3d5da2d9f732d79d9a897ed5e7e452a09867) --- .../src/views/users/views/users/users.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 57c87807fc..57c1128151 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -150,7 +150,7 @@ From a69d4349d1f589574d4056420abc5f6bf01d1f5d Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Mon, 7 Jun 2021 02:40:14 +0200 Subject: [PATCH 240/289] Support custom SVG icon in Nested Content (#10368) * Use custom SVG icon in Nested Content * Get icon once * Assign value instead of compare (cherry picked from commit 340273a6045e6466f41d5e14f335d6e990da9b75) --- .../nestedcontent/nestedcontent.propertyeditor.html | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html index aaebb5d07e..22897e3ca2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html @@ -6,9 +6,9 @@
    -
    +
    - +
    @@ -18,7 +18,13 @@ ng-hide="vm.singleMode" umb-auto-focus="{{vm.focusOnNode && vm.currentNode.key === node.key ? 'true' : 'false'}}"> -
    +
    + + + +
    -
    From bb79544e2a88af0e93d44ca0eb4bc62959a7b4f6 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 22 Jun 2021 09:35:41 +0200 Subject: [PATCH 241/289] Update 01_bug_report.yml --- .github/ISSUE_TEMPLATE/01_bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/01_bug_report.yml b/.github/ISSUE_TEMPLATE/01_bug_report.yml index 04d1a0e04c..800ac53e68 100644 --- a/.github/ISSUE_TEMPLATE/01_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/01_bug_report.yml @@ -6,7 +6,7 @@ body: - type: input id: "version" attributes: - label: "Which Umbraco version are you using?" + label: "Which *exact* Umbraco version are you using? For example: 8.13.1 - don't just write v8" description: "Use the help icon in the Umbraco backoffice to find the version you're using" validations: required: true From 8a810e698c1b99affc9536abcd06f0cb3f40e581 Mon Sep 17 00:00:00 2001 From: Patrick de Mooij Date: Fri, 11 Jun 2021 22:09:08 +0200 Subject: [PATCH 242/289] 10429: Make sure to set filtered variable on media item --- .../infiniteeditors/mediapicker/mediapicker.controller.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js index 029dedf214..0b9c59f2da 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js @@ -477,9 +477,15 @@ angular.module("umbraco") vm.loading = true; entityResource.getPagedDescendants($scope.filterOptions.excludeSubFolders ? $scope.currentFolder.id : $scope.startNodeId, "Media", vm.searchOptions) .then(function (data) { + // update image data to work with image grid if (data.items) { - data.items.forEach(mediaItem => setMediaMetaData(mediaItem)); + var allowedTypes = dialogOptions.filter ? dialogOptions.filter.split(",") : null; + + data.items.forEach(function(mediaItem) { + setMediaMetaData(mediaItem); + mediaItem.filtered = allowedTypes && allowedTypes.indexOf(mediaItem.metaData.ContentTypeAlias) < 0; + }); } // update images From 797a06e72122ec96e6cf785343bacdab59c9015a Mon Sep 17 00:00:00 2001 From: Patrick de Mooij Date: Fri, 11 Jun 2021 22:09:08 +0200 Subject: [PATCH 243/289] 10429: Make sure to set filtered variable on media item (cherry picked from commit 8a810e698c1b99affc9536abcd06f0cb3f40e581) --- .../infiniteeditors/mediapicker/mediapicker.controller.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js index 029dedf214..0b9c59f2da 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js @@ -477,9 +477,15 @@ angular.module("umbraco") vm.loading = true; entityResource.getPagedDescendants($scope.filterOptions.excludeSubFolders ? $scope.currentFolder.id : $scope.startNodeId, "Media", vm.searchOptions) .then(function (data) { + // update image data to work with image grid if (data.items) { - data.items.forEach(mediaItem => setMediaMetaData(mediaItem)); + var allowedTypes = dialogOptions.filter ? dialogOptions.filter.split(",") : null; + + data.items.forEach(function(mediaItem) { + setMediaMetaData(mediaItem); + mediaItem.filtered = allowedTypes && allowedTypes.indexOf(mediaItem.metaData.ContentTypeAlias) < 0; + }); } // update images From f04e27ca197ae99975ed90aedaa4e53f95a2aee7 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 22 Jun 2021 14:16:05 +0200 Subject: [PATCH 244/289] Bump version to 8.14.1 --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 3619dc1371..3a43aec402 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.14.0")] -[assembly: AssemblyInformationalVersion("8.14.0")] +[assembly: AssemblyFileVersion("8.14.1")] +[assembly: AssemblyInformationalVersion("8.14.1")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index f3652a1273..f238984205 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -348,9 +348,9 @@ False True - 8140 + 8141 / - http://localhost:8140 + http://localhost:8141 8131 / http://localhost:8131 From 94d525d88f713b36419f28bfda4d82ee68637d83 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 22 Jun 2021 15:12:07 -0600 Subject: [PATCH 245/289] small changes based on review. --- .../Migrations/Upgrade/UmbracoPlan.cs | 4 +- .../AddCmsContentNuByteColumn.cs | 2 +- .../UpgradedIncludeIndexes.cs | 2 +- src/Umbraco.Core/Umbraco.Core.csproj | 4 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 7 +- .../MsgPackContentNestedDataSerializer.cs | 87 +------------------ 6 files changed, 10 insertions(+), 96 deletions(-) rename src/Umbraco.Core/Migrations/Upgrade/{V_8_12_0 => V_8_15_0}/AddCmsContentNuByteColumn.cs (90%) rename src/Umbraco.Core/Migrations/Upgrade/{V_8_12_0 => V_8_15_0}/UpgradedIncludeIndexes.cs (98%) diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 2a24c800b5..d9a71a1cc2 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Migrations.Upgrade.V_8_1_0; using Umbraco.Core.Migrations.Upgrade.V_8_6_0; using Umbraco.Core.Migrations.Upgrade.V_8_9_0; using Umbraco.Core.Migrations.Upgrade.V_8_10_0; -using Umbraco.Core.Migrations.Upgrade.V_8_12_0; +using Umbraco.Core.Migrations.Upgrade.V_8_15_0; namespace Umbraco.Core.Migrations.Upgrade { @@ -203,7 +203,7 @@ namespace Umbraco.Core.Migrations.Upgrade // to 8.10.0 To("{D6A8D863-38EC-44FB-91EC-ACD6A668BD18}"); - // to 8.12.0... + // to 8.15.0... To("{8DDDCD0B-D7D5-4C97-BD6A-6B38CA65752F}"); To("{4695D0C9-0729-4976-985B-048D503665D8}"); diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_12_0/AddCmsContentNuByteColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs similarity index 90% rename from src/Umbraco.Core/Migrations/Upgrade/V_8_12_0/AddCmsContentNuByteColumn.cs rename to src/Umbraco.Core/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs index 7c793688ec..3eab1a812e 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_12_0/AddCmsContentNuByteColumn.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs @@ -1,7 +1,7 @@ using System.Linq; using Umbraco.Core.Persistence.Dtos; -namespace Umbraco.Core.Migrations.Upgrade.V_8_12_0 +namespace Umbraco.Core.Migrations.Upgrade.V_8_15_0 { public class AddCmsContentNuByteColumn : MigrationBase { diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_12_0/UpgradedIncludeIndexes.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_15_0/UpgradedIncludeIndexes.cs similarity index 98% rename from src/Umbraco.Core/Migrations/Upgrade/V_8_12_0/UpgradedIncludeIndexes.cs rename to src/Umbraco.Core/Migrations/Upgrade/V_8_15_0/UpgradedIncludeIndexes.cs index d88abdef75..d51f99fb44 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_12_0/UpgradedIncludeIndexes.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_15_0/UpgradedIncludeIndexes.cs @@ -3,7 +3,7 @@ using Umbraco.Core.Migrations.Expressions.Execute.Expressions; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; -namespace Umbraco.Core.Migrations.Upgrade.V_8_12_0 +namespace Umbraco.Core.Migrations.Upgrade.V_8_15_0 { public class UpgradedIncludeIndexes : MigrationBase { diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 956ea7602e..514bd8d798 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -139,8 +139,8 @@ - - + + diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 2d6ce89a7b..815db985ce 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -79,8 +79,6 @@ - - 1.8.14 @@ -102,10 +100,7 @@ - - 4.14.5 - - + diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index 944d93107d..6ae872ef69 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -28,8 +28,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // TODO: We want to be able to intern the strings for aliases when deserializing like we do for Newtonsoft but I'm unsure exactly how // to do that but it would seem to be with a custom message pack resolver but I haven't quite figured out based on the docs how // to do that since that is part of the int key -> string mapping operation, might have to see the source code to figure that one out. - - // resolver custom types first + // There are docs here on how to build one of these: https://github.com/neuecc/MessagePack-CSharp/blob/master/README.md#low-level-api-imessagepackformattert + // and there are a couple examples if you search on google for them but this will need to be a separate project. + // NOTE: resolver custom types first // new ContentNestedDataResolver(), // finally use standard resolver @@ -121,87 +122,5 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } } } - - - - //private class ContentNestedDataResolver : IFormatterResolver - //{ - // // GetFormatter's get cost should be minimized so use type cache. - // public IMessagePackFormatter GetFormatter() => FormatterCache.Formatter; - - // private static class FormatterCache - // { - // public static readonly IMessagePackFormatter Formatter; - - // // generic's static constructor should be minimized for reduce type generation size! - // // use outer helper method. - // static FormatterCache() - // { - // Formatter = (IMessagePackFormatter)SampleCustomResolverGetFormatterHelper.GetFormatter(typeof(T)); - // } - // } - //} - - //internal static class SampleCustomResolverGetFormatterHelper - //{ - // // If type is concrete type, use type-formatter map - // static readonly Dictionary _formatterMap = new Dictionary() - // { - // {typeof(ContentNestedData), new ContentNestedDataFormatter()} - // // add more your own custom serializers. - // }; - - // internal static object GetFormatter(Type t) - // { - // object formatter; - // if (_formatterMap.TryGetValue(t, out formatter)) - // { - // return formatter; - // } - - // // If target type is generics, use MakeGenericType. - // if (t.IsGenericParameter && t.GetGenericTypeDefinition() == typeof(ValueTuple<,>)) - // { - // return Activator.CreateInstance(typeof(ValueTupleFormatter<,>).MakeGenericType(t.GenericTypeArguments)); - // } - - // // If type can not get, must return null for fallback mechanism. - // return null; - // } - //} - - //public class ContentNestedDataFormatter : IMessagePackFormatter - //{ - // public void Serialize(ref MessagePackWriter writer, ContentNestedData value, MessagePackSerializerOptions options) - // { - // if (value == null) - // { - // writer.WriteNil(); - // return; - // } - - // writer.WriteArrayHeader(3); - // writer.WriteString(value.UrlSegment); - // writer.WriteString(value.FullName); - // writer.WriteString(value.Age); - - // writer.WriteString(value.FullName); - // } - - // public ContentNestedData Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) - // { - // if (reader.TryReadNil()) - // { - // return null; - // } - - // options.Security.DepthStep(ref reader); - - // var path = reader.ReadString(); - - // reader.Depth--; - // return new FileInfo(path); - // } - //} } } From ec65a6c99b1887c2abe03e5210d8898cc8185ba9 Mon Sep 17 00:00:00 2001 From: Russell Date: Wed, 23 Jun 2021 09:15:05 +1200 Subject: [PATCH 246/289] Add lang to extended valid elements for span Resolution to https://github.com/umbraco/Umbraco-CMS/issues/10505 --- .../src/common/services/tinymce.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index fbe342d44f..20e7ad665d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -11,7 +11,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s //These are absolutely required in order for the macros to render inline //we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce - var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style]"; + var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style|lang]"; var fallbackStyles = [{ title: "Page header", block: "h2" }, { title: "Section header", block: "h3" }, { title: "Paragraph header", block: "h4" }, { title: "Normal", block: "p" }, { title: "Quote", block: "blockquote" }, { title: "Code", block: "code" }]; // these languages are available for localization var availableLanguages = [ From 6bd2f7f696bd6d7c04379c090ae096bbea00a526 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 23 Jun 2021 09:10:40 +0200 Subject: [PATCH 247/289] Fixes the acceptance tests "Content with contentpicker" --- .../cypress/integration/Content/content.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts index 0cec374c5d..25d0b02e1d 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts @@ -605,7 +605,7 @@ context('Content', () => { cy.umbracoEditorHeaderName('ContentPickerContent'); cy.get('.umb-node-preview-add').click(); // Should really try and find a better way to do this, but umbracoTreeItem tries to click the content pane in the background - cy.get('[ng-if="vm.treeReady"] > .umb-tree > [ng-if="!tree.root.containsGroups"] > .umb-animated > .umb-tree-item__inner').click(); + cy.get('[ng-if="vm.treeReady"] > .umb-tree .umb-tree-item__inner').click(); // We have to wait for the picked content to show up or it wont be added. cy.get('.umb-node-preview__description').should('be.visible'); //save and publish From 9382f6c32d430a4bafe0bfdbc3098c02f1f00925 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 23 Jun 2021 10:10:28 +0200 Subject: [PATCH 248/289] Add the ability to convert Media Picker v2 to v3 progressively (#10517) Co-authored-by: Ronald Barendse --- .../obsoletemediapickernotice.html | 1 - .../MediaPicker3PropertyEditor.cs | 99 ++++++++++++---- .../MediaPickerConfiguration.cs | 3 - .../MediaPickerWithCropsValueConverter.cs | 109 ++++++------------ 4 files changed, 116 insertions(+), 96 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/views/prevalueeditors/obsoletemediapickernotice.html diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/obsoletemediapickernotice.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/obsoletemediapickernotice.html deleted file mode 100644 index 617ffd80d6..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/obsoletemediapickernotice.html +++ /dev/null @@ -1 +0,0 @@ -

    Important: switching from "Media Picker (legacy)" to "Media Picker" is not supported and doing so will mean all data (references to previously selected media items) will no longer be available on existing content items.

    diff --git a/src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs index 4ce376f543..43d190e173 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs @@ -1,10 +1,15 @@ -using System.Collections.Generic; -using Newtonsoft.Json; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; -using Umbraco.Web.PropertyEditors.ValueConverters; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Core.Services; namespace Umbraco.Web.PropertyEditors { @@ -22,43 +27,97 @@ namespace Umbraco.Web.PropertyEditors public class MediaPicker3PropertyEditor : DataEditor { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// + /// The logger. public MediaPicker3PropertyEditor(ILogger logger) : base(logger) - { - } + { } /// protected override IConfigurationEditor CreateConfigurationEditor() => new MediaPicker3ConfigurationEditor(); + /// protected override IDataValueEditor CreateValueEditor() => new MediaPicker3PropertyValueEditor(Attribute); internal class MediaPicker3PropertyValueEditor : DataValueEditor, IDataValueReference { - /// - /// Note: no FromEditor() and ToEditor() methods - /// We do not want to transform the way the data is stored in the DB and would like to keep a raw JSON string - /// - public MediaPicker3PropertyValueEditor(DataEditorAttribute attribute) : base(attribute) + public MediaPicker3PropertyValueEditor(DataEditorAttribute attribute) + : base(attribute) + { } + + public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null) { + var value = property.GetValue(culture, segment); + + return Deserialize(value); } public IEnumerable GetReferences(object value) { - var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); - - if (rawJson.IsNullOrWhiteSpace()) - yield break; - - var mediaWithCropsDtos = JsonConvert.DeserializeObject(rawJson); - - foreach (var mediaWithCropsDto in mediaWithCropsDtos) + foreach (var dto in Deserialize(value)) { - yield return new UmbracoEntityReference(GuidUdi.Create(Constants.UdiEntityType.Media, mediaWithCropsDto.MediaKey)); + yield return new UmbracoEntityReference(Udi.Create(Constants.UdiEntityType.Media, dto.MediaKey)); } } + internal static IEnumerable Deserialize(object value) + { + var rawJson = value is string str ? str : value?.ToString(); + if (string.IsNullOrWhiteSpace(rawJson)) + { + yield break; + } + + if (!rawJson.DetectIsJson()) + { + // Old comma seperated UDI format + foreach (var udiStr in rawJson.Split(Constants.CharArrays.Comma)) + { + if (GuidUdi.TryParse(udiStr, out var udi)) + { + yield return new MediaWithCropsDto + { + Key = Guid.NewGuid(), + MediaKey = udi.Guid, + Crops = Enumerable.Empty(), + FocalPoint = new ImageCropperValue.ImageCropperFocalPoint + { + Left = 0.5m, + Top = 0.5m + } + }; + } + } + } + else + { + // New JSON format + foreach (var dto in JsonConvert.DeserializeObject>(rawJson)) + { + yield return dto; + } + } + } + + /// + /// Model/DTO that represents the JSON that the MediaPicker3 stores. + /// + [DataContract] + internal class MediaWithCropsDto + { + [DataMember(Name = "key")] + public Guid Key { get; set; } + + [DataMember(Name = "mediaKey")] + public Guid MediaKey { get; set; } + + [DataMember(Name = "crops")] + public IEnumerable Crops { get; set; } + + [DataMember(Name = "focalPoint")] + public ImageCropperValue.ImageCropperFocalPoint FocalPoint { get; set; } + } } } } diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs index 7266be9c26..b8b9476184 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerConfiguration.cs @@ -8,9 +8,6 @@ namespace Umbraco.Web.PropertyEditors /// public class MediaPickerConfiguration : IIgnoreUserStartNodesConfig { - [ConfigurationField("notice", "You can NOT change the property editor", "obsoletemediapickernotice")] - public bool Notice { get; set; } - [ConfigurationField("multiPicker", "Pick multiple items", "boolean")] public bool Multiple { get; set; } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs index f9b2ad75e1..f2f055d698 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs @@ -1,8 +1,6 @@ -using Newtonsoft.Json; -using System; -using System.Collections; +using System; using System.Collections.Generic; -using System.Runtime.Serialization; +using System.Linq; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; @@ -14,7 +12,6 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class MediaPickerWithCropsValueConverter : PropertyValueConverterBase { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; public MediaPickerWithCropsValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor) @@ -22,98 +19,66 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); } - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; - - /// - /// Enusre this property value convertor is for the New Media Picker with Crops aka MediaPicker 3 - /// public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.Equals(Core.Constants.PropertyEditors.Aliases.MediaPicker3); - /// - /// Check if the raw JSON value is not an empty array - /// - public override bool? IsValue(object value, PropertyValueLevel level) => value?.ToString() != "[]"; - - /// - /// What C# model type does the raw JSON return for Models & Views - /// - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + public override bool? IsValue(object value, PropertyValueLevel level) { - // Check do we want to return IPublishedContent collection still or a NEW model ? - var isMultiple = IsMultipleDataType(propertyType.DataType); - return isMultiple - ? typeof(IEnumerable) - : typeof(MediaWithCrops); + var isValue = base.IsValue(value, level); + if (isValue != false && level == PropertyValueLevel.Source) + { + // Empty JSON array is not a value + isValue = value?.ToString() != "[]"; + } + + return isValue; } - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) => source?.ToString(); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => IsMultipleDataType(propertyType.DataType) + ? typeof(IEnumerable) + : typeof(MediaWithCrops); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { - var mediaItems = new List(); var isMultiple = IsMultipleDataType(propertyType.DataType); - if (inter == null) + if (string.IsNullOrEmpty(inter?.ToString())) { - return isMultiple ? mediaItems: null; + // Short-circuit on empty value + return isMultiple ? Enumerable.Empty() : null; } - var dtos = JsonConvert.DeserializeObject>(inter.ToString()); + var mediaItems = new List(); + var dtos = MediaPicker3PropertyEditor.MediaPicker3PropertyValueEditor.Deserialize(inter); - foreach(var media in dtos) + foreach (var dto in dtos) { - var item = _publishedSnapshotAccessor.PublishedSnapshot.Media.GetById(media.MediaKey); - if (item != null) + var mediaItem = _publishedSnapshotAccessor.PublishedSnapshot.Media.GetById(preview, dto.MediaKey); + if (mediaItem != null) { mediaItems.Add(new MediaWithCrops { - MediaItem = item, + MediaItem = mediaItem, LocalCrops = new ImageCropperValue { - Crops = media.Crops, - FocalPoint = media.FocalPoint, - Src = item.Url() + Crops = dto.Crops, + FocalPoint = dto.FocalPoint, + Src = mediaItem.Url() } }); + + if (!isMultiple) + { + // Short-circuit on single item + break; + } } } - return isMultiple ? mediaItems : FirstOrDefault(mediaItems); + return isMultiple ? mediaItems : mediaItems.FirstOrDefault(); } - /// - /// Is the media picker configured to pick multiple media items - /// - /// - /// - private bool IsMultipleDataType(PublishedDataType dataType) - { - var config = dataType.ConfigurationAs(); - return config.Multiple; - } - - private object FirstOrDefault(IList mediaItems) - { - return mediaItems.Count == 0 ? null : mediaItems[0]; - } - - - /// - /// Model/DTO that represents the JSON that the MediaPicker3 stores - /// - [DataContract] - internal class MediaWithCropsDto - { - [DataMember(Name = "key")] - public Guid Key { get; set; } - - [DataMember(Name = "mediaKey")] - public Guid MediaKey { get; set; } - - [DataMember(Name = "crops")] - public IEnumerable Crops { get; set; } - - [DataMember(Name = "focalPoint")] - public ImageCropperValue.ImageCropperFocalPoint FocalPoint { get; set; } - } + private bool IsMultipleDataType(PublishedDataType dataType) => dataType.ConfigurationAs().Multiple; } } From 2c59da91b60c32b7762cdf3c755988a980ab5745 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 23 Jun 2021 11:02:36 -0600 Subject: [PATCH 249/289] fix merge --- .../DataSource/BTree.DictionaryOfPropertyDataSerializer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs index 1b96538dd0..579e53177d 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs @@ -34,7 +34,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource for (var j = 0; j < vcount; j++) { var pdata = new PropertyData(); - pdatas[j] =pdata; + pdatas[j] = pdata; // everything that can be null is read/written as object // even though - culture and segment should never be null here, as 'null' represents From 56b7b0df69e473d49dc89cf3f43817560e66d23a Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 23 Jun 2021 11:13:19 -0600 Subject: [PATCH 250/289] fix build, re-adds braces after merge --- ...Tree.DictionaryOfPropertyDataSerializer.cs | 4 +-- src/Umbraco.Web/PublishedPropertyExtension.cs | 26 +++++++++++++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs index fa0d853e35..304fc6def0 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs @@ -13,12 +13,10 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource { public IDictionary ReadFrom(Stream stream) { - // read properties count - var pcount = PrimitiveSerializer.Int32.ReadFrom(stream); + var pcount = PrimitiveSerializer.Int32.ReadFrom(stream); var dict = new Dictionary(pcount,StringComparer.InvariantCultureIgnoreCase); - var dict = new Dictionary(pcount,StringComparer.InvariantCultureIgnoreCase); // read each property for (var i = 0; i < pcount; i++) { diff --git a/src/Umbraco.Web/PublishedPropertyExtension.cs b/src/Umbraco.Web/PublishedPropertyExtension.cs index b431f24828..4f27a429a4 100644 --- a/src/Umbraco.Web/PublishedPropertyExtension.cs +++ b/src/Umbraco.Web/PublishedPropertyExtension.cs @@ -36,10 +36,17 @@ namespace Umbraco.Web { // we have a value // try to cast or convert it - var value = property.GetValue(culture, segment); - if (value is T valueAsT) return valueAsT; + var value = property.GetValue(culture, segment); + if (value is T valueAsT) + { + return valueAsT; + } + var valueConverted = value.TryConvertTo(); - if (valueConverted) return valueConverted.Result; + if (valueConverted) + { + return valueConverted.Result; + } // cannot cast nor convert the value, nothing we can return but 'default' // note: we don't want to fallback in that case - would make little sense @@ -48,14 +55,23 @@ namespace Umbraco.Web // we don't have a value, try fallback if (PublishedValueFallback.TryGetValue(property, culture, segment, fallback, defaultValue, out var fallbackValue)) + { return fallbackValue; + } // we don't have a value - neither direct nor fallback // give a chance to the converter to return something (eg empty enumerable) var noValue = property.GetValue(culture, segment); - if (noValue is T noValueAsT) return noValueAsT; + if (noValue is T noValueAsT) + { + return noValueAsT; + } + var noValueConverted = noValue.TryConvertTo(); - if (noValueConverted) return noValueConverted.Result; + if (noValueConverted) + { + return noValueConverted.Result; + } // cannot cast noValue nor convert it, nothing we can return but 'default' return default; From 76d206a4badd7b267d861a36043be62cb7ab25cf Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 23 Jun 2021 12:46:50 -0600 Subject: [PATCH 251/289] Fixes back office searching --- src/Umbraco.Web/IPublishedContentQuery.cs | 4 +- .../Models/Mapping/EntityMapDefinition.cs | 2 +- .../Search/IUmbracoTreeSearcherFields.cs | 11 ++- .../Search/IUmbracoTreeSearcherFields2.cs | 17 +++-- src/Umbraco.Web/Search/UmbracoTreeSearcher.cs | 30 ++++++-- .../Search/UmbracoTreeSearcherFields.cs | 75 ++++++++++--------- 6 files changed, 84 insertions(+), 55 deletions(-) diff --git a/src/Umbraco.Web/IPublishedContentQuery.cs b/src/Umbraco.Web/IPublishedContentQuery.cs index f513c1ac02..369fb6ca3d 100644 --- a/src/Umbraco.Web/IPublishedContentQuery.cs +++ b/src/Umbraco.Web/IPublishedContentQuery.cs @@ -8,7 +8,7 @@ using Umbraco.Core.Xml; namespace Umbraco.Web { - using Examine = global::Examine; + // TODO: Merge this into IPublishedContentQuery for v9! public interface IPublishedContentQuery2 : IPublishedContentQuery { /// @@ -32,7 +32,7 @@ namespace Umbraco.Web /// /// While enumerating results, the ambient culture is changed to be the searched culture. /// - IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Umbraco.Core.Constants.UmbracoIndexes.ExternalIndexName, ISet loadedFields = null); + IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName, ISet loadedFields = null); } /// diff --git a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs index 1c4ca6087c..621487ae04 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs @@ -180,7 +180,7 @@ namespace Umbraco.Web.Models.Mapping target.Name = source.Values.ContainsKey("nodeName") ? source.Values["nodeName"] : "[no name]"; - var culture = context.GetCulture(); + var culture = context.GetCulture()?.ToLowerInvariant(); if(culture.IsNullOrWhiteSpace() == false) { target.Name = source.Values.ContainsKey($"nodeName_{culture}") ? source.Values[$"nodeName_{culture}"] : target.Name; diff --git a/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs index c5a6c53d19..b3a4ec7055 100644 --- a/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs +++ b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs @@ -8,19 +8,22 @@ namespace Umbraco.Web.Search public interface IUmbracoTreeSearcherFields { /// - /// Propagate list of searchable fields for all node types + /// The default index fields that are searched on in the back office search for umbraco content entities. /// IEnumerable GetBackOfficeFields(); + /// - /// Propagate list of searchable fields for Members + /// The additional index fields that are searched on in the back office for member entities. /// IEnumerable GetBackOfficeMembersFields(); + /// - /// Propagate list of searchable fields for Media + /// The additional index fields that are searched on in the back office for media entities. /// IEnumerable GetBackOfficeMediaFields(); + /// - /// Propagate list of searchable fields for Documents + /// The additional index fields that are searched on in the back office for document entities. /// IEnumerable GetBackOfficeDocumentFields(); } diff --git a/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields2.cs b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields2.cs index 42f592a965..da0cd26644 100644 --- a/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields2.cs +++ b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields2.cs @@ -1,28 +1,29 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Generic; namespace Umbraco.Web.Search { + // TODO: Merge this interface to IUmbracoTreeSearcherFields for v9. + // We should probably make these method make a little more sense when they are combined so have + // a single method for getting fields to search and fields to load for each category. public interface IUmbracoTreeSearcherFields2 : IUmbracoTreeSearcherFields { /// /// Set of fields for all node types to be loaded /// ISet GetBackOfficeFieldsToLoad(); + /// - /// Set list of fields for Members to be loaded + /// Additional set list of fields for Members to be loaded /// ISet GetBackOfficeMembersFieldsToLoad(); + /// - /// Set of fields for Media to be loaded + /// Additional set of fields for Media to be loaded /// ISet GetBackOfficeMediaFieldsToLoad(); /// - /// Set of fields for Documents to be loaded + /// Additional set of fields for Documents to be loaded /// ISet GetBackOfficeDocumentFieldsToLoad(); } diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 839656ebef..ed2d8d3c86 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -101,7 +101,9 @@ namespace Umbraco.Web.Search string type; var indexName = Constants.UmbracoIndexes.InternalIndexName; var fields = _umbracoTreeSearcherFields.GetBackOfficeFields().ToList(); - ISet fieldsToLoad = null; + ISet fieldsToLoad = _umbracoTreeSearcherFields is IUmbracoTreeSearcherFields2 searcherFields2 + ? new HashSet(searcherFields2.GetBackOfficeFieldsToLoad()) + : null; // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string // manipulation for things like start paths, member types, etc... @@ -124,7 +126,10 @@ namespace Umbraco.Web.Search fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeMembersFields()); if (_umbracoTreeSearcherFields is IUmbracoTreeSearcherFields2 umbracoTreeSearcherFieldMember) { - fieldsToLoad = umbracoTreeSearcherFieldMember.GetBackOfficeMembersFieldsToLoad(); + foreach(var field in umbracoTreeSearcherFieldMember.GetBackOfficeMembersFieldsToLoad()) + { + fieldsToLoad.Add(field); + } } if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && searchFrom.Trim() != "-1") { @@ -138,9 +143,13 @@ namespace Umbraco.Web.Search fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeMediaFields()); if (_umbracoTreeSearcherFields is IUmbracoTreeSearcherFields2 umbracoTreeSearcherFieldsMedia) { - fieldsToLoad = umbracoTreeSearcherFieldsMedia.GetBackOfficeMediaFieldsToLoad(); + foreach (var field in umbracoTreeSearcherFieldsMedia.GetBackOfficeMediaFieldsToLoad()) + { + fieldsToLoad.Add(field); + } } - var allMediaStartNodes = _umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(_entityService _appCaches); + + var allMediaStartNodes = _umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(_entityService, _appCaches); AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; case UmbracoEntityTypes.Document: @@ -148,7 +157,10 @@ namespace Umbraco.Web.Search fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeDocumentFields()); if (_umbracoTreeSearcherFields is IUmbracoTreeSearcherFields2 umbracoTreeSearcherFieldsDocument) { - fieldsToLoad = umbracoTreeSearcherFieldsDocument.GetBackOfficeDocumentFieldsToLoad(); + foreach (var field in umbracoTreeSearcherFieldsDocument.GetBackOfficeDocumentFieldsToLoad()) + { + fieldsToLoad.Add(field); + } } var allContentStartNodes = _umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(_entityService, _appCaches); AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService); @@ -168,7 +180,13 @@ namespace Umbraco.Web.Search return Enumerable.Empty(); } - var result = internalSearcher.CreateQuery().NativeQuery(sb.ToString()).SelectFields(fieldsToLoad) + var examineQuery = internalSearcher.CreateQuery().NativeQuery(sb.ToString()); + if (fieldsToLoad != null) + { + examineQuery.SelectFields(fieldsToLoad); + } + + var result = examineQuery //only return the number of items specified to read up to the amount of records to fill from 0 -> the number of items on the page requested .Execute(Convert.ToInt32(pageSize * (pageIndex + 1))); diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs index 5a2fd91d18..3025f869b4 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs @@ -1,55 +1,62 @@ +using Examine; +using Examine.LuceneEngine.Providers; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Services; using Umbraco.Examine; namespace Umbraco.Web.Search { public class UmbracoTreeSearcherFields : IUmbracoTreeSearcherFields2 { - private IReadOnlyList _backOfficeFields = new List {"id", "__NodeId", "__Key"}; - public IEnumerable GetBackOfficeFields() + private IReadOnlyList _backOfficeFields = new List {"id", LuceneIndex.ItemIdFieldName, UmbracoExamineIndex.NodeKeyFieldName}; + private readonly ISet _backOfficeFieldsToLoad = new HashSet { "id", LuceneIndex.ItemIdFieldName, UmbracoExamineIndex.NodeKeyFieldName, "nodeName", UmbracoExamineIndex.IconFieldName, LuceneIndex.CategoryFieldName, "parentID", LuceneIndex.ItemTypeFieldName }; + private IReadOnlyList _backOfficeMediaFields = new List { UmbracoExamineIndex.UmbracoFileFieldName }; + private readonly ISet _backOfficeMediaFieldsToLoad = new HashSet { UmbracoExamineIndex.UmbracoFileFieldName }; + private IReadOnlyList _backOfficeMembersFields = new List { "email", "loginName" }; + private readonly ISet _backOfficeMembersFieldsToLoad = new HashSet { "email", "loginName" }; + private readonly ISet _backOfficeDocumentFieldsToLoad = new HashSet { UmbracoContentIndex.VariesByCultureFieldName }; + private readonly ILocalizationService _localizationService; + + public UmbracoTreeSearcherFields(ILocalizationService localizationService) { - return _backOfficeFields; + _localizationService = localizationService; } + /// + public IEnumerable GetBackOfficeFields() => _backOfficeFields; - private IReadOnlyList _backOfficeMembersFields = new List {"email", "loginName"}; - public IEnumerable GetBackOfficeMembersFields() - { - return _backOfficeMembersFields; - } - private IReadOnlyList _backOfficeMediaFields = new List {UmbracoExamineIndex.UmbracoFileFieldName }; - public IEnumerable GetBackOfficeMediaFields() - { - return _backOfficeMediaFields; - } - public IEnumerable GetBackOfficeDocumentFields() - { - return Enumerable.Empty(); - } + /// + public IEnumerable GetBackOfficeMembersFields() => _backOfficeMembersFields; - private readonly ISet _backOfficeFieldsToLoad = new HashSet { "id", "__NodeId", "__Key" }; - public ISet GetBackOfficeFieldsToLoad() - { - return _backOfficeFieldsToLoad; - } + /// + public IEnumerable GetBackOfficeMediaFields() => _backOfficeMediaFields; - private readonly ISet _backOfficeMembersFieldsToLoad = new HashSet { "id", "__NodeId", "__Key", "email", "loginName" }; - public ISet GetBackOfficeMembersFieldsToLoad() - { - return _backOfficeMembersFieldsToLoad; - } + /// + public IEnumerable GetBackOfficeDocumentFields() => Enumerable.Empty(); - private readonly ISet _backOfficeMediaFieldsToLoad = new HashSet { "id", "__NodeId", "__Key", UmbracoExamineIndex.UmbracoFileFieldName }; - public ISet GetBackOfficeMediaFieldsToLoad() - { - return _backOfficeMediaFieldsToLoad; - } - private readonly ISet _backOfficeDocumentFieldsToLoad = new HashSet { "id", "__NodeId", "__Key" }; + /// + public ISet GetBackOfficeFieldsToLoad() => _backOfficeFieldsToLoad; + /// + public ISet GetBackOfficeMembersFieldsToLoad() => _backOfficeMembersFieldsToLoad; + + /// + public ISet GetBackOfficeMediaFieldsToLoad() => _backOfficeMediaFieldsToLoad; + + /// public ISet GetBackOfficeDocumentFieldsToLoad() { - return _backOfficeDocumentFieldsToLoad; + var fields = _backOfficeDocumentFieldsToLoad; + + // We need to load all nodeName_* fields but we won't know those up front so need to get + // all langs (this is cached) + foreach(var field in _localizationService.GetAllLanguages().Select(x => "nodeName_" + x.IsoCode.ToLowerInvariant())) + { + fields.Add(field); + } + + return fields; } } } From 30d57125ce44b25b9d143b829190ecf9c932f79b Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 23 Jun 2021 13:14:22 -0600 Subject: [PATCH 252/289] Adds notes, obsoletes old localize methods --- .../Services/ILocalizedTextService.cs | 7 +++++- .../Implement/LocalizedTextService.cs | 15 +++++------ .../LocalizedTextServiceExtensions.cs | 25 +++++++++++++++++-- .../Editors/BackOfficeController.cs | 2 +- src/Umbraco.Web/Search/UmbracoTreeSearcher.cs | 2 ++ 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Core/Services/ILocalizedTextService.cs b/src/Umbraco.Core/Services/ILocalizedTextService.cs index 73212546a5..c74aa209c6 100644 --- a/src/Umbraco.Core/Services/ILocalizedTextService.cs +++ b/src/Umbraco.Core/Services/ILocalizedTextService.cs @@ -1,9 +1,13 @@ -using System.Collections; +using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; namespace Umbraco.Core.Services { + // TODO: This needs to be merged into one interface in v9, but better yet + // the Localize method should just the based on area + alias and we should remove + // the one with the 'key' (the concatenated area/alias) to ensure that we never use that again. public interface ILocalizedTextService2 : ILocalizedTextService { @@ -41,6 +45,7 @@ namespace Umbraco.Core.Services /// /// This can be null /// + [Obsolete("Use LocalizedTextServiceExtensions.Localize or ILocalizedTextService2.Localize instead")] string Localize(string key, CultureInfo culture, IDictionary tokens = null); /// diff --git a/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs b/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs index 8acb75645b..4f1766d6f4 100644 --- a/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs +++ b/src/Umbraco.Core/Services/Implement/LocalizedTextService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -9,8 +8,6 @@ using Umbraco.Core.Logging; namespace Umbraco.Core.Services.Implement { - // TODO: Convert all of this over to Niels K's localization framework one day - public class LocalizedTextService : ILocalizedTextService2 { private readonly ILogger _logger; @@ -27,9 +24,9 @@ namespace Umbraco.Core.Services.Implement /// public LocalizedTextService(Lazy fileSources, ILogger logger) { - if (logger == null) throw new ArgumentNullException("logger"); + if (logger == null) throw new ArgumentNullException(nameof(logger)); _logger = logger; - if (fileSources == null) throw new ArgumentNullException("fileSources"); + if (fileSources == null) throw new ArgumentNullException(nameof(fileSources)); _dictionarySourceLazy = new Lazy>>>(() => FileSourcesToAreaDictionarySources(fileSources.Value)); _noAreaDictionarySourceLazy = new Lazy>>(() => FileSourcesToNoAreaDictionarySources(fileSources.Value)); _fileSources = fileSources; @@ -78,9 +75,9 @@ namespace Umbraco.Core.Services.Implement /// public LocalizedTextService(IDictionary> source, ILogger logger) { - if (source == null) throw new ArgumentNullException("source"); - if (logger == null) throw new ArgumentNullException("logger"); - _logger = logger; + if (source == null) throw new ArgumentNullException(nameof(source)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _dictionarySourceLazy = new Lazy>>>(() => XmlSourcesToAreaDictionary(source)); _noAreaDictionarySourceLazy = new Lazy>>(() => XmlSourceToNoAreaDictionary(source)); @@ -150,7 +147,7 @@ namespace Umbraco.Core.Services.Implement /// public IDictionary GetAllStoredValues(CultureInfo culture) { - if (culture == null) throw new ArgumentNullException("culture"); + if (culture == null) throw new ArgumentNullException(nameof(culture)); // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode culture = ConvertToSupportedCultureWithRegionCode(culture); diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs index 176a1ae240..2911441578 100644 --- a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs +++ b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; @@ -12,6 +13,8 @@ namespace Umbraco.Core.Services /// public static class LocalizedTextServiceExtensions { + // TODO: Remove these extension methods checking for ILocalizedTextService2 in v9 when these interfaces merge + public static string Localize(this ILocalizedTextService manager, string area, string alias, CultureInfo culture) { if(manager is ILocalizedTextService2 manager2) @@ -23,8 +26,11 @@ namespace Umbraco.Core.Services { fullKey = string.Concat(area, "/", alias); } +#pragma warning disable CS0618 // Type or member is obsolete return manager.Localize(fullKey, culture); +#pragma warning restore CS0618 // Type or member is obsolete } + public static string Localize(this ILocalizedTextService manager, string area, string alias) { if (manager is ILocalizedTextService2 manager2) @@ -36,8 +42,11 @@ namespace Umbraco.Core.Services { fullKey = string.Concat(area, "/", alias); } +#pragma warning disable CS0618 // Type or member is obsolete return manager.Localize(fullKey, Thread.CurrentThread.CurrentUICulture); +#pragma warning restore CS0618 // Type or member is obsolete } + /// /// Localize using the current thread culture /// @@ -57,8 +66,11 @@ namespace Umbraco.Core.Services { fullKey = string.Concat(area, "/", alias); } +#pragma warning disable CS0618 // Type or member is obsolete return manager.Localize(fullKey, Thread.CurrentThread.CurrentUICulture, tokens); +#pragma warning restore CS0618 // Type or member is obsolete } + /// /// Localize using the current thread culture /// @@ -78,10 +90,11 @@ namespace Umbraco.Core.Services { fullKey = string.Concat(area, "/", alias); } +#pragma warning disable CS0618 // Type or member is obsolete return manager.Localize(fullKey, Thread.CurrentThread.CurrentUICulture, tokens); +#pragma warning restore CS0618 // Type or member is obsolete } - /// /// Localize a key without any variables /// @@ -102,7 +115,9 @@ namespace Umbraco.Core.Services { fullKey = string.Concat(area, "/", alias); } +#pragma warning disable CS0618 // Type or member is obsolete return manager.Localize(fullKey, culture, ConvertToDictionaryVars(tokens)); +#pragma warning restore CS0618 // Type or member is obsolete } /// @@ -125,8 +140,11 @@ namespace Umbraco.Core.Services { fullKey = string.Concat(area, "/", alias); } +#pragma warning disable CS0618 // Type or member is obsolete return manager.Localize(fullKey, culture, tokens); +#pragma warning restore CS0618 // Type or member is obsolete } + /// /// Localize using the current thread culture /// @@ -134,6 +152,7 @@ namespace Umbraco.Core.Services /// /// /// + [Obsolete("Use the overload specifying an area and alias instead of key")] public static string Localize(this ILocalizedTextService manager, string key, string[] tokens) { return manager.Localize(key, Thread.CurrentThread.CurrentUICulture, tokens); @@ -146,6 +165,7 @@ namespace Umbraco.Core.Services /// /// /// + [Obsolete("Use the overload specifying an area and alias instead of key")] public static string Localize(this ILocalizedTextService manager, string key, IDictionary tokens = null) { return manager.Localize(key, Thread.CurrentThread.CurrentUICulture, tokens); @@ -159,6 +179,7 @@ namespace Umbraco.Core.Services /// /// /// + [Obsolete("Use the overload specifying an area and alias instead of key")] public static string Localize(this ILocalizedTextService manager, string key, CultureInfo culture, string[] tokens) { return manager.Localize(key, culture, ConvertToDictionaryVars(tokens)); diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index baba9da894..a242e72c32 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -178,7 +178,7 @@ namespace Umbraco.Web.Editors : CultureInfo.GetCultureInfo(GlobalSettings.DefaultUILanguage) : CultureInfo.GetCultureInfo(culture); - + // TODO: Remove this check in v9 when these interfaces merge if(Services.TextService is ILocalizedTextService2 localizedText2) { var nestedDictionary2 = localizedText2.GetAllStoredValuesByAreaAndAlias(cultureInfo); diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index ed2d8d3c86..c4560b20c3 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -101,6 +101,8 @@ namespace Umbraco.Web.Search string type; var indexName = Constants.UmbracoIndexes.InternalIndexName; var fields = _umbracoTreeSearcherFields.GetBackOfficeFields().ToList(); + + // TODO: Remove these checks in v9 when these interfaces merge ISet fieldsToLoad = _umbracoTreeSearcherFields is IUmbracoTreeSearcherFields2 searcherFields2 ? new HashSet(searcherFields2.GetBackOfficeFieldsToLoad()) : null; From 44c814a550f1060e4255895a5e9515cbf153b6b8 Mon Sep 17 00:00:00 2001 From: Chad Date: Mon, 28 Jun 2021 12:20:38 +1200 Subject: [PATCH 253/289] Fix Rollback feature not creating diffs when properties are moved between tabs in EditorModel events (#10376) * Find the old property by alias instead of by indexes as if you move around properties in the EditorModel events Rollback breaks. * optimise property lookup - avoids iterating entire property set Co-authored-by: Nathan Woulfe --- .../rollback/rollback.controller.js | 73 +++++++++++-------- 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js index 0d49c7dd9c..4b0dfcb8b4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js @@ -112,7 +112,6 @@ * This will load in a new version */ function createDiff(currentVersion, previousVersion) { - vm.diff = {}; vm.diff.properties = []; @@ -120,41 +119,55 @@ vm.diff.name = JsDiff.diffWords(currentVersion.name, previousVersion.name); // extract all properties from the tabs and create new object for the diff - currentVersion.tabs.forEach((tab, tabIndex) => { - tab.properties.forEach((property, propertyIndex) => { - var oldProperty = previousVersion.tabs[tabIndex].properties[propertyIndex]; + currentVersion.tabs.forEach(function (tab) { + tab.properties.forEach(function (property) { + let oldTabIndex = -1; + let oldTabPropertyIndex = -1; + const previousVersionTabs = previousVersion.tabs; - // copy existing properties, so it doesn't manipulate existing properties on page - oldProperty = Utilities.copy(oldProperty); - property = Utilities.copy(property); - - // we have to make properties storing values as object into strings (Grid, nested content, etc.) - if(property.value instanceof Object) { - property.value = JSON.stringify(property.value, null, 1); - property.isObject = true; + // find the property by alias, but only search until we find it + for (var oti = 0, length = previousVersionTabs.length; oti < length; oti++) { + const opi = previousVersionTabs[oti].properties.findIndex(p => p.alias === property.alias); + if (opi !== -1) { + oldTabIndex = oti; + oldTabPropertyIndex = opi; + break; + } } - if(oldProperty.value instanceof Object) { - oldProperty.value = JSON.stringify(oldProperty.value, null, 1); - oldProperty.isObject = true; + if (oldTabIndex !== -1 && oldTabPropertyIndex !== -1) { + let oldProperty = previousVersion.tabs[oldTabIndex].properties[oldTabPropertyIndex]; + + // copy existing properties, so it doesn't manipulate existing properties on page + oldProperty = Utilities.copy(oldProperty); + property = Utilities.copy(property); + + // we have to make properties storing values as object into strings (Grid, nested content, etc.) + if (property.value instanceof Object) { + property.value = JSON.stringify(property.value, null, 1); + property.isObject = true; + } + + if (oldProperty.value instanceof Object) { + oldProperty.value = JSON.stringify(oldProperty.value, null, 1); + oldProperty.isObject = true; + } + + // diff requires a string + property.value = property.value ? property.value + '' : ''; + oldProperty.value = oldProperty.value ? oldProperty.value + '' : ''; + + const diffProperty = { + 'alias': property.alias, + 'label': property.label, + 'diff': property.isObject ? JsDiff.diffJson(property.value, oldProperty.value) : JsDiff.diffWords(property.value, oldProperty.value), + 'isObject': property.isObject || oldProperty.isObject + }; + + vm.diff.properties.push(diffProperty); } - - // diff requires a string - property.value = property.value ? property.value + "" : ""; - oldProperty.value = oldProperty.value ? oldProperty.value + "" : ""; - - var diffProperty = { - "alias": property.alias, - "label": property.label, - "diff": (property.isObject) ? JsDiff.diffJson(property.value, oldProperty.value) : JsDiff.diffWords(property.value, oldProperty.value), - "isObject": (property.isObject || oldProperty.isObject) ? true : false - }; - - vm.diff.properties.push(diffProperty); - }); }); - } function rollback() { From 5b9cd1bd87ad2c2440c6227e8aea7994e6c9b192 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 28 Jun 2021 09:28:32 +0200 Subject: [PATCH 254/289] Improvements to media pickers/crop handling and URL generation (#10529) --- src/Umbraco.Core/Models/MediaWithCrops.cs | 79 ++++++++++- .../ImageCropperConfiguration.cs | 35 +++++ .../ValueConverters/ImageCropperValue.cs | 53 +++----- .../PropertyEditors/ImageCropperTest.cs | 14 +- .../ImageCropperTemplateCoreExtensions.cs | 126 ++++++++++++++---- .../ImageCropperTemplateExtensions.cs | 45 ++++++- .../MediaPicker3Configuration.cs | 40 ++++++ .../BlockListPropertyValueConverter.cs | 1 + .../MediaPickerWithCropsValueConverter.cs | 23 ++-- src/Umbraco.Web/UrlHelperRenderExtensions.cs | 111 +++++++++------ 10 files changed, 407 insertions(+), 120 deletions(-) diff --git a/src/Umbraco.Core/Models/MediaWithCrops.cs b/src/Umbraco.Core/Models/MediaWithCrops.cs index ef3205bd94..fefb4e6b80 100644 --- a/src/Umbraco.Core/Models/MediaWithCrops.cs +++ b/src/Umbraco.Core/Models/MediaWithCrops.cs @@ -1,15 +1,86 @@ +using System; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors.ValueConverters; namespace Umbraco.Core.Models { /// - /// Model used in Razor Views for rendering + /// Represents a media item with local crops. /// - public class MediaWithCrops + /// + public class MediaWithCrops : PublishedContentWrapped { - public IPublishedContent MediaItem { get; set; } + /// + /// Gets the media item. + /// + /// + /// The media item. + /// + [Obsolete("This instance now implements IPublishedContent by wrapping the media item, use the extension methods directly on MediaWithCrops or use the Content property to get the media item instead.")] + public IPublishedContent MediaItem => Content; - public ImageCropperValue LocalCrops { get; set; } + /// + /// Gets the content/media item. + /// + /// + /// The content/media item. + /// + public IPublishedContent Content => Unwrap(); + + /// + /// Gets the local crops. + /// + /// + /// The local crops. + /// + public ImageCropperValue LocalCrops { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The content. + /// The local crops. + public MediaWithCrops(IPublishedContent content, ImageCropperValue localCrops) + : base(content) + { + LocalCrops = localCrops; + } + } + + /// + /// Represents a media item with local crops. + /// + /// The type of the media item. + /// + public class MediaWithCrops : MediaWithCrops + where T : IPublishedContent + { + /// + /// Gets the media item. + /// + /// + /// The media item. + /// + public new T Content { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The content. + /// The local crops. + public MediaWithCrops(T content, ImageCropperValue localCrops) + : base(content, localCrops) + { + Content = content; + } + + /// + /// Performs an implicit conversion from to . + /// + /// The media with crops. + /// + /// The result of the conversion. + /// + public static implicit operator T(MediaWithCrops mediaWithCrops) => mediaWithCrops.Content; } } diff --git a/src/Umbraco.Core/PropertyEditors/ImageCropperConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ImageCropperConfiguration.cs index 2ce6e2ec04..855ec76a5a 100644 --- a/src/Umbraco.Core/PropertyEditors/ImageCropperConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/ImageCropperConfiguration.cs @@ -1,4 +1,8 @@ using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.PropertyEditors.ValueConverters; +using static Umbraco.Core.PropertyEditors.ValueConverters.ImageCropperValue; namespace Umbraco.Core.PropertyEditors { @@ -22,4 +26,35 @@ namespace Umbraco.Core.PropertyEditors public int Height { get; set; } } } + + internal static class ImageCropperConfigurationExtensions + { + /// + /// Applies the configuration to ensure only valid crops are kept and have the correct width/height. + /// + /// The configuration. + public static void ApplyConfiguration(this ImageCropperValue imageCropperValue, ImageCropperConfiguration configuration) + { + var crops = new List(); + + var configuredCrops = configuration?.Crops; + if (configuredCrops != null) + { + foreach (var configuredCrop in configuredCrops) + { + var crop = imageCropperValue.Crops?.FirstOrDefault(x => x.Alias == configuredCrop.Alias); + + crops.Add(new ImageCropperCrop + { + Alias = configuredCrop.Alias, + Width = configuredCrop.Width, + Height = configuredCrop.Height, + Coordinates = crop?.Coordinates + }); + } + } + + imageCropperValue.Crops = crops; + } + } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs index 2c6ec9b8aa..f2151778d9 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs @@ -140,7 +140,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters /// Determines whether the value has a specified crop. /// public bool HasCrop(string alias) - => Crops.Any(x => x.Alias == alias); + => Crops != null && Crops.Any(x => x.Alias == alias); /// /// Determines whether the value has a source image. @@ -148,46 +148,35 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters public bool HasImage() => !string.IsNullOrWhiteSpace(Src); - /// - /// Applies a configuration. - /// - /// Ensures that all crops defined in the configuration exists in the value. - internal void ApplyConfiguration(ImageCropperConfiguration configuration) + internal ImageCropperValue Merge(ImageCropperValue imageCropperValue) { - // merge the crop values - the alias + width + height comes from - // configuration, but each crop can store its own coordinates - - var configuredCrops = configuration?.Crops; - if (configuredCrops == null) return; - - //Use Crops if it's not null, otherwise create a new list var crops = Crops?.ToList() ?? new List(); - foreach (var configuredCrop in configuredCrops) + var incomingCrops = imageCropperValue?.Crops; + if (incomingCrops != null) { - var crop = crops.FirstOrDefault(x => x.Alias == configuredCrop.Alias); - if (crop != null) + foreach (var incomingCrop in incomingCrops) { - // found, apply the height & width - crop.Width = configuredCrop.Width; - crop.Height = configuredCrop.Height; - } - else - { - // not found, add - crops.Add(new ImageCropperCrop + var crop = crops.FirstOrDefault(x => x.Alias == incomingCrop.Alias); + if (crop == null) { - Alias = configuredCrop.Alias, - Width = configuredCrop.Width, - Height = configuredCrop.Height - }); + // Add incoming crop + crops.Add(incomingCrop); + } + else if (crop.Coordinates == null) + { + // Use incoming crop coordinates + crop.Coordinates = incomingCrop.Coordinates; + } } } - // assume we don't have to remove the crops in value, that - // are not part of configuration anymore? - - Crops = crops; + return new ImageCropperValue() + { + Src = !string.IsNullOrWhiteSpace(Src) ? Src : imageCropperValue?.Src, + Crops = crops, + FocalPoint = FocalPoint ?? imageCropperValue?.FocalPoint + }; } #region IEquatable diff --git a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs index c5c2b4e61f..c40708770e 100644 --- a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs +++ b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs @@ -82,8 +82,20 @@ namespace Umbraco.Tests.PropertyEditors var mediaFileSystem = new MediaFileSystem(Mock.Of(), config, scheme, logger); + var imageCropperConfiguration = new ImageCropperConfiguration() + { + Crops = new[] + { + new ImageCropperConfiguration.Crop() + { + Alias = "thumb", + Width = 100, + Height = 100 + } + } + }; var dataTypeService = new TestObjects.TestDataTypeService( - new DataType(new ImageCropperPropertyEditor(Mock.Of(), mediaFileSystem, Mock.Of(), Mock.Of())) { Id = 1 }); + new DataType(new ImageCropperPropertyEditor(Mock.Of(), mediaFileSystem, Mock.Of(), Mock.Of())) { Id = 1, Configuration = imageCropperConfiguration }); var factory = new PublishedContentTypeFactory(Mock.Of(), new PropertyValueConverterCollection(Array.Empty()), dataTypeService); diff --git a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs index 766cb1e99f..8773f1bb39 100644 --- a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs @@ -28,9 +28,30 @@ namespace Umbraco.Web return mediaItem.GetCropUrl(imageUrlGenerator, cropAlias: cropAlias, useCropDimensions: true); } + public static string GetCropUrl(this MediaWithCrops mediaWithCrops, string cropAlias, IImageUrlGenerator imageUrlGenerator) + { + return mediaWithCrops.GetCropUrl(imageUrlGenerator, cropAlias: cropAlias, useCropDimensions: true); + } + + [Obsolete("Use the GetCropUrl overload with the updated parameter order and note this implementation has changed to get the URL from the media item.")] public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias, IImageUrlGenerator imageUrlGenerator, ImageCropperValue imageCropperValue) { - return mediaItem.Url().GetCropUrl(imageUrlGenerator, imageCropperValue, cropAlias: cropAlias, useCropDimensions: true); + return mediaItem.GetCropUrl(imageCropperValue, cropAlias, imageUrlGenerator); + } + + /// + /// Gets the crop URL by using only the specified . + /// + /// The media item. + /// The image cropper value. + /// The crop alias. + /// The image URL generator. + /// + /// The image crop URL. + /// + public static string GetCropUrl(this IPublishedContent mediaItem, ImageCropperValue imageCropperValue, string cropAlias, IImageUrlGenerator imageUrlGenerator) + { + return mediaItem.GetCropUrl(imageUrlGenerator, imageCropperValue, true, cropAlias: cropAlias, useCropDimensions: true); } /// @@ -53,6 +74,11 @@ namespace Umbraco.Web return mediaItem.GetCropUrl(imageUrlGenerator, propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true); } + public static string GetCropUrl(this MediaWithCrops mediaWithCrops, string propertyAlias, string cropAlias, IImageUrlGenerator imageUrlGenerator) + { + return mediaWithCrops.GetCropUrl(imageUrlGenerator, propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true); + } + /// /// Gets the ImageProcessor URL from the IPublishedContent item. /// @@ -123,7 +149,51 @@ namespace Umbraco.Web ImageCropRatioMode? ratioMode = null, bool upScale = true) { - if (mediaItem == null) throw new ArgumentNullException("mediaItem"); + return mediaItem.GetCropUrl(imageUrlGenerator, null, false, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale); + } + + public static string GetCropUrl( + this MediaWithCrops mediaWithCrops, + IImageUrlGenerator imageUrlGenerator, + int? width = null, + int? height = null, + string propertyAlias = Constants.Conventions.Media.File, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + bool cacheBuster = true, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true) + { + if (mediaWithCrops == null) throw new ArgumentNullException(nameof(mediaWithCrops)); + + return mediaWithCrops.Content.GetCropUrl(imageUrlGenerator, mediaWithCrops.LocalCrops, false, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale); + } + + private static string GetCropUrl( + this IPublishedContent mediaItem, + IImageUrlGenerator imageUrlGenerator, + ImageCropperValue localCrops, + bool localCropsOnly, + int? width = null, + int? height = null, + string propertyAlias = Constants.Conventions.Media.File, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + bool cacheBuster = true, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true) + { + if (mediaItem == null) throw new ArgumentNullException(nameof(mediaItem)); var cacheBusterValue = cacheBuster ? mediaItem.UpdateDate.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture) : null; @@ -132,31 +202,38 @@ namespace Umbraco.Web var mediaItemUrl = mediaItem.MediaUrl(propertyAlias: propertyAlias); - //get the default obj from the value converter - var cropperValue = mediaItem.Value(propertyAlias); - - //is it strongly typed? - var stronglyTyped = cropperValue as ImageCropperValue; - if (stronglyTyped != null) + // Only get crops from media when required and used + if (localCropsOnly == false && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) { - return GetCropUrl( - mediaItemUrl, imageUrlGenerator, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, - cacheBusterValue, furtherOptions, ratioMode, upScale); + // Get the default cropper value from the value converter + var cropperValue = mediaItem.Value(propertyAlias); + + var mediaCrops = cropperValue as ImageCropperValue; + + if (mediaCrops == null && cropperValue is JObject jobj) + { + mediaCrops = jobj.ToObject(); + } + + if (mediaCrops == null && cropperValue is string imageCropperValue && + string.IsNullOrEmpty(imageCropperValue) == false && imageCropperValue.DetectIsJson()) + { + mediaCrops = imageCropperValue.DeserializeImageCropperValue(); + } + + // Merge crops + if (localCrops == null) + { + localCrops = mediaCrops; + } + else if (mediaCrops != null) + { + localCrops = localCrops.Merge(mediaCrops); + } } - //this shouldn't be the case but we'll check - var jobj = cropperValue as JObject; - if (jobj != null) - { - stronglyTyped = jobj.ToObject(); - return GetCropUrl( - mediaItemUrl, imageUrlGenerator, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, - cacheBusterValue, furtherOptions, ratioMode, upScale); - } - - //it's a single string return GetCropUrl( - mediaItemUrl, imageUrlGenerator, width, height, mediaItemUrl, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, + mediaItemUrl, imageUrlGenerator, localCrops, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); } @@ -237,6 +314,7 @@ namespace Umbraco.Web { cropDataSet = imageCropperValue.DeserializeImageCropperValue(); } + return GetCropUrl( imageUrl, imageUrlGenerator, cropDataSet, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); @@ -381,10 +459,10 @@ namespace Umbraco.Web return imageUrlGenerator.GetImageUrl(options); } + [Obsolete("Use GetCrop to merge local and media crops, get automatic cache buster value and have more parameters.")] public static string GetLocalCropUrl(this MediaWithCrops mediaWithCrops, string alias, IImageUrlGenerator imageUrlGenerator, string cacheBusterValue) { return mediaWithCrops.LocalCrops.Src + mediaWithCrops.LocalCrops.GetCropUrl(alias, imageUrlGenerator, cacheBusterValue: cacheBusterValue); - } } } diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs index 51845946f1..d9218a8974 100644 --- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -30,7 +30,21 @@ namespace Umbraco.Web /// public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, cropAlias, Current.ImageUrlGenerator); - public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias, ImageCropperValue imageCropperValue) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, cropAlias, Current.ImageUrlGenerator, imageCropperValue); + public static string GetCropUrl(this MediaWithCrops mediaWithCrops, string cropAlias) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaWithCrops, cropAlias, Current.ImageUrlGenerator); + + [Obsolete("Use the GetCropUrl overload with the updated parameter order and note this implementation has changed to get the URL from the media item.")] + public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias, ImageCropperValue imageCropperValue) => mediaItem.GetCropUrl(imageCropperValue, cropAlias); + + /// + /// Gets the crop URL by using only the specified . + /// + /// The media item. + /// The image cropper value. + /// The crop alias. + /// + /// The image crop URL. + /// + public static string GetCropUrl(this IPublishedContent mediaItem, ImageCropperValue imageCropperValue, string cropAlias) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, imageCropperValue, cropAlias, Current.ImageUrlGenerator); /// /// Gets the ImageProcessor URL by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. @@ -49,6 +63,8 @@ namespace Umbraco.Web /// public static string GetCropUrl(this IPublishedContent mediaItem, string propertyAlias, string cropAlias) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, propertyAlias, cropAlias, Current.ImageUrlGenerator); + public static string GetCropUrl(this MediaWithCrops mediaWithCrops, string propertyAlias, string cropAlias) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaWithCrops, propertyAlias, cropAlias, Current.ImageUrlGenerator); + /// /// Gets the ImageProcessor URL from the IPublishedContent item. /// @@ -118,12 +134,21 @@ namespace Umbraco.Web ImageCropRatioMode? ratioMode = null, bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, Current.ImageUrlGenerator, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale); - public static string GetLocalCropUrl(this MediaWithCrops mediaWithCrops, - string alias, - string cacheBusterValue = null) - => ImageCropperTemplateCoreExtensions.GetLocalCropUrl(mediaWithCrops, alias, Current.ImageUrlGenerator, cacheBusterValue); - - + public static string GetCropUrl( + this MediaWithCrops mediaWithCrops, + int? width = null, + int? height = null, + string propertyAlias = Constants.Conventions.Media.File, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + bool cacheBuster = true, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaWithCrops, Current.ImageUrlGenerator, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale); /// /// Gets the ImageProcessor URL from the image path. @@ -261,6 +286,12 @@ namespace Umbraco.Web ImageCropRatioMode? ratioMode = null, bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(imageUrl, Current.ImageUrlGenerator, cropDataSet, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); + [Obsolete("Use GetCrop to merge local and media crops, get automatic cache buster value and have more parameters.")] + public static string GetLocalCropUrl(this MediaWithCrops mediaWithCrops, + string alias, + string cacheBusterValue = null) + => ImageCropperTemplateCoreExtensions.GetLocalCropUrl(mediaWithCrops, alias, Current.ImageUrlGenerator, cacheBusterValue); + private static readonly JsonSerializerSettings ImageCropperValueJsonSerializerSettings = new JsonSerializerSettings { Culture = CultureInfo.InvariantCulture, diff --git a/src/Umbraco.Web/PropertyEditors/MediaPicker3Configuration.cs b/src/Umbraco.Web/PropertyEditors/MediaPicker3Configuration.cs index 4c3c6564a5..1a6a1cde0b 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPicker3Configuration.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPicker3Configuration.cs @@ -1,6 +1,10 @@ using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; using Umbraco.Core; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; +using static Umbraco.Core.PropertyEditors.ValueConverters.ImageCropperValue; namespace Umbraco.Web.PropertyEditors { @@ -57,4 +61,40 @@ namespace Umbraco.Web.PropertyEditors public int Height { get; set; } } } + + internal static class MediaPicker3ConfigurationExtensions + { + /// + /// Applies the configuration to ensure only valid crops are kept and have the correct width/height. + /// + /// The configuration. + public static void ApplyConfiguration(this ImageCropperValue imageCropperValue, MediaPicker3Configuration configuration) + { + var crops = new List(); + + var configuredCrops = configuration?.Crops; + if (configuredCrops != null) + { + foreach (var configuredCrop in configuredCrops) + { + var crop = imageCropperValue.Crops?.FirstOrDefault(x => x.Alias == configuredCrop.Alias); + + crops.Add(new ImageCropperCrop + { + Alias = configuredCrop.Alias, + Width = configuredCrop.Width, + Height = configuredCrop.Height, + Coordinates = crop?.Coordinates + }); + } + } + + imageCropperValue.Crops = crops; + + if (configuration?.EnableLocalFocalPoint == false) + { + imageCropperValue.FocalPoint = null; + } + } + } } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index f46c118174..5d216f2b4c 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -120,6 +120,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters settingsData = null; } + // TODO: This should be optimized/cached, as calling Activator.CreateInstance is slow var layoutType = typeof(BlockListItem<,>).MakeGenericType(contentData.GetType(), settingsData?.GetType() ?? typeof(IPublishedElement)); var layoutRef = (BlockListItem)Activator.CreateInstance(layoutType, contentGuidUdi, contentData, settingGuidUdi, settingsData); diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs index f2f055d698..17907e8546 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs @@ -51,22 +51,27 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var mediaItems = new List(); var dtos = MediaPicker3PropertyEditor.MediaPicker3PropertyValueEditor.Deserialize(inter); + var configuration = propertyType.DataType.ConfigurationAs(); foreach (var dto in dtos) { var mediaItem = _publishedSnapshotAccessor.PublishedSnapshot.Media.GetById(preview, dto.MediaKey); if (mediaItem != null) { - mediaItems.Add(new MediaWithCrops + var localCrops = new ImageCropperValue { - MediaItem = mediaItem, - LocalCrops = new ImageCropperValue - { - Crops = dto.Crops, - FocalPoint = dto.FocalPoint, - Src = mediaItem.Url() - } - }); + Crops = dto.Crops, + FocalPoint = dto.FocalPoint, + Src = mediaItem.Url() + }; + + localCrops.ApplyConfiguration(configuration); + + // TODO: This should be optimized/cached, as calling Activator.CreateInstance is slow + var mediaWithCropsType = typeof(MediaWithCrops<>).MakeGenericType(mediaItem.GetType()); + var mediaWithCrops = (MediaWithCrops)Activator.CreateInstance(mediaWithCropsType, mediaItem, localCrops); + + mediaItems.Add(mediaWithCrops); if (!isMultiple) { diff --git a/src/Umbraco.Web/UrlHelperRenderExtensions.cs b/src/Umbraco.Web/UrlHelperRenderExtensions.cs index 592c88945b..2c547c841e 100644 --- a/src/Umbraco.Web/UrlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/UrlHelperRenderExtensions.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Web; using System.Web.Mvc; using Umbraco.Core; +using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Web.Composing; @@ -17,9 +18,10 @@ namespace Umbraco.Web /// public static class UrlHelperRenderExtensions { - private static readonly IHtmlString EmptyHtmlString = new HtmlString(string.Empty); + private static IHtmlString CreateHtmlString(string value, bool htmlEncode) => htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(value)) : new HtmlString(value); + #region GetCropUrl /// @@ -42,7 +44,17 @@ namespace Umbraco.Web if (mediaItem == null) return EmptyHtmlString; var url = mediaItem.GetCropUrl(cropAlias: cropAlias, useCropDimensions: true); - return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); + + return CreateHtmlString(url, htmlEncode); + } + + public static IHtmlString GetCropUrl(this UrlHelper urlHelper, MediaWithCrops mediaWithCrops, string cropAlias, bool htmlEncode = true) + { + if (mediaWithCrops == null) return EmptyHtmlString; + + var url = mediaWithCrops.GetCropUrl(cropAlias: cropAlias, useCropDimensions: true); + + return CreateHtmlString(url, htmlEncode); } /// @@ -70,7 +82,17 @@ namespace Umbraco.Web if (mediaItem == null) return EmptyHtmlString; var url = mediaItem.GetCropUrl(propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true); - return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); + + return CreateHtmlString(url, htmlEncode); + } + + public static IHtmlString GetCropUrl(this UrlHelper urlHelper, MediaWithCrops mediaWithCrops, string propertyAlias, string cropAlias, bool htmlEncode = true) + { + if (mediaWithCrops == null) return EmptyHtmlString; + + var url = mediaWithCrops.GetCropUrl(propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true); + + return CreateHtmlString(url, htmlEncode); } /// @@ -150,10 +172,33 @@ namespace Umbraco.Web { if (mediaItem == null) return EmptyHtmlString; - var url = mediaItem.GetCropUrl(width, height, propertyAlias, cropAlias, quality, imageCropMode, - imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, - upScale); - return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); + var url = mediaItem.GetCropUrl(width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale); + + return CreateHtmlString(url, htmlEncode); + } + + public static IHtmlString GetCropUrl(this UrlHelper urlHelper, + MediaWithCrops mediaWithCrops, + int? width = null, + int? height = null, + string propertyAlias = Umbraco.Core.Constants.Conventions.Media.File, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + bool cacheBuster = true, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true, + bool htmlEncode = true) + { + if (mediaWithCrops == null) return EmptyHtmlString; + + var url = mediaWithCrops.GetCropUrl(width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale); + + return CreateHtmlString(url, htmlEncode); } /// @@ -231,10 +276,18 @@ namespace Umbraco.Web bool upScale = true, bool htmlEncode = true) { - var url = imageUrl.GetCropUrl(width, height, imageCropperValue, cropAlias, quality, imageCropMode, - imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, - upScale); - return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); + var url = imageUrl.GetCropUrl(width, height, imageCropperValue, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); + + return CreateHtmlString(url, htmlEncode); + } + + public static IHtmlString GetCropUrl(this UrlHelper urlHelper, ImageCropperValue imageCropperValue, string cropAlias, bool htmlEncode = true) + { + if (imageCropperValue == null || string.IsNullOrEmpty(imageCropperValue.Src)) return EmptyHtmlString; + + var url = imageCropperValue.Src.GetCropUrl(imageCropperValue, cropAlias: cropAlias, useCropDimensions: true); + + return CreateHtmlString(url, htmlEncode); } public static IHtmlString GetCropUrl(this UrlHelper urlHelper, @@ -253,41 +306,13 @@ namespace Umbraco.Web bool upScale = true, bool htmlEncode = true) { - if (imageCropperValue == null) return EmptyHtmlString; + if (imageCropperValue == null || string.IsNullOrEmpty(imageCropperValue.Src)) return EmptyHtmlString; - var imageUrl = imageCropperValue.Src; - var url = imageUrl.GetCropUrl(imageCropperValue, width, height, cropAlias, quality, imageCropMode, - imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, - upScale); - return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); + var url = imageCropperValue.Src.GetCropUrl(imageCropperValue, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); + + return CreateHtmlString(url, htmlEncode); } - public static IHtmlString GetCropUrl(this UrlHelper urlHelper, - ImageCropperValue imageCropperValue, - string cropAlias, - int? width = null, - int? height = null, - int? quality = null, - ImageCropMode? imageCropMode = null, - ImageCropAnchor? imageCropAnchor = null, - bool preferFocalPoint = false, - bool useCropDimensions = true, - string cacheBusterValue = null, - string furtherOptions = null, - ImageCropRatioMode? ratioMode = null, - bool upScale = true, - bool htmlEncode = true) - { - if (imageCropperValue == null) return EmptyHtmlString; - - var imageUrl = imageCropperValue.Src; - var url = imageUrl.GetCropUrl(imageCropperValue, width, height, cropAlias, quality, imageCropMode, - imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, - upScale); - return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); - } - - #endregion /// From 83ee9b4699d3428dbb9342f7afbacb592df25d9c Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 28 Jun 2021 09:32:42 +0200 Subject: [PATCH 255/289] Reduce sql queries when rendering blocklists in the content panel (#10521) Co-authored-by: Nikolaj --- src/Umbraco.Core/Services/IContentService.cs | 7 +- .../Services/Implement/ContentService.cs | 31 +++++- src/Umbraco.Web/Editors/ContentController.cs | 94 ++++++++++++++----- .../Models/Mapping/ContentMapDefinition.cs | 48 ++++++++-- 4 files changed, 141 insertions(+), 39 deletions(-) diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index c291500533..e94d40184e 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -323,7 +323,7 @@ namespace Umbraco.Core.Services /// /// Empties the Recycle Bin by deleting all that resides in the bin /// - /// Optional Id of the User emptying the Recycle Bin + /// Optional Id of the User emptying the Recycle Bin OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId); /// @@ -499,6 +499,11 @@ namespace Umbraco.Core.Services /// IContent Create(string name, int parentId, string documentTypeAlias, int userId = Constants.Security.SuperUserId); + /// + /// Creates a document + /// + IContent Create(string name, int parentId, IContentType contentType, int userId = Constants.Security.SuperUserId); + /// /// Creates a document. /// diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index dc914ca3ac..af14d7fa69 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -188,11 +188,34 @@ namespace Umbraco.Core.Services.Implement // TODO: what about culture? var contentType = GetContentType(contentTypeAlias); - if (contentType == null) - throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); + return Create(name, parentId, contentType, userId); + } + + /// + /// Creates an object of a specified content type. + /// + /// This method simply returns a new, non-persisted, IContent without any identity. It + /// is intended as a shortcut to creating new content objects that does not invoke a save + /// operation against the database. + /// + /// The name of the content object. + /// The identifier of the parent, or -1. + /// The content type of the content + /// The optional id of the user creating the content. + /// The content object. + public IContent Create(string name, int parentId, IContentType contentType, + int userId = Constants.Security.SuperUserId) + { + if (contentType is null) + { + throw new ArgumentException("Content type must be specified", nameof(contentType)); + } + var parent = parentId > 0 ? GetById(parentId) : null; - if (parentId > 0 && parent == null) + if (parentId > 0 && parent is null) + { throw new ArgumentException("No content with that id.", nameof(parentId)); + } var content = new Content(name, parentId, contentType); using (var scope = ScopeProvider.CreateScope()) @@ -1088,7 +1111,7 @@ namespace Umbraco.Core.Services.Implement /// /// /// Business logic cases such: as unpublishing a mandatory culture, or unpublishing the last culture, checking for pending scheduled publishing, etc... is dealt with in this method. - /// There is quite a lot of cases to take into account along with logic that needs to deal with scheduled saving/publishing, branch saving/publishing, etc... + /// There is quite a lot of cases to take into account along with logic that needs to deal with scheduled saving/publishing, branch saving/publishing, etc... /// /// private PublishResult CommitDocumentChangesInternal(IScope scope, IContent content, diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index fc473d729d..9c384edd75 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -37,7 +37,7 @@ using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence; using Umbraco.Core.Security; using Umbraco.Web.Routing; -using Umbraco.Core.Collections; +using Umbraco.Core.Mapping; using Umbraco.Core.Scoping; namespace Umbraco.Web.Editors @@ -388,20 +388,65 @@ namespace Umbraco.Web.Editors } } - private ContentItemDisplay GetEmpty(IContentType contentType, int parentId) + private ContentItemDisplay CleanContentItemDisplay(ContentItemDisplay display) { - var emptyContent = Services.ContentService.Create("", parentId, contentType.Alias, Security.GetUserId().ResultOr(0)); - var mapped = MapToDisplay(emptyContent); // translate the content type name if applicable - mapped.ContentTypeName = Services.TextService.UmbracoDictionaryTranslate(mapped.ContentTypeName); + display.ContentTypeName = Services.TextService.UmbracoDictionaryTranslate(display.ContentTypeName); // if your user type doesn't have access to the Settings section it would not get this property mapped - if (mapped.DocumentType != null) - mapped.DocumentType.Name = Services.TextService.UmbracoDictionaryTranslate(mapped.DocumentType.Name); + if (display.DocumentType != null) + display.DocumentType.Name = Services.TextService.UmbracoDictionaryTranslate(display.DocumentType.Name); //remove the listview app if it exists - mapped.ContentApps = mapped.ContentApps.Where(x => x.Alias != "umbListView").ToList(); + display.ContentApps = display.ContentApps.Where(x => x.Alias != "umbListView").ToList(); + return display; + } - return mapped; + private ContentItemDisplay GetEmpty(IContentType contentType, int parentId) + { + var emptyContent = Services.ContentService.Create("", parentId, contentType, Security.GetUserId().ResultOr(0)); + var mapped = MapToDisplay(emptyContent); + return CleanContentItemDisplay(mapped); + } + + /// + /// Gets an empty for each content type in the IEnumerable, all with the same parent ID + /// + /// Will attempt to re-use the same permissions for every content as long as the path and user are the same + /// + /// + /// + private IEnumerable GetEmpties(IEnumerable contentTypes, int parentId) + { + var result = new List(); + var userId = Security.GetUserId().ResultOr(0); + var currentUser = Security.CurrentUser; + // We know that if the ID is less than 0 the parent is null. + // Since this is called with parent ID it's safe to assume that the parent is the same for all the content types. + var parent = parentId > 0 ? Services.ContentService.GetById(parentId) : null; + // Since the parent is the same and the path used to get permissions is based on the parent we only have to do it once + var path = parent == null ? "-1" : parent.Path; + var permissions = new Dictionary + { + [path] = Services.UserService.GetPermissionsForPath(currentUser, path) + }; + + foreach (var contentType in contentTypes) + { + var emptyContent = Services.ContentService.Create("", parentId, contentType, userId); + + var mapped = MapToDisplay(emptyContent, context => + { + // Since the permissions depend on current user and path, we add both of these to context as well, + // that way we can compare the path and current user when mapping, if they're the same just take permissions + // and skip getting them again, in theory they should always be the same, but better safe than sorry., + context.Items["Parent"] = parent; + context.Items["CurrentUser"] = currentUser; + context.Items["Permissions"] = permissions; + }); + result.Add(CleanContentItemDisplay(mapped)); + } + + return result; } /// @@ -412,22 +457,9 @@ namespace Umbraco.Web.Editors [OutgoingEditorModelEvent] public IDictionary GetEmptyByKeys([FromUri] Guid[] contentTypeKeys, [FromUri] int parentId) { - var result = new Dictionary(); - using var scope = _scopeProvider.CreateScope(autoComplete: true); var contentTypes = Services.ContentTypeService.GetAll(contentTypeKeys).ToList(); - - foreach (var contentType in contentTypes) - { - if (contentType is null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - result.Add(contentType.Key, GetEmpty(contentType, parentId)); - } - - return result; + return GetEmpties(contentTypes, parentId).ToDictionary(x => x.ContentTypeKey); } [OutgoingEditorModelEvent] @@ -2274,12 +2306,22 @@ namespace Umbraco.Web.Editors /// /// /// - private ContentItemDisplay MapToDisplay(IContent content) - { - var display = Mapper.Map(content, context => + private ContentItemDisplay MapToDisplay(IContent content) => + MapToDisplay(content, context => { context.Items["CurrentUser"] = Security.CurrentUser; }); + + /// + /// Used to map an instance to a and ensuring AllowPreview is set correctly. + /// Also allows you to pass in an action for the mapper context where you can pass additional information on to the mapper. + /// + /// + /// + /// + private ContentItemDisplay MapToDisplay(IContent content, Action contextOptions) + { + var display = Mapper.Map(content, contextOptions); display.AllowPreview = display.AllowPreview && content.Trashed == false && content.ContentType.IsElement == false; return display; } diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs index 0360ee568b..cb2d0f1aba 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs @@ -86,7 +86,20 @@ namespace Umbraco.Web.Models.Mapping // Umbraco.Code.MapAll -AllowPreview -Errors -PersistedContent private void Map(IContent source, ContentItemDisplay target, MapperContext context) { - target.AllowedActions = GetActions(source); + // Both GetActions and DetermineIsChildOfListView use parent, so get it once here + // Parent might already be in context, so check there before using content service + IContent parent; + if (context.Items.TryGetValue("Parent", out var parentObj) && + parentObj is IContent typedParent) + { + parent = typedParent; + } + else + { + parent = _contentService.GetParent(source); + } + + target.AllowedActions = GetActions(source, parent, context); target.AllowedTemplates = GetAllowedTemplates(source); target.ContentApps = _commonMapper.GetContentApps(source); target.ContentTypeId = source.ContentType.Id; @@ -97,7 +110,7 @@ namespace Umbraco.Web.Models.Mapping target.Icon = source.ContentType.Icon; target.Id = source.Id; target.IsBlueprint = source.Blueprint; - target.IsChildOfListView = DetermineIsChildOfListView(source, context); + target.IsChildOfListView = DetermineIsChildOfListView(source, parent, context); target.IsContainer = source.ContentType.IsContainer; target.IsElement = source.ContentType.IsElement; target.Key = source.Key; @@ -156,7 +169,7 @@ namespace Umbraco.Web.Models.Mapping target.VariesByCulture = source.ContentType.VariesByCulture(); } - private IEnumerable GetActions(IContent source) + private IEnumerable GetActions(IContent source, IContent parent, MapperContext context) { var umbracoContext = _umbracoContextAccessor.UmbracoContext; @@ -169,10 +182,24 @@ namespace Umbraco.Web.Models.Mapping path = source.Path; else { - var parent = _contentService.GetById(source.ParentId); path = parent == null ? "-1" : parent.Path; } + // A bit of a mess, but we need to ensure that all the required values are here AND that they're the right type. + if (context.Items.TryGetValue("CurrentUser", out var userObject) && + context.Items.TryGetValue("Permissions", out var permissionsObject) && + userObject is IUser currentUser && + permissionsObject is Dictionary permissionsDict) + { + // If we already have permissions for a given path, + // and the current user is the same as was used to generate the permissions, return the stored permissions. + if (umbracoContext.Security.CurrentUser.Id == currentUser.Id && + permissionsDict.TryGetValue(path, out var permissions)) + { + return permissions.GetAllPermissions(); + } + } + // TODO: This is certainly not ideal usage here - perhaps the best way to deal with this in the future is // with the IUmbracoContextAccessor. In the meantime, if used outside of a web app this will throw a null // reference exception :( @@ -232,6 +259,7 @@ namespace Umbraco.Web.Models.Mapping /// Checks if the content item is a descendant of a list view /// /// + /// /// /// /// Returns true if the content item is a descendant of a list view and where the content is @@ -243,7 +271,7 @@ namespace Umbraco.Web.Models.Mapping /// false because the item is technically not being rendered as part of a list view but instead as a /// real tree node. If we didn't perform this check then tree syncing wouldn't work correctly. /// - private bool DetermineIsChildOfListView(IContent source, MapperContext context) + private bool DetermineIsChildOfListView(IContent source, IContent parent, MapperContext context) { var userStartNodes = Array.Empty(); @@ -258,12 +286,10 @@ namespace Umbraco.Web.Models.Mapping // return false if this is the user's actual start node, the node will be rendered in the tree // regardless of if it's a list view or not if (userStartNodes.Contains(source.Id)) - return false; + return false; } } - var parent = _contentService.GetParent(source); - if (parent == null) return false; @@ -297,6 +323,12 @@ namespace Umbraco.Web.Models.Mapping private IDictionary GetAllowedTemplates(IContent source) { + // Element types can't have templates, so no need to query to get the content type + if (source.ContentType.IsElement) + { + return new Dictionary(); + } + var contentType = _contentTypeService.Get(source.ContentTypeId); return contentType.AllowedTemplates From 31e9c875f5f2ec6d45f51a6ebc8770a5427e675b Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 28 Jun 2021 09:36:09 +0200 Subject: [PATCH 256/289] Bump version to 8.15.0-rc --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 3619dc1371..6ef4d6ce85 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.14.0")] -[assembly: AssemblyInformationalVersion("8.14.0")] +[assembly: AssemblyFileVersion("8.15.0")] +[assembly: AssemblyInformationalVersion("8.15.0-rc")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 180837bc80..d6b2374c8e 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -330,7 +330,7 @@ $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v14.0\Web\Microsoft.Web.Publishing.Tasks.dll $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v15.0\Web\Microsoft.Web.Publishing.Tasks.dll $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v16.0\Web\Microsoft.Web.Publishing.Tasks.dll - $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v17.0\Web\Microsoft.Web.Publishing.Tasks.dll + $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v17.0\Web\Microsoft.Web.Publishing.Tasks.dll @@ -348,9 +348,9 @@ False True - 8140 + 8150 / - http://localhost:8140 + http://localhost:8150 8131 / http://localhost:8131 @@ -436,4 +436,4 @@ - + \ No newline at end of file From 5d8fb9670bd75b4a002017b0dcd849c1fa159497 Mon Sep 17 00:00:00 2001 From: Jan Skovgaard <1932158+BatJan@users.noreply.github.com> Date: Tue, 29 Jun 2021 00:37:19 +0200 Subject: [PATCH 257/289] Textstring prevalue editor view: Set id attribute (#10453) * Add missing focus styling * Set id attribute Co-authored-by: BatJan Co-authored-by: Jan Skovgaard Olsen --- .../src/views/prevalueeditors/textstring.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/textstring.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/textstring.html index 717b9a8ce2..5d613881b1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/textstring.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/textstring.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 897cb11f005c706f4b7f80907e4395c87b3a4fc8 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Date: Tue, 29 Jun 2021 09:55:02 +0200 Subject: [PATCH 258/289] Nested content optimizations (#10236) * Reducing the large amount of objects allocated and make use of the attemped dictionary cache * Reverting optimization by passing data from a higher level - not really possible to achieve what we wanted and adding faster way to lookup value editors --- .../PropertyEditors/NestedContentPropertyEditor.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 1047c4317d..b0eeacacd9 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -156,6 +156,7 @@ namespace Umbraco.Web.PropertyEditors public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null) { var val = property.GetValue(culture, segment); + var valEditors = new Dictionary(); var rows = _nestedContentValues.GetPropertyValues(val); @@ -184,8 +185,15 @@ namespace Umbraco.Web.PropertyEditors continue; } - var tempConfig = dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration; - var valEditor = propEditor.GetValueEditor(tempConfig); + var dataTypeId = prop.Value.PropertyType.DataTypeId; + if (!valEditors.TryGetValue(dataTypeId, out var valEditor)) + { + var tempConfig = dataTypeService.GetDataType(dataTypeId).Configuration; + valEditor = propEditor.GetValueEditor(tempConfig); + + valEditors.Add(dataTypeId, valEditor); + } + var convValue = valEditor.ToEditor(tempProp, dataTypeService); // update the raw value since this is what will get serialized out From 3e89e468406f4eb3ad33522bcad8a357fc7bff14 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Date: Tue, 29 Jun 2021 09:55:02 +0200 Subject: [PATCH 259/289] Nested content optimizations (#10236) * Reducing the large amount of objects allocated and make use of the attemped dictionary cache * Reverting optimization by passing data from a higher level - not really possible to achieve what we wanted and adding faster way to lookup value editors (cherry picked from commit 897cb11f005c706f4b7f80907e4395c87b3a4fc8) --- .../PropertyEditors/NestedContentPropertyEditor.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 1047c4317d..b0eeacacd9 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -156,6 +156,7 @@ namespace Umbraco.Web.PropertyEditors public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null) { var val = property.GetValue(culture, segment); + var valEditors = new Dictionary(); var rows = _nestedContentValues.GetPropertyValues(val); @@ -184,8 +185,15 @@ namespace Umbraco.Web.PropertyEditors continue; } - var tempConfig = dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration; - var valEditor = propEditor.GetValueEditor(tempConfig); + var dataTypeId = prop.Value.PropertyType.DataTypeId; + if (!valEditors.TryGetValue(dataTypeId, out var valEditor)) + { + var tempConfig = dataTypeService.GetDataType(dataTypeId).Configuration; + valEditor = propEditor.GetValueEditor(tempConfig); + + valEditors.Add(dataTypeId, valEditor); + } + var convValue = valEditor.ToEditor(tempProp, dataTypeService); // update the raw value since this is what will get serialized out From 917c89cde7825ea92bbcc0f1535b7fd00e7b0754 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Date: Tue, 29 Jun 2021 11:46:07 +0200 Subject: [PATCH 260/289] Ability to configure a remote URL for displaying different content on the content dashboard (#10257) * Enabling configuration of remote URL for fetching content on the content dashboard and adding additional params indicating whether solution is running on Umbraco Cloud * Determining if Umbraco is running on Umbraco Cloud by checking whether Umbraco.Deploy.Cloud.dll is present and making its value more accessible for other contexts. Passing a hosting parameter as string instead of boolean * Changing from Unknown to OnPremises * Updated content dashboard to use hard-coded URL but path derived from configuration. Co-authored-by: Andy Butland --- src/Umbraco.Core/Constants-AppSettings.cs | 7 ++++++- .../Dashboards/ContentDashboardSettings.cs | 10 ++++++++++ .../Dashboards/IContentDashboardSettings.cs | 6 ++++++ src/Umbraco.Web.UI/web.Template.config | 1 + .../Editors/DashboardController.cs | 19 ++++++++++++++++--- 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Constants-AppSettings.cs b/src/Umbraco.Core/Constants-AppSettings.cs index 1f096ab9f9..99ea26b4d6 100644 --- a/src/Umbraco.Core/Constants-AppSettings.cs +++ b/src/Umbraco.Core/Constants-AppSettings.cs @@ -109,12 +109,17 @@ namespace Umbraco.Core /// A true or false indicating whether umbraco should force a secure (https) connection to the backoffice. /// public const string UseHttps = "Umbraco.Core.UseHttps"; - + /// /// A true/false value indicating whether the content dashboard should be visible for all user groups. /// public const string AllowContentDashboardAccessToAllUsers = "Umbraco.Core.AllowContentDashboardAccessToAllUsers"; + /// + /// The path to use when constructing the URL for retrieving data for the content dashboard. + /// + public const string ContentDashboardPath = "Umbraco.Core.ContentDashboardPath"; + /// /// TODO: FILL ME IN /// diff --git a/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs b/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs index f8fb5c7b06..b370f93eca 100644 --- a/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs +++ b/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs @@ -4,6 +4,7 @@ namespace Umbraco.Core.Dashboards { public class ContentDashboardSettings: IContentDashboardSettings { + private const string DefaultContentDashboardPath = "cms"; /// /// Gets a value indicating whether the content dashboard should be available to all users. @@ -20,5 +21,14 @@ namespace Umbraco.Core.Dashboards return value; } } + + /// + /// Gets the path to use when constructing the URL for retrieving data for the content dashboard. + /// + /// The URL path. + public string ContentDashboardPath => + ConfigurationManager.AppSettings.ContainsKey(Constants.AppSettings.ContentDashboardPath) + ? ConfigurationManager.AppSettings[Constants.AppSettings.ContentDashboardPath] + : DefaultContentDashboardPath; } } diff --git a/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs b/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs index 862a28b90e..f5c4e3da78 100644 --- a/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs +++ b/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs @@ -10,5 +10,11 @@ /// and the default access rules for that dashboard will be in use. /// bool AllowContentDashboardAccessToAllUsers { get; } + + /// + /// Gets the path to use when constructing the URL for retrieving data for the content dashboard. + /// + /// The URL path. + string ContentDashboardPath { get; } } } diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index 8c4b421839..c6b1eb686c 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -38,6 +38,7 @@ + diff --git a/src/Umbraco.Web/Editors/DashboardController.cs b/src/Umbraco.Web/Editors/DashboardController.cs index 97db8818f2..aa9691e3dd 100644 --- a/src/Umbraco.Web/Editors/DashboardController.cs +++ b/src/Umbraco.Web/Editors/DashboardController.cs @@ -17,6 +17,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Core.Dashboards; +using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Web.Services; @@ -32,14 +33,19 @@ namespace Umbraco.Web.Editors public class DashboardController : UmbracoApiController { private readonly IDashboardService _dashboardService; + private readonly IContentDashboardSettings _dashboardSettings; /// /// Initializes a new instance of the with all its dependencies. /// - public DashboardController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, IDashboardService dashboardService, UmbracoHelper umbracoHelper) + public DashboardController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, + ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, + IRuntimeState runtimeState, IDashboardService dashboardService, UmbracoHelper umbracoHelper, + IContentDashboardSettings dashboardSettings) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) { _dashboardService = dashboardService; + _dashboardSettings = dashboardSettings; } //we have just one instance of HttpClient shared for the entire application @@ -47,7 +53,7 @@ namespace Umbraco.Web.Editors //we have baseurl as a param to make previewing easier, so we can test with a dev domain from client side [ValidateAngularAntiForgeryToken] - public async Task GetRemoteDashboardContent(string section, string baseUrl = "https://dashboard.umbraco.org/") + public async Task GetRemoteDashboardContent(string section, string baseUrl = "https://dashboard.umbraco.com/") { var user = Security.CurrentUser; var allowedSections = string.Join(",", user.AllowedSections); @@ -55,7 +61,14 @@ namespace Umbraco.Web.Editors var version = UmbracoVersion.SemanticVersion.ToSemanticString(); var isAdmin = user.IsAdmin(); - var url = string.Format(baseUrl + "{0}?section={0}&allowed={1}&lang={2}&version={3}&admin={4}", section, allowedSections, language, version, isAdmin); + var url = string.Format("{0}{1}?section={2}&allowed={3}&lang={4}&version={5}&admin={6}", + baseUrl, + _dashboardSettings.ContentDashboardPath, + section, + allowedSections, + language, + version, + isAdmin); var key = "umbraco-dynamic-dashboard-" + language + allowedSections.Replace(",", "-") + section; var content = AppCaches.RuntimeCache.GetCacheItem(key); From 4f46edbaa9dd69cd3870b227e376cabe45de67cd Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 28 Jun 2021 09:36:09 +0200 Subject: [PATCH 261/289] Bump version to 8.15.0-rc --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 3619dc1371..6ef4d6ce85 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.14.0")] -[assembly: AssemblyInformationalVersion("8.14.0")] +[assembly: AssemblyFileVersion("8.15.0")] +[assembly: AssemblyInformationalVersion("8.15.0-rc")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 180837bc80..d6b2374c8e 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -330,7 +330,7 @@ $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v14.0\Web\Microsoft.Web.Publishing.Tasks.dll $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v15.0\Web\Microsoft.Web.Publishing.Tasks.dll $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v16.0\Web\Microsoft.Web.Publishing.Tasks.dll - $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v17.0\Web\Microsoft.Web.Publishing.Tasks.dll + $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v17.0\Web\Microsoft.Web.Publishing.Tasks.dll @@ -348,9 +348,9 @@ False True - 8140 + 8150 / - http://localhost:8140 + http://localhost:8150 8131 / http://localhost:8131 @@ -436,4 +436,4 @@ - + \ No newline at end of file From 861fc40572c82564a5f925bdda36323d94033f55 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Date: Tue, 29 Jun 2021 11:46:07 +0200 Subject: [PATCH 262/289] Ability to configure a remote URL for displaying different content on the content dashboard (#10257) * Enabling configuration of remote URL for fetching content on the content dashboard and adding additional params indicating whether solution is running on Umbraco Cloud * Determining if Umbraco is running on Umbraco Cloud by checking whether Umbraco.Deploy.Cloud.dll is present and making its value more accessible for other contexts. Passing a hosting parameter as string instead of boolean * Changing from Unknown to OnPremises * Updated content dashboard to use hard-coded URL but path derived from configuration. Co-authored-by: Andy Butland (cherry picked from commit 917c89cde7825ea92bbcc0f1535b7fd00e7b0754) --- src/Umbraco.Core/Constants-AppSettings.cs | 7 ++++++- .../Dashboards/ContentDashboardSettings.cs | 10 ++++++++++ .../Dashboards/IContentDashboardSettings.cs | 6 ++++++ src/Umbraco.Web.UI/web.Template.config | 1 + .../Editors/DashboardController.cs | 19 ++++++++++++++++--- 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Constants-AppSettings.cs b/src/Umbraco.Core/Constants-AppSettings.cs index 1f096ab9f9..99ea26b4d6 100644 --- a/src/Umbraco.Core/Constants-AppSettings.cs +++ b/src/Umbraco.Core/Constants-AppSettings.cs @@ -109,12 +109,17 @@ namespace Umbraco.Core /// A true or false indicating whether umbraco should force a secure (https) connection to the backoffice. /// public const string UseHttps = "Umbraco.Core.UseHttps"; - + /// /// A true/false value indicating whether the content dashboard should be visible for all user groups. /// public const string AllowContentDashboardAccessToAllUsers = "Umbraco.Core.AllowContentDashboardAccessToAllUsers"; + /// + /// The path to use when constructing the URL for retrieving data for the content dashboard. + /// + public const string ContentDashboardPath = "Umbraco.Core.ContentDashboardPath"; + /// /// TODO: FILL ME IN /// diff --git a/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs b/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs index f8fb5c7b06..b370f93eca 100644 --- a/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs +++ b/src/Umbraco.Core/Dashboards/ContentDashboardSettings.cs @@ -4,6 +4,7 @@ namespace Umbraco.Core.Dashboards { public class ContentDashboardSettings: IContentDashboardSettings { + private const string DefaultContentDashboardPath = "cms"; /// /// Gets a value indicating whether the content dashboard should be available to all users. @@ -20,5 +21,14 @@ namespace Umbraco.Core.Dashboards return value; } } + + /// + /// Gets the path to use when constructing the URL for retrieving data for the content dashboard. + /// + /// The URL path. + public string ContentDashboardPath => + ConfigurationManager.AppSettings.ContainsKey(Constants.AppSettings.ContentDashboardPath) + ? ConfigurationManager.AppSettings[Constants.AppSettings.ContentDashboardPath] + : DefaultContentDashboardPath; } } diff --git a/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs b/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs index 862a28b90e..f5c4e3da78 100644 --- a/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs +++ b/src/Umbraco.Core/Dashboards/IContentDashboardSettings.cs @@ -10,5 +10,11 @@ /// and the default access rules for that dashboard will be in use. /// bool AllowContentDashboardAccessToAllUsers { get; } + + /// + /// Gets the path to use when constructing the URL for retrieving data for the content dashboard. + /// + /// The URL path. + string ContentDashboardPath { get; } } } diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index 8c4b421839..c6b1eb686c 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -38,6 +38,7 @@ + diff --git a/src/Umbraco.Web/Editors/DashboardController.cs b/src/Umbraco.Web/Editors/DashboardController.cs index 97db8818f2..aa9691e3dd 100644 --- a/src/Umbraco.Web/Editors/DashboardController.cs +++ b/src/Umbraco.Web/Editors/DashboardController.cs @@ -17,6 +17,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Core.Dashboards; +using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Web.Services; @@ -32,14 +33,19 @@ namespace Umbraco.Web.Editors public class DashboardController : UmbracoApiController { private readonly IDashboardService _dashboardService; + private readonly IContentDashboardSettings _dashboardSettings; /// /// Initializes a new instance of the with all its dependencies. /// - public DashboardController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, IDashboardService dashboardService, UmbracoHelper umbracoHelper) + public DashboardController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, + ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, + IRuntimeState runtimeState, IDashboardService dashboardService, UmbracoHelper umbracoHelper, + IContentDashboardSettings dashboardSettings) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) { _dashboardService = dashboardService; + _dashboardSettings = dashboardSettings; } //we have just one instance of HttpClient shared for the entire application @@ -47,7 +53,7 @@ namespace Umbraco.Web.Editors //we have baseurl as a param to make previewing easier, so we can test with a dev domain from client side [ValidateAngularAntiForgeryToken] - public async Task GetRemoteDashboardContent(string section, string baseUrl = "https://dashboard.umbraco.org/") + public async Task GetRemoteDashboardContent(string section, string baseUrl = "https://dashboard.umbraco.com/") { var user = Security.CurrentUser; var allowedSections = string.Join(",", user.AllowedSections); @@ -55,7 +61,14 @@ namespace Umbraco.Web.Editors var version = UmbracoVersion.SemanticVersion.ToSemanticString(); var isAdmin = user.IsAdmin(); - var url = string.Format(baseUrl + "{0}?section={0}&allowed={1}&lang={2}&version={3}&admin={4}", section, allowedSections, language, version, isAdmin); + var url = string.Format("{0}{1}?section={2}&allowed={3}&lang={4}&version={5}&admin={6}", + baseUrl, + _dashboardSettings.ContentDashboardPath, + section, + allowedSections, + language, + version, + isAdmin); var key = "umbraco-dynamic-dashboard-" + language + allowedSections.Replace(",", "-") + section; var content = AppCaches.RuntimeCache.GetCacheItem(key); From dc27a31a6fe35a885eab9079d2e002fc53da6391 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 29 Jun 2021 13:35:22 +0200 Subject: [PATCH 263/289] Fix creating / editing datatype in infinite editing mode, related to #10163 --- .../src/common/services/datatypehelper.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/datatypehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/datatypehelper.service.js index f4317b51b7..7aff3faaaf 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/datatypehelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/datatypehelper.service.js @@ -14,7 +14,7 @@ function dataTypeHelper() { for (var i = 0; i < preVals.length; i++) { preValues.push({ hideLabel: preVals[i].hideLabel, - alias: preVals[i].key, + alias: preVals[i].key != undefined ? preVals[i].key : preVals[i].alias, description: preVals[i].description, label: preVals[i].label, view: preVals[i].view, From a19b79553d16dad705c62804df9adcad7312bac8 Mon Sep 17 00:00:00 2001 From: David Challener <694674+dchallener@users.noreply.github.com> Date: Tue, 29 Jun 2021 16:12:45 +0100 Subject: [PATCH 264/289] Update contributing.md Visual Studio min version Having had an older version of visual studio running I've been unable to build the source without making changes to the solution. However, upgrading to at least VS 16.8 has meant those changes haven't been required. This is to do with how the language target "latest" was working. Propose upping the min version listed here to avoid build issues being raised. --- .github/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 3432ac472a..bada94c30b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -136,7 +136,7 @@ You can get in touch with [the core contributors team](#the-core-contributors-te In order to build the Umbraco source code locally, first make sure you have the following installed. - * [Visual Studio 2019 v16.3+ (with .NET Core 3.0)](https://visualstudio.microsoft.com/vs/) + * [Visual Studio 2019 v16.8+ (with .NET Core 3.0)](https://visualstudio.microsoft.com/vs/) * [Node.js v10+](https://nodejs.org/en/download/) * npm v6.4.1+ (installed with Node.js) * [Git command line](https://git-scm.com/download/) From c1a4e07e87df428d71314174854c954749842144 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 30 Jun 2021 15:41:07 +0200 Subject: [PATCH 265/289] #10520 Correct mandatory markers in nested content (#10563) --- .../src/less/components/umb-nested-content.less | 11 ----------- .../src/views/components/property/umb-property.html | 2 +- .../nestedcontent/nestedcontent.editor.html | 2 +- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less index 9dd40a4386..f6c252cc4d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -15,17 +15,6 @@ pointer-events: none; } -.umb-nested-content--mandatory { - /* - yeah so this is a pain, but we must be super specific in targeting the mandatory property labels, - otherwise all properties within a reqired, nested, nested content property will all appear mandatory - */ - .umb-property > ng-form > .control-group > .umb-el-wrap > .control-header label:after { - content: '*'; - color: @red; - } -} - .umb-nested-content-overlay { position: absolute; top: 0; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html index 5b8e6d8f04..51a6f65c9c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html @@ -16,7 +16,7 @@ {{vm.property.label}} - + * diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html index 125e920fe6..e14bd03291 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html @@ -4,7 +4,7 @@ From 0b3c4a726c9d97480a804aaf6fdeb317ee8b59cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 30 Jun 2021 16:27:53 +0200 Subject: [PATCH 266/289] #10520 ensure property culture in Nested Content (#10562) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Niels Lyngsø --- .../nestedcontent/nestedcontent.controller.js | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index 68d375360e..446fb8c076 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -190,7 +190,7 @@ }; vm.openNodeTypePicker = function ($event) { - + if (vm.nodes.length >= vm.maxItems) { return; } @@ -537,15 +537,20 @@ if (tab) { scaffold.variants[0].tabs.push(tab); - tab.properties.forEach(function (property) { + tab.properties.forEach( + function (property) { if (_.find(notSupported, function (x) { return x === property.editor; })) { property.notSupported = true; // TODO: Not supported message to be replaced with 'content_nestedContentEditorNotSupported' dictionary key. Currently not possible due to async/timing quirk. property.notSupportedMessage = "Property " + property.label + " uses editor " + property.editor + " which is not supported by Nested Content."; } - }); + } + ); } + // Ensure Culture Data for Complex Validation. + ensureCultureData(scaffold); + // Store the scaffold object vm.scaffolds.push(scaffold); } @@ -558,6 +563,29 @@ }); }); + /** + * Ensure that the containing content variant language and current property culture is transferred along + * to the scaffolded content object representing this block. + * This is required for validation along with ensuring that the umb-property inheritance is constantly maintained. + * @param {any} content + */ + function ensureCultureData(content) { + + if (!content || !vm.umbVariantContent || !vm.umbProperty) return; + + if (vm.umbVariantContent.editor.content.language) { + // set the scaffolded content's language to the language of the current editor + content.language = vm.umbVariantContent.editor.content.language; + } + // currently we only ever deal with invariant content for blocks so there's only one + content.variants[0].tabs.forEach(tab => { + tab.properties.forEach(prop => { + // set the scaffolded property to the culture of the containing property + prop.culture = vm.umbProperty.property.culture; + }); + }); + } + var initIfAllScaffoldsHaveLoaded = function () { // Initialize when all scaffolds have loaded if (model.config.contentTypes.length === scaffoldsLoaded) { From 3e226384c3eb7940cad6357f3dbab4411c1a5bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 30 Jun 2021 16:27:53 +0200 Subject: [PATCH 267/289] #10520 ensure property culture in Nested Content (#10562) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Niels Lyngsø (cherry picked from commit 0b3c4a726c9d97480a804aaf6fdeb317ee8b59cf) --- .../nestedcontent/nestedcontent.controller.js | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index 68d375360e..446fb8c076 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -190,7 +190,7 @@ }; vm.openNodeTypePicker = function ($event) { - + if (vm.nodes.length >= vm.maxItems) { return; } @@ -537,15 +537,20 @@ if (tab) { scaffold.variants[0].tabs.push(tab); - tab.properties.forEach(function (property) { + tab.properties.forEach( + function (property) { if (_.find(notSupported, function (x) { return x === property.editor; })) { property.notSupported = true; // TODO: Not supported message to be replaced with 'content_nestedContentEditorNotSupported' dictionary key. Currently not possible due to async/timing quirk. property.notSupportedMessage = "Property " + property.label + " uses editor " + property.editor + " which is not supported by Nested Content."; } - }); + } + ); } + // Ensure Culture Data for Complex Validation. + ensureCultureData(scaffold); + // Store the scaffold object vm.scaffolds.push(scaffold); } @@ -558,6 +563,29 @@ }); }); + /** + * Ensure that the containing content variant language and current property culture is transferred along + * to the scaffolded content object representing this block. + * This is required for validation along with ensuring that the umb-property inheritance is constantly maintained. + * @param {any} content + */ + function ensureCultureData(content) { + + if (!content || !vm.umbVariantContent || !vm.umbProperty) return; + + if (vm.umbVariantContent.editor.content.language) { + // set the scaffolded content's language to the language of the current editor + content.language = vm.umbVariantContent.editor.content.language; + } + // currently we only ever deal with invariant content for blocks so there's only one + content.variants[0].tabs.forEach(tab => { + tab.properties.forEach(prop => { + // set the scaffolded property to the culture of the containing property + prop.culture = vm.umbProperty.property.culture; + }); + }); + } + var initIfAllScaffoldsHaveLoaded = function () { // Initialize when all scaffolds have loaded if (model.config.contentTypes.length === scaffoldsLoaded) { From fc108511d28153495353abd8527032a78170df8f Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 30 Jun 2021 15:41:07 +0200 Subject: [PATCH 268/289] #10520 Correct mandatory markers in nested content (#10563) (cherry picked from commit c1a4e07e87df428d71314174854c954749842144) --- .../src/less/components/umb-nested-content.less | 11 ----------- .../src/views/components/property/umb-property.html | 2 +- .../nestedcontent/nestedcontent.editor.html | 2 +- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less index 9dd40a4386..f6c252cc4d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -15,17 +15,6 @@ pointer-events: none; } -.umb-nested-content--mandatory { - /* - yeah so this is a pain, but we must be super specific in targeting the mandatory property labels, - otherwise all properties within a reqired, nested, nested content property will all appear mandatory - */ - .umb-property > ng-form > .control-group > .umb-el-wrap > .control-header label:after { - content: '*'; - color: @red; - } -} - .umb-nested-content-overlay { position: absolute; top: 0; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html index 5b8e6d8f04..51a6f65c9c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html @@ -16,7 +16,7 @@ {{vm.property.label}} - + * diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html index 125e920fe6..e14bd03291 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html @@ -4,7 +4,7 @@ From 33e44181c1d044f4c5a04a3a495290725df69d2a Mon Sep 17 00:00:00 2001 From: David Challener <694674+dchallener@users.noreply.github.com> Date: Tue, 29 Jun 2021 22:05:18 +0100 Subject: [PATCH 269/289] Disable cached partials if in preview mode We don't want unpublished items in cached views - possible leak to live site. Show previews using normal partial view instead. --- src/Umbraco.Web/CacheHelperExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/CacheHelperExtensions.cs b/src/Umbraco.Web/CacheHelperExtensions.cs index ae8df63415..e3b96106db 100644 --- a/src/Umbraco.Web/CacheHelperExtensions.cs +++ b/src/Umbraco.Web/CacheHelperExtensions.cs @@ -37,7 +37,8 @@ namespace Umbraco.Web ViewDataDictionary viewData = null) { //disable cached partials in debug mode: http://issues.umbraco.org/issue/U4-5940 - if (htmlHelper.ViewContext.HttpContext.IsDebuggingEnabled) + //disable cached partials in preview mode: https://github.com/umbraco/Umbraco-CMS/issues/10384 + if (htmlHelper.ViewContext.HttpContext.IsDebuggingEnabled || Umbraco.Web.Composing.Current.UmbracoContext.InPreviewMode) { // just return a normal partial view instead return htmlHelper.Partial(partialViewName, model, viewData); From 7afcd590374e18c7232f38d913c1ee9d51177992 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Thu, 1 Jul 2021 13:48:56 +0100 Subject: [PATCH 270/289] Fixed "user-dialog" dashboard markup The logic in the markup was previously incorrect. I changed the check from `tab.length` to `dashboard.length`. The custom dashboard's label isn't `.caption` and it's per dashboard, not per "property". Lastly, `.path` should be `.view`. Resolves #6417 --- .../src/views/common/overlays/user/user.html | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html index fdd2671200..2de04a0147 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html @@ -130,13 +130,11 @@
    -
    +
    +
    {{tab.label}}
    -
    -

    {{property.caption}}

    -
    -
    +
    From d6a27d6646de5454e6480a1f7d1e22a63034a650 Mon Sep 17 00:00:00 2001 From: Jan Skovgaard <1932158+BatJan@users.noreply.github.com> Date: Thu, 1 Jul 2021 23:58:59 +0200 Subject: [PATCH 271/289] Color picker: Fix label view (#10445) * Add missing focus styling * Change width to ensure label does not get too wide Co-authored-by: BatJan Co-authored-by: Jan Skovgaard Olsen --- .../src/less/components/umb-color-swatches.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-color-swatches.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-color-swatches.less index 8cf64e183c..bdfc55f648 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-color-swatches.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-color-swatches.less @@ -59,7 +59,7 @@ font-size: 14px; padding: 1px 5px; min-height: 45px; - max-width: 100%; + max-width: calc(100% - 8px); margin-top: auto; margin-bottom: -3px; margin-left: -1px; From e67d59a789995fc95cb0f0a30233c9bf0c0899e2 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Mon, 22 Mar 2021 18:15:39 +0100 Subject: [PATCH 272/289] Adjust showLabels to be parsed as boolean --- .../src/views/propertyeditors/boolean/boolean.controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js index 018c2b72c1..c93b558a23 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js @@ -11,8 +11,8 @@ function booleanEditorController($scope) { showLabels: false }; - if ($scope.model.config && $scope.model.config.showLabels && Object.toBoolean($scope.model.config.showLabels)) { - config.showLabels = true; + if ($scope.model.config) { + $scope.model.config.showLabels = $scope.model.config.showLabels ? Object.toBoolean($scope.model.config.showLabels) : config.showLabels; } // Map the user config From 3e208904356f1fe1262a7edee052dbf9663a6ad3 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Fri, 2 Jul 2021 11:47:39 +0200 Subject: [PATCH 273/289] Fix dependencies to make 8.15 installable via NuGet --- build/NuSpecs/UmbracoCms.Web.nuspec | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index e96a217f4e..7aebfae108 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -43,7 +43,8 @@ - + + From 6816ae7ca41fb825b54dd23c9ffdfb1de32722a2 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Fri, 2 Jul 2021 11:47:39 +0200 Subject: [PATCH 274/289] Fix dependencies to make 8.15 installable via NuGet (cherry picked from commit 3e208904356f1fe1262a7edee052dbf9663a6ad3) --- build/NuSpecs/UmbracoCms.Web.nuspec | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index e96a217f4e..7aebfae108 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -43,7 +43,8 @@ - + + From 1e54d0551438b7ec97072b5fb14551163d17a81f Mon Sep 17 00:00:00 2001 From: Henk Jan Pluim Date: Tue, 6 Jul 2021 16:46:07 +0200 Subject: [PATCH 275/289] #10577 bugfix - Umbraco backoffice allows you to assign the same 'login' on two different Members (#10591) --- src/Umbraco.Core/Services/Implement/MemberService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/Implement/MemberService.cs b/src/Umbraco.Core/Services/Implement/MemberService.cs index 877bb5c9ce..b26a6bef39 100644 --- a/src/Umbraco.Core/Services/Implement/MemberService.cs +++ b/src/Umbraco.Core/Services/Implement/MemberService.cs @@ -626,7 +626,7 @@ namespace Umbraco.Core.Services.Implement query.Where(member => member.Username.EndsWith(login)); break; case StringPropertyMatchType.Wildcard: - query.Where(member => member.Email.SqlWildcard(login, TextColumnType.NVarchar)); + query.Where(member => member.Username.SqlWildcard(login, TextColumnType.NVarchar)); break; default: throw new ArgumentOutOfRangeException(nameof(matchType)); From a1e0af6fff971af3270b6292b0be38a6988d3a70 Mon Sep 17 00:00:00 2001 From: Rachel Breeze Date: Tue, 6 Jul 2021 18:39:56 +0100 Subject: [PATCH 276/289] Accessibility improvements for Extensions in Razor Markup (#9576) * Support for language of parts * Changed casing of "FallBack" to "Fallback" to be consistent with the existing code. * Now tests the type to determine if span should be added. * Remove magic strings and return
    for IHtmlString values This fixes the "string" vs "String" issue in the previous commit and prevents the function returning invalid markup * Fix for test providing string as "object" Co-authored-by: Joe Glombek --- .../Models/PublishedContent/Fallback.cs | 6 ++ .../PublishedContentLanguageVariantTests.cs | 8 ++- .../PublishedValueFallback.cs | 65 ++++++++++++++++--- 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Models/PublishedContent/Fallback.cs b/src/Umbraco.Core/Models/PublishedContent/Fallback.cs index 0434218555..805f14d21b 100644 --- a/src/Umbraco.Core/Models/PublishedContent/Fallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/Fallback.cs @@ -71,5 +71,11 @@ namespace Umbraco.Core.Models.PublishedContent { return GetEnumerator(); } + + public const int DisplayFallbackLanguage = 4; + /// + /// Gets the fallback to tree ancestors policy. + /// + public static Fallback ToDisplayFallbackLanguage => new Fallback(new[] { DisplayFallbackLanguage }); } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs index 636f8502ed..05cf8d01c1 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs @@ -235,7 +235,13 @@ namespace Umbraco.Tests.PublishedContent var value = content.Value("welcomeText", "it", fallback: Fallback.To(Fallback.Language, Fallback.Ancestors)); Assert.AreEqual("Welcome", value); } - + [Test] + public void Can_Get_Content_For_Unpopulated_Requested_Language_With_Fallback_And_Language_Change() + { + var content = Current.UmbracoContext.Content.GetAtRoot().First(); + var value = content.Value("welcomeText", "it", fallback: Fallback.To(Fallback.Language, Fallback.DisplayFallbackLanguage)); + Assert.AreEqual("Welcome", value); + } [Test] public void Do_Not_GetContent_For_Unpopulated_Requested_Language_With_Fallback_Over_That_Loops() { diff --git a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs index 57c3094eaf..b5901fef69 100644 --- a/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Web/Models/PublishedContent/PublishedValueFallback.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Web; +using System.Web.Mvc; using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; @@ -35,6 +37,8 @@ namespace Umbraco.Web.Models.PublishedContent { _variationContextAccessor.ContextualizeVariation(property.PropertyType.Variations, ref culture, ref segment); + var includeFallbackLanguage = fallback.Contains(Fallback.DisplayFallbackLanguage); + foreach (var f in fallback) { switch (f) @@ -45,9 +49,11 @@ namespace Umbraco.Web.Models.PublishedContent value = defaultValue; return true; case Fallback.Language: - if (TryGetValueWithLanguageFallback(property, culture, segment, out value)) + if (TryGetValueWithLanguageFallback(property, culture, segment, out value, includeFallbackLanguage)) return true; break; + case Fallback.DisplayFallbackLanguage: + continue; default: throw NotSupportedFallbackMethod(f, "property"); } @@ -67,6 +73,7 @@ namespace Umbraco.Web.Models.PublishedContent public bool TryGetValue(IPublishedElement content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value) { var propertyType = content.ContentType.GetPropertyType(alias); + var includeFallbackLanguage = fallback.Contains(Fallback.DisplayFallbackLanguage); if (propertyType == null) { value = default; @@ -85,9 +92,11 @@ namespace Umbraco.Web.Models.PublishedContent value = defaultValue; return true; case Fallback.Language: - if (TryGetValueWithLanguageFallback(content, alias, culture, segment, out value)) + if (TryGetValueWithLanguageFallback(content, alias, culture, segment, out value, includeFallbackLanguage)) return true; break; + case Fallback.DisplayFallbackLanguage: + continue; default: throw NotSupportedFallbackMethod(f, "element"); } @@ -107,6 +116,7 @@ namespace Umbraco.Web.Models.PublishedContent public virtual bool TryGetValue(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value, out IPublishedProperty noValueProperty) { noValueProperty = default; + var includeFallbackLanguage = fallback.Contains(Fallback.DisplayFallbackLanguage); var propertyType = content.ContentType.GetPropertyType(alias); if (propertyType != null) @@ -131,13 +141,15 @@ namespace Umbraco.Web.Models.PublishedContent case Fallback.Language: if (propertyType == null) continue; - if (TryGetValueWithLanguageFallback(content, alias, culture, segment, out value)) + if (TryGetValueWithLanguageFallback(content, alias, culture, segment, out value, includeFallbackLanguage)) return true; break; case Fallback.Ancestors: - if (TryGetValueWithAncestorsFallback(content, alias, culture, segment, out value, ref noValueProperty)) + if (TryGetValueWithAncestorsFallback(content, alias, culture, segment, out value, ref noValueProperty, includeFallbackLanguage)) return true; break; + case Fallback.DisplayFallbackLanguage: + continue; default: throw NotSupportedFallbackMethod(f, "content"); } @@ -155,9 +167,10 @@ namespace Umbraco.Web.Models.PublishedContent // tries to get a value, recursing the tree // because we recurse, content may not even have the a property with the specified alias (but only some ancestor) // in case no value was found, noValueProperty contains the first property that was found (which does not have a value) - private bool TryGetValueWithAncestorsFallback(IPublishedContent content, string alias, string culture, string segment, out T value, ref IPublishedProperty noValueProperty) + private bool TryGetValueWithAncestorsFallback(IPublishedContent content, string alias, string culture, string segment, out T value, ref IPublishedProperty noValueProperty, bool includeFallbackLanguage) { IPublishedProperty property; // if we are here, content's property has no value + var originalCulture = culture; do { content = content.Parent; @@ -183,6 +196,10 @@ namespace Umbraco.Web.Models.PublishedContent if (property != null && property.HasValue(culture, segment)) { value = property.Value(culture, segment); + if (includeFallbackLanguage && originalCulture != culture) + { + value = GetMarkUpForFallbackLanguage(culture, value); + } return true; } @@ -191,7 +208,7 @@ namespace Umbraco.Web.Models.PublishedContent } // tries to get a value, falling back onto other languages - private bool TryGetValueWithLanguageFallback(IPublishedProperty property, string culture, string segment, out T value) + private bool TryGetValueWithLanguageFallback(IPublishedProperty property, string culture, string segment, out T value, bool includeFallbackLanguage) { value = default; @@ -217,6 +234,10 @@ namespace Umbraco.Web.Models.PublishedContent if (property.HasValue(culture2, segment)) { value = property.Value(culture2, segment); + if (includeFallbackLanguage && culture2 != culture) + { + value = GetMarkUpForFallbackLanguage(culture2, value); + } return true; } @@ -225,7 +246,7 @@ namespace Umbraco.Web.Models.PublishedContent } // tries to get a value, falling back onto other languages - private bool TryGetValueWithLanguageFallback(IPublishedElement content, string alias, string culture, string segment, out T value) + private bool TryGetValueWithLanguageFallback(IPublishedElement content, string alias, string culture, string segment, out T value, bool includeFallbackLanguage) { value = default; @@ -251,6 +272,10 @@ namespace Umbraco.Web.Models.PublishedContent if (content.HasValue(alias, culture2, segment)) { value = content.Value(alias, culture2, segment); + if (includeFallbackLanguage && culture2 != culture) + { + value = GetMarkUpForFallbackLanguage(culture2, value); + } return true; } @@ -259,7 +284,7 @@ namespace Umbraco.Web.Models.PublishedContent } // tries to get a value, falling back onto other languages - private bool TryGetValueWithLanguageFallback(IPublishedContent content, string alias, string culture, string segment, out T value) + private bool TryGetValueWithLanguageFallback(IPublishedContent content, string alias, string culture, string segment, out T value, bool includeFallbackLanguage) { value = default; @@ -288,11 +313,35 @@ namespace Umbraco.Web.Models.PublishedContent if (content.HasValue(alias, culture2, segment)) { value = content.Value(alias, culture2, segment); + if (includeFallbackLanguage && culture2 != culture) + { + value = GetMarkUpForFallbackLanguage(culture2, value); + + } return true; } language = language2; } } + + private T GetMarkUpForFallbackLanguage(string culture2, T value) + { + var typeOfT = typeof(T); + if (value is string) + { + var newValue = "" + value + ""; + return (T)Convert.ChangeType(newValue, typeof(T)); + } + else if (typeOfT == typeof(IHtmlString)) + { + // we want to return a block element here since the IHtmlString could contain futher block elements + var newValue = "
    " + value + "
    "; + IHtmlString htmlString = new MvcHtmlString(newValue); + return (T)htmlString; + } + + return value; + } } } From 4ac29927deb0183e9e27b42db32eaad661e1536d Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 7 Jul 2021 11:22:50 +0200 Subject: [PATCH 277/289] Bump version to 8.15.0 --- src/SolutionInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 6ef4d6ce85..201213cffa 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -19,4 +19,4 @@ using System.Resources; // these are FYI and changed automatically [assembly: AssemblyFileVersion("8.15.0")] -[assembly: AssemblyInformationalVersion("8.15.0-rc")] +[assembly: AssemblyInformationalVersion("8.15.0")] From 6b6525cc9891cc82d5be8701b91394bdb8ff5a9d Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Fri, 9 Jul 2021 19:33:42 +1200 Subject: [PATCH 278/289] Add whether the current content is published. Allowing for compression of just non published, non media content. --- .../IPropertyCacheCompression.cs | 10 +++++++-- .../IPropertyCacheCompressionOptions.cs | 9 +++++++- .../NoopPropertyCacheCompressionOptions.cs | 2 +- .../PropertyCacheCompression.cs | 10 ++++----- .../ContentSerializationTests.cs | 8 +++---- .../NuCache/DataSource/DatabaseDataSource.cs | 15 +++++++------ .../DataSource/IContentCacheDataSerializer.cs | 4 ++-- .../JsonContentNestedDataSerializer.cs | 4 ++-- .../MsgPackContentNestedDataSerializer.cs | 21 ++++++++++--------- ...gPackContentNestedDataSerializerFactory.cs | 2 +- .../NuCache/PublishedSnapshotService.cs | 2 +- 11 files changed, 52 insertions(+), 35 deletions(-) diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs index 96a559630b..eb89173581 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs @@ -9,7 +9,13 @@ namespace Umbraco.Core.PropertyEditors /// /// public interface IPropertyCacheCompression - { - bool IsCompressed(IReadOnlyContentBase content, string propertyTypeAlias); + { + /// + /// Whether a property on the content is/should be compressed + /// + /// The content + /// The property to compress or not + /// Whether this content is the published version + bool IsCompressed(IReadOnlyContentBase content, string propertyTypeAlias,bool published); } } diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs index 2fa0153f9e..71eb782ee2 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs @@ -4,6 +4,13 @@ namespace Umbraco.Core.PropertyEditors { public interface IPropertyCacheCompressionOptions { - bool IsCompressed(IReadOnlyContentBase content, PropertyType propertyType, IDataEditor dataEditor); + /// + /// Whether a property on the content is/should be compressed + /// + /// The content + /// The property to compress or not + /// The datatype of the property to compress or not + /// Whether this content is the published version + bool IsCompressed(IReadOnlyContentBase content, PropertyType propertyType, IDataEditor dataEditor,bool published); } } diff --git a/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs index 1f12d45769..f566172cd9 100644 --- a/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs +++ b/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs @@ -7,6 +7,6 @@ namespace Umbraco.Core.PropertyEditors /// internal class NoopPropertyCacheCompressionOptions : IPropertyCacheCompressionOptions { - public bool IsCompressed(IReadOnlyContentBase content, PropertyType propertyType, IDataEditor dataEditor) => false; + public bool IsCompressed(IReadOnlyContentBase content, PropertyType propertyType, IDataEditor dataEditor,bool published) => false; } } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs b/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs index 6be21fca7f..be0553ce65 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs @@ -14,13 +14,13 @@ namespace Umbraco.Core.PropertyEditors private readonly IPropertyCacheCompressionOptions _compressionOptions; private readonly IReadOnlyDictionary _contentTypes; private readonly PropertyEditorCollection _propertyEditors; - private readonly ConcurrentDictionary<(int contentTypeId, string propertyAlias), bool> _isCompressedCache; + private readonly ConcurrentDictionary<(int contentTypeId, string propertyAlias,bool published), bool> _isCompressedCache; public PropertyCacheCompression( IPropertyCacheCompressionOptions compressionOptions, IReadOnlyDictionary contentTypes, PropertyEditorCollection propertyEditors, - ConcurrentDictionary<(int, string), bool> compressedStoragePropertyEditorCache) + ConcurrentDictionary<(int, string,bool), bool> compressedStoragePropertyEditorCache) { _compressionOptions = compressionOptions; _contentTypes = contentTypes ?? throw new System.ArgumentNullException(nameof(contentTypes)); @@ -28,9 +28,9 @@ namespace Umbraco.Core.PropertyEditors _isCompressedCache = compressedStoragePropertyEditorCache; } - public bool IsCompressed(IReadOnlyContentBase content, string alias) + public bool IsCompressed(IReadOnlyContentBase content, string alias, bool published) { - var compressedStorage = _isCompressedCache.GetOrAdd((content.ContentTypeId, alias), x => + var compressedStorage = _isCompressedCache.GetOrAdd((content.ContentTypeId, alias, published), x => { if (!_contentTypes.TryGetValue(x.contentTypeId, out var ct)) return false; @@ -40,7 +40,7 @@ namespace Umbraco.Core.PropertyEditors if (!_propertyEditors.TryGet(propertyType.PropertyEditorAlias, out var propertyEditor)) return false; - return _compressionOptions.IsCompressed(content, propertyType, propertyEditor); + return _compressionOptions.IsCompressed(content, propertyType, propertyEditor, published); }); return compressedStorage; diff --git a/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs b/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs index b3543dad1a..9acad53364 100644 --- a/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs +++ b/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs @@ -56,14 +56,14 @@ namespace Umbraco.Tests.PublishedContent var content = Mock.Of(x => x.ContentTypeId == 1); - var json = jsonSerializer.Serialize(content, cacheModel).StringData; - var msgPack = msgPackSerializer.Serialize(content, cacheModel).ByteData; + var json = jsonSerializer.Serialize(content, cacheModel,false).StringData; + var msgPack = msgPackSerializer.Serialize(content, cacheModel, false).ByteData; Console.WriteLine(json); Console.WriteLine(msgPackSerializer.ToJson(msgPack)); - var jsonContent = jsonSerializer.Deserialize(content, json, null); - var msgPackContent = msgPackSerializer.Deserialize(content, null, msgPack); + var jsonContent = jsonSerializer.Deserialize(content, json, null,false); + var msgPackContent = msgPackSerializer.Deserialize(content, null, msgPack,false); CollectionAssert.AreEqual(jsonContent.CultureData.Keys, msgPackContent.CultureData.Keys); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index c112cc6efa..5ec51bffb8 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -393,12 +393,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } else { - var deserializedContent = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw); + bool published = false; + var deserializedContent = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw, published); d = new ContentData { Name = dto.EditName, - Published = false, + Published = published, TemplateId = dto.EditTemplateId, VersionId = dto.VersionId, VersionDate = dto.EditVersionDate, @@ -420,13 +421,14 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } else { - var deserializedContent = serializer.Deserialize(dto, dto.PubData, dto.PubDataRaw); + bool published = true; + var deserializedContent = serializer.Deserialize(dto, dto.PubData, dto.PubDataRaw, published); p = new ContentData { Name = dto.PubName, UrlSegment = deserializedContent.UrlSegment, - Published = true, + Published = published, TemplateId = dto.PubTemplateId, VersionId = dto.VersionId, VersionDate = dto.PubVersionDate, @@ -456,12 +458,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource if (dto.EditData == null && dto.EditDataRaw == null) throw new InvalidOperationException("No data for media " + dto.Id); - var deserializedMedia = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw); + bool published = true; + var deserializedMedia = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw, published); var p = new ContentData { Name = dto.EditName, - Published = true, + Published = published, TemplateId = -1, VersionId = dto.VersionId, VersionDate = dto.EditVersionDate, diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs index d1a83d8452..6c37046e8b 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs @@ -14,12 +14,12 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// /// Deserialize the data into a /// - ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData); + ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData, bool published); /// /// Serializes the /// - ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model); + ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model,bool published); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs index 21cd0bf763..0cc10f5f98 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs @@ -24,7 +24,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource DateFormatString = "o" }; private readonly JsonNameTable _propertyNameTable = new DefaultJsonNameTable(); - public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData) + public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData,bool published) { if (stringData == null && byteData != null) throw new NotSupportedException($"{typeof(JsonContentNestedDataSerializer)} does not support byte[] serialization"); @@ -39,7 +39,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } } - public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model) + public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model,bool published) { // note that numeric values (which are Int32) are serialized without their // type (eg "value":1234) and JsonConvert by default deserializes them as Int64 diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index 6ae872ef69..f69232aad3 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -39,7 +39,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource _options = defaultOptions .WithResolver(resolver) - .WithCompression(MessagePackCompression.Lz4BlockArray); + .WithCompression(MessagePackCompression.Lz4BlockArray) + .WithSecurity(MessagePackSecurity.UntrustedData); } public string ToJson(byte[] bin) @@ -48,12 +49,12 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return json; } - public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData) + public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData,bool published) { if (byteData != null) { var cacheModel = MessagePackSerializer.Deserialize(byteData, _options); - Expand(content, cacheModel); + Expand(content, cacheModel, published); return cacheModel; } else if (stringData != null) @@ -61,7 +62,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // NOTE: We don't really support strings but it's possible if manually used (i.e. tests) var bin = Convert.FromBase64String(stringData); var cacheModel = MessagePackSerializer.Deserialize(bin, _options); - Expand(content, cacheModel); + Expand(content, cacheModel,published); return cacheModel; } else @@ -70,9 +71,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } } - public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model) + public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model,bool published) { - Compress(content, model); + Compress(content, model, published); var bytes = MessagePackSerializer.Serialize(model, _options); return new ContentCacheDataSerializationResult(null, bytes); } @@ -88,11 +89,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// read/decompressed as a string to be displayed on the front-end. This allows for potentially a significant /// memory savings but could also affect performance of first rendering pages while decompression occurs. /// - private void Compress(IReadOnlyContentBase content, ContentCacheDataModel model) + private void Compress(IReadOnlyContentBase content, ContentCacheDataModel model,bool published) { foreach(var propertyAliasToData in model.PropertyData) { - if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key)) + if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key,published)) { foreach(var property in propertyAliasToData.Value.Where(x => x.Value != null && x.Value is string)) { @@ -106,11 +107,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// Used during deserialization to map the property data as lazy or expand the value /// /// - private void Expand(IReadOnlyContentBase content, ContentCacheDataModel nestedData) + private void Expand(IReadOnlyContentBase content, ContentCacheDataModel nestedData,bool published) { foreach (var propertyAliasToData in nestedData.PropertyData) { - if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key)) + if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key,published)) { foreach (var property in propertyAliasToData.Value.Where(x => x.Value != null)) { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs index fcc3fa2bb8..29378caf0f 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs @@ -13,7 +13,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource private readonly IMemberTypeService _memberTypeService; private readonly PropertyEditorCollection _propertyEditors; private readonly IPropertyCacheCompressionOptions _compressionOptions; - private readonly ConcurrentDictionary<(int, string), bool> _isCompressedCache = new ConcurrentDictionary<(int, string), bool>(); + private readonly ConcurrentDictionary<(int, string,bool), bool> _isCompressedCache = new ConcurrentDictionary<(int, string,bool), bool>(); public MsgPackContentNestedDataSerializerFactory( IContentTypeService contentTypeService, diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index f9c25b7b35..7ca425aad9 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1491,7 +1491,7 @@ namespace Umbraco.Web.PublishedCache.NuCache UrlSegment = content.GetUrlSegment(_urlSegmentProviders) }; - var serialized = serializer.Serialize(ReadOnlyContentBaseAdapter.Create(content), contentCacheData); + var serialized = serializer.Serialize(ReadOnlyContentBaseAdapter.Create(content), contentCacheData,published); var dto = new ContentNuDto { From 044c900c2ba52428b7acecb8d15fc0769c5b9eaa Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Fri, 9 Jul 2021 20:14:35 +1200 Subject: [PATCH 279/289] Make IPropertyCacheCompressionOptions useful out of the box. key = "Umbraco.Web.PublishedCache.NuCache.CompressUnPublishedContent" value = "true" will compress all ntext properties on unpublished content --- ...dContentPropertyCacheCompressionOptions.cs | 29 +++++++++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../PublishedCache/NuCache/NuCacheComposer.cs | 12 ++++++-- .../NuCache/NuCacheSerializerComponent.cs | 1 + 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs diff --git a/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs new file mode 100644 index 0000000000..51433e476e --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Models; + +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Compress large, non published text properties + /// + internal class UnPublishedContentPropertyCacheCompressionOptions : IPropertyCacheCompressionOptions + { + public bool IsCompressed(IReadOnlyContentBase content, PropertyType propertyType, IDataEditor dataEditor, bool published) + { + if (published) + { + return false; + } + if (propertyType.SupportsPublishing && propertyType.ValueStorageType == ValueStorageType.Ntext) + { + //Only compress non published content that supports publishing and the property is text + return true; + } + return false; + } + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 931cef070f..f6a523441e 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -171,6 +171,7 @@ + diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs index c6b214102b..4b3d7b20fa 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs @@ -26,8 +26,16 @@ namespace Umbraco.Web.PublishedCache.NuCache { composition.RegisterUnique(); } - - composition.RegisterUnique(); + var unPublishedContentCompression = ConfigurationManager.AppSettings[NuCacheSerializerComponent.Nucache_UnPublishedContentCompression_Key]; + if ("MsgPack" == serializer && "true" == unPublishedContentCompression) + { + composition.RegisterUnique(); + } + else + { + composition.RegisterUnique(); + } + composition.RegisterUnique(factory => new ContentDataSerializer(new DictionaryOfPropertyDataSerializer())); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs index a1d3ed2b12..c2d24b16ac 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs @@ -16,6 +16,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public class NuCacheSerializerComponent : IComponent { internal const string Nucache_Serializer_Key = "Umbraco.Web.PublishedCache.NuCache.Serializer"; + internal const string Nucache_UnPublishedContentCompression_Key = "Umbraco.Web.PublishedCache.NuCache.CompressUnPublishedContent"; private const string JSON_SERIALIZER_VALUE = "JSON"; private readonly Lazy _service; private readonly IKeyValueService _keyValueService; From b119201480da1f99bf463776f71d2e73901f2aef Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Fri, 9 Jul 2021 20:55:03 +1200 Subject: [PATCH 280/289] fix serializer swap message --- .../PublishedCache/NuCache/NuCacheSerializerComponent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs index c2d24b16ac..41afb2b781 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs @@ -55,7 +55,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { _profilingLogger.Warn($"Database NuCache was serialized using {currentSerializer}. Currently configured NuCache serializer {serializer}. Rebuilding Nucache"); - using (_profilingLogger.TraceDuration($"Rebuilding NuCache database with {currentSerializer} serializer")) + using (_profilingLogger.TraceDuration($"Rebuilding NuCache database with {serializer} serializer")) { _service.Value.Rebuild(); _keyValueService.SetValue(Nucache_Serializer_Key, serializer); From 6a8ed8012cc282e87b8d55c23f518f9991c0daba Mon Sep 17 00:00:00 2001 From: nzdev <834725+nzdev@users.noreply.github.com> Date: Fri, 9 Jul 2021 21:22:30 +1200 Subject: [PATCH 281/289] simplify --- .../UnPublishedContentPropertyCacheCompressionOptions.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs index 51433e476e..7a5212a985 100644 --- a/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs +++ b/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs @@ -14,11 +14,7 @@ namespace Umbraco.Core.PropertyEditors { public bool IsCompressed(IReadOnlyContentBase content, PropertyType propertyType, IDataEditor dataEditor, bool published) { - if (published) - { - return false; - } - if (propertyType.SupportsPublishing && propertyType.ValueStorageType == ValueStorageType.Ntext) + if (!published && propertyType.SupportsPublishing && propertyType.ValueStorageType == ValueStorageType.Ntext) { //Only compress non published content that supports publishing and the property is text return true; From 5e8b4572a0a322b8cfbe27fea3038367c76415fd Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 9 Jul 2021 09:40:31 -0600 Subject: [PATCH 282/289] Ensures that the content nu data column is updated to support null during migration. --- .../Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs | 3 +++ .../PublishedCache/NuCache/NuCacheSerializerComponent.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs index 3eab1a812e..9eeaa67189 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs @@ -16,6 +16,9 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_15_0 var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); AddColumnIfNotExists(columns, "dataRaw"); + + // allow null + AlterColumn(Constants.DatabaseSchema.Tables.NodeData, "data"); } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs index a1d3ed2b12..fcbb5a3715 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs @@ -54,7 +54,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { _profilingLogger.Warn($"Database NuCache was serialized using {currentSerializer}. Currently configured NuCache serializer {serializer}. Rebuilding Nucache"); - using (_profilingLogger.TraceDuration($"Rebuilding NuCache database with {currentSerializer} serializer")) + using (_profilingLogger.TraceDuration($"Rebuilding NuCache database with {serializer} serializer")) { _service.Value.Rebuild(); _keyValueService.SetValue(Nucache_Serializer_Key, serializer); From 7dc03256edf1bbe15b5b44c5d44a033369ce0e07 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 9 Jul 2021 12:05:19 -0600 Subject: [PATCH 283/289] Fixes SQLCE migration for nucache table. --- .../Create/Table/CreateTableOfDtoBuilder.cs | 2 + .../V_8_15_0/AddCmsContentNuByteColumn.cs | 47 +++++++++++++++++-- .../SqlSyntax/SqlSyntaxProviderBase.cs | 15 +++--- 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs index 4b73e9435d..a5e2fca1f7 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs @@ -9,6 +9,8 @@ namespace Umbraco.Core.Migrations.Expressions.Create.Table public class CreateTableOfDtoBuilder : IExecutableBuilder { private readonly IMigrationContext _context; + + // TODO: This doesn't do anything. private readonly DatabaseType[] _supportedDatabaseTypes; public CreateTableOfDtoBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs index 9eeaa67189..5217fc9870 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs @@ -1,5 +1,10 @@ -using System.Linq; +using NPoco; +using System.Data; +using System.Linq; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.PropertyEditors; namespace Umbraco.Core.Migrations.Upgrade.V_8_15_0 { @@ -13,12 +18,46 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_15_0 public override void Migrate() { + // allow null for the `data` field + if (DatabaseType.IsSqlCe()) + { + // SQLCE does not support altering NTEXT, so we have to jump through some hoops to do it + // All column ordering must remain the same as what is defined in the DTO so we need to create a temp table, + // drop orig and then re-create/copy. + Create.Table(withoutKeysAndIndexes: true).Do(); + Execute.Sql($"INSERT INTO [{TempTableName}] SELECT nodeId, published, data, rv FROM [{Constants.DatabaseSchema.Tables.NodeData}]").Do(); + Delete.Table(Constants.DatabaseSchema.Tables.NodeData).Do(); + Create.Table().Do(); + Execute.Sql($"INSERT INTO [{Constants.DatabaseSchema.Tables.NodeData}] SELECT nodeId, published, data, rv, NULL FROM [{TempTableName}]").Do(); + } + else + { + AlterColumn(Constants.DatabaseSchema.Tables.NodeData, "data"); + } + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - AddColumnIfNotExists(columns, "dataRaw"); + } - // allow null - AlterColumn(Constants.DatabaseSchema.Tables.NodeData, "data"); + private const string TempTableName = Constants.DatabaseSchema.TableNamePrefix + "cms" + "ContentNuTEMP"; + + [TableName(TempTableName)] + [ExplicitColumns] + private class ContentNuDtoTemp + { + [Column("nodeId")] + public int NodeId { get; set; } + + [Column("published")] + public bool Published { get; set; } + + [Column("data")] + [SpecialDbType(SpecialDbTypes.NTEXT)] + [NullSetting(NullSetting = NullSettings.Null)] + public string Data { get; set; } + + [Column("rv")] + public long Rv { get; set; } } } } diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 4d6b2eeea1..0d2d7aeb21 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -352,7 +352,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax sql.Append(" "); sql.Append(FormatIdentity(column)); - var isNullable = column.IsNullable; + //var isNullable = column.IsNullable; //var constraint = FormatConstraint(column)?.TrimStart("CONSTRAINT "); //var hasConstraint = !string.IsNullOrWhiteSpace(constraint); @@ -360,11 +360,14 @@ namespace Umbraco.Core.Persistence.SqlSyntax //var defaultValue = FormatDefaultValue(column); //var hasDefaultValue = !string.IsNullOrWhiteSpace(defaultValue); - if (isNullable /*&& !hasConstraint && !hasDefaultValue*/) - { - sqls = Enumerable.Empty(); - return sql.ToString(); - } + // TODO: This used to exit if nullable but that means this would never work + // to return SQL if the column was nullable?!? I don't get it. This was here + // 4 years ago, I've removed it so that this works for nullable columns. + //if (isNullable /*&& !hasConstraint && !hasDefaultValue*/) + //{ + // sqls = Enumerable.Empty(); + // return sql.ToString(); + //} var msql = new List(); sqls = msql; From 93f5dd76317b437f14f37db49f1ac39ace300c39 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 9 Jul 2021 13:35:43 -0600 Subject: [PATCH 284/289] Changes bulk copy timeout to be infinite. Changes bulk copy to not allocate an arbitrary array. --- .../NPocoDatabaseExtensions-Bulk.cs | 35 ++++++++++++++----- .../Persistence/PocoDataDataReader.cs | 5 +-- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs index bff682d095..77cc0d6601 100644 --- a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs +++ b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs @@ -62,26 +62,33 @@ namespace Umbraco.Core.Persistence /// The number of records that were inserted. public static int BulkInsertRecords(this IUmbracoDatabase database, IEnumerable records, bool useNativeBulkInsert = true) { - var recordsA = records.ToArray(); - if (recordsA.Length == 0) return 0; + if (!records.Any()) return 0; var pocoData = database.PocoDataFactory.ForType(typeof(T)); if (pocoData == null) throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); if (database.DatabaseType.IsSqlCe()) { - if (useNativeBulkInsert) return BulkInsertRecordsSqlCe(database, pocoData, recordsA); + if (useNativeBulkInsert) + { + return BulkInsertRecordsSqlCe(database, pocoData, records); + } + // else, no other choice - foreach (var record in recordsA) + var count = 0; + foreach (var record in records) + { database.Insert(record); - return recordsA.Length; + count++; + } + return count; } if (database.DatabaseType.IsSqlServer()) { return useNativeBulkInsert && database.DatabaseType.IsSqlServer2008OrLater() - ? BulkInsertRecordsSqlServer(database, pocoData, recordsA) - : BulkInsertRecordsWithCommands(database, recordsA); + ? BulkInsertRecordsSqlServer(database, pocoData, records) + : BulkInsertRecordsWithCommands(database, records.ToArray()); } throw new NotSupportedException(); } @@ -96,7 +103,9 @@ namespace Umbraco.Core.Persistence private static int BulkInsertRecordsWithCommands(IUmbracoDatabase database, T[] records) { foreach (var command in database.GenerateBulkInsertCommands(records)) + { command.ExecuteNonQuery(); + } return records.Length; // what else? } @@ -241,6 +250,10 @@ namespace Umbraco.Core.Persistence /// The number of records that were inserted. internal static int BulkInsertRecordsSqlServer(IUmbracoDatabase database, PocoData pocoData, IEnumerable records) { + // TODO: The main reason this exists is because the NPoco InsertBulk method doesn't return the number of items. + // It is worth investigating the performance of this vs NPoco's because we use a custom BulkDataReader + // which in theory should be more efficient than NPocos way of building up an in-memory DataTable. + // create command against the original database.Connection using (var command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty)) { @@ -252,7 +265,13 @@ namespace Umbraco.Core.Persistence var syntax = database.SqlContext.SqlSyntax as SqlServerSyntaxProvider; if (syntax == null) throw new NotSupportedException("SqlSyntax must be SqlServerSyntaxProvider."); - using (var copy = new SqlBulkCopy(tConnection, SqlBulkCopyOptions.Default, tTransaction) { BulkCopyTimeout = 10000, DestinationTableName = tableName }) + using (var copy = new SqlBulkCopy(tConnection, SqlBulkCopyOptions.Default, tTransaction) + { + BulkCopyTimeout = 0, // 0 = no bulk copy timeout. If a timeout occurs it will be an connection/command timeout. + DestinationTableName = tableName, + // be consistent with NPoco: https://github.com/schotime/NPoco/blob/5117a55fde57547e928246c044fd40bd00b2d7d1/src/NPoco.SqlServer/SqlBulkCopyHelper.cs#L50 + BatchSize = 4096 + }) 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 diff --git a/src/Umbraco.Core/Persistence/PocoDataDataReader.cs b/src/Umbraco.Core/Persistence/PocoDataDataReader.cs index 460a4d3d90..397d903cc2 100644 --- a/src/Umbraco.Core/Persistence/PocoDataDataReader.cs +++ b/src/Umbraco.Core/Persistence/PocoDataDataReader.cs @@ -40,9 +40,10 @@ namespace Umbraco.Core.Persistence _tableDefinition = DefinitionFactory.GetTableDefinition(pd.Type, sqlSyntaxProvider); if (_tableDefinition == null) throw new InvalidOperationException("No table definition found for type " + pd.Type); - // only real columns, exclude result columns + // only real columns, exclude result/computed columns + // Like NPoco does: https://github.com/schotime/NPoco/blob/5117a55fde57547e928246c044fd40bd00b2d7d1/src/NPoco.SqlServer/SqlBulkCopyHelper.cs#L59 _readerColumns = pd.Columns - .Where(x => x.Value.ResultColumn == false) + .Where(x => x.Value.ResultColumn == false && x.Value.ComputedColumn == false) .Select(x => x.Value) .ToArray(); From a37f6c604f399b1c6f387adcc97fa71d0dc9abea Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Sat, 10 Jul 2021 14:25:26 +0200 Subject: [PATCH 285/289] Inconsistent formatting --- .../IPropertyCacheCompression.cs | 4 ++-- .../IPropertyCacheCompressionOptions.cs | 2 +- .../NoopPropertyCacheCompressionOptions.cs | 2 +- ...edContentPropertyCacheCompressionOptions.cs | 7 +------ .../ContentSerializationTests.cs | 6 +++--- .../DataSource/IContentCacheDataSerializer.cs | 4 ++-- .../JsonContentNestedDataSerializer.cs | 4 ++-- .../MsgPackContentNestedDataSerializer.cs | 18 +++++++++++------- ...sgPackContentNestedDataSerializerFactory.cs | 2 +- .../PublishedCache/NuCache/NuCacheComposer.cs | 8 ++++---- .../NuCache/PublishedSnapshotService.cs | 8 ++++---- 11 files changed, 32 insertions(+), 33 deletions(-) diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs index eb89173581..69d10a2276 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.PropertyEditors /// Determines if a property type's value should be compressed in memory /// /// - /// + /// /// public interface IPropertyCacheCompression { @@ -16,6 +16,6 @@ namespace Umbraco.Core.PropertyEditors /// The content /// The property to compress or not /// Whether this content is the published version - bool IsCompressed(IReadOnlyContentBase content, string propertyTypeAlias,bool published); + bool IsCompressed(IReadOnlyContentBase content, string propertyTypeAlias, bool published); } } diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs index 71eb782ee2..e4603d55e3 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs @@ -11,6 +11,6 @@ namespace Umbraco.Core.PropertyEditors /// The property to compress or not /// The datatype of the property to compress or not /// Whether this content is the published version - bool IsCompressed(IReadOnlyContentBase content, PropertyType propertyType, IDataEditor dataEditor,bool published); + bool IsCompressed(IReadOnlyContentBase content, PropertyType propertyType, IDataEditor dataEditor, bool published); } } diff --git a/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs index f566172cd9..ea3b8d8a2e 100644 --- a/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs +++ b/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs @@ -7,6 +7,6 @@ namespace Umbraco.Core.PropertyEditors /// internal class NoopPropertyCacheCompressionOptions : IPropertyCacheCompressionOptions { - public bool IsCompressed(IReadOnlyContentBase content, PropertyType propertyType, IDataEditor dataEditor,bool published) => false; + public bool IsCompressed(IReadOnlyContentBase content, PropertyType propertyType, IDataEditor dataEditor, bool published) => false; } } diff --git a/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs index 7a5212a985..ece25479cc 100644 --- a/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs +++ b/src/Umbraco.Core/PropertyEditors/UnPublishedContentPropertyCacheCompressionOptions.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Umbraco.Core.Models; +using Umbraco.Core.Models; namespace Umbraco.Core.PropertyEditors { diff --git a/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs b/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs index 9acad53364..9a44cf35f9 100644 --- a/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs +++ b/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs @@ -56,14 +56,14 @@ namespace Umbraco.Tests.PublishedContent var content = Mock.Of(x => x.ContentTypeId == 1); - var json = jsonSerializer.Serialize(content, cacheModel,false).StringData; + var json = jsonSerializer.Serialize(content, cacheModel, false).StringData; var msgPack = msgPackSerializer.Serialize(content, cacheModel, false).ByteData; Console.WriteLine(json); Console.WriteLine(msgPackSerializer.ToJson(msgPack)); - var jsonContent = jsonSerializer.Deserialize(content, json, null,false); - var msgPackContent = msgPackSerializer.Deserialize(content, null, msgPack,false); + var jsonContent = jsonSerializer.Deserialize(content, json, null, false); + var msgPackContent = msgPackSerializer.Deserialize(content, null, msgPack, false); CollectionAssert.AreEqual(jsonContent.CultureData.Keys, msgPackContent.CultureData.Keys); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs index 6c37046e8b..4bdf7e9665 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs @@ -17,9 +17,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData, bool published); /// - /// Serializes the + /// Serializes the /// - ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model,bool published); + ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model, bool published); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs index 0cc10f5f98..358561cabd 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs @@ -24,7 +24,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource DateFormatString = "o" }; private readonly JsonNameTable _propertyNameTable = new DefaultJsonNameTable(); - public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData,bool published) + public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData, bool published) { if (stringData == null && byteData != null) throw new NotSupportedException($"{typeof(JsonContentNestedDataSerializer)} does not support byte[] serialization"); @@ -39,7 +39,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } } - public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model,bool published) + public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model, bool published) { // note that numeric values (which are Int32) are serialized without their // type (eg "value":1234) and JsonConvert by default deserializes them as Int64 diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs index f69232aad3..f1400382e6 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -40,7 +40,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource _options = defaultOptions .WithResolver(resolver) .WithCompression(MessagePackCompression.Lz4BlockArray) - .WithSecurity(MessagePackSecurity.UntrustedData); + .WithSecurity(MessagePackSecurity.UntrustedData); } public string ToJson(byte[] bin) @@ -49,7 +49,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return json; } - public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData,bool published) + public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData, bool published) { if (byteData != null) { @@ -62,7 +62,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // NOTE: We don't really support strings but it's possible if manually used (i.e. tests) var bin = Convert.FromBase64String(stringData); var cacheModel = MessagePackSerializer.Deserialize(bin, _options); - Expand(content, cacheModel,published); + Expand(content, cacheModel, published); return cacheModel; } else @@ -71,7 +71,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource } } - public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model,bool published) + public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model, bool published) { Compress(content, model, published); var bytes = MessagePackSerializer.Serialize(model, _options); @@ -81,7 +81,9 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// /// Used during serialization to compress properties /// + /// /// + /// /// /// This will essentially 'double compress' property data. The MsgPack data as a whole will already be compressed /// but this will go a step further and double compress property data so that it is stored in the nucache file @@ -89,11 +91,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// read/decompressed as a string to be displayed on the front-end. This allows for potentially a significant /// memory savings but could also affect performance of first rendering pages while decompression occurs. /// - private void Compress(IReadOnlyContentBase content, ContentCacheDataModel model,bool published) + private void Compress(IReadOnlyContentBase content, ContentCacheDataModel model, bool published) { foreach(var propertyAliasToData in model.PropertyData) { - if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key,published)) + if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key, published)) { foreach(var property in propertyAliasToData.Value.Where(x => x.Value != null && x.Value is string)) { @@ -106,8 +108,10 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// /// Used during deserialization to map the property data as lazy or expand the value /// + /// /// - private void Expand(IReadOnlyContentBase content, ContentCacheDataModel nestedData,bool published) + /// + private void Expand(IReadOnlyContentBase content, ContentCacheDataModel nestedData, bool published) { foreach (var propertyAliasToData in nestedData.PropertyData) { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs index 29378caf0f..5245df8353 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs @@ -13,7 +13,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource private readonly IMemberTypeService _memberTypeService; private readonly PropertyEditorCollection _propertyEditors; private readonly IPropertyCacheCompressionOptions _compressionOptions; - private readonly ConcurrentDictionary<(int, string,bool), bool> _isCompressedCache = new ConcurrentDictionary<(int, string,bool), bool>(); + private readonly ConcurrentDictionary<(int, string,bool), bool> _isCompressedCache = new ConcurrentDictionary<(int, string, bool), bool>(); public MsgPackContentNestedDataSerializerFactory( IContentTypeService contentTypeService, diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs index 4b3d7b20fa..dd3907e254 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs @@ -24,10 +24,10 @@ namespace Umbraco.Web.PublishedCache.NuCache } else { - composition.RegisterUnique(); + composition.RegisterUnique(); } - var unPublishedContentCompression = ConfigurationManager.AppSettings[NuCacheSerializerComponent.Nucache_UnPublishedContentCompression_Key]; - if ("MsgPack" == serializer && "true" == unPublishedContentCompression) + var unPublishedContentCompression = ConfigurationManager.AppSettings[NuCacheSerializerComponent.Nucache_UnPublishedContentCompression_Key]; + if (serializer == "MsgPack" && unPublishedContentCompression == "true") { composition.RegisterUnique(); } @@ -35,7 +35,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { composition.RegisterUnique(); } - + composition.RegisterUnique(factory => new ContentDataSerializer(new DictionaryOfPropertyDataSerializer())); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 7ca425aad9..f3373dab6c 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -89,7 +89,7 @@ namespace Umbraco.Web.PublishedCache.NuCache IPublishedModelFactory publishedModelFactory, UrlSegmentProviderCollection urlSegmentProviders, ISyncBootStateAccessor syncBootStateAccessor, - IContentCacheDataSerializerFactory contentCacheDataSerializerFactory, + IContentCacheDataSerializerFactory contentCacheDataSerializerFactory, ContentDataSerializer contentDataSerializer = null) : base(publishedSnapshotAccessor, variationContextAccessor) { @@ -262,7 +262,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (!okMedia) _logger.Warn("Loading media from local db raised warnings, will reload from database."); } - + if (!okContent) LockAndLoadContent(scope => LoadContentFromDatabaseLocked(scope, true)); @@ -1168,7 +1168,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (Volatile.Read(ref _isReady) == false) { throw new InvalidOperationException("The published snapshot service has not properly initialized."); - } + } var preview = previewToken.IsNullOrWhiteSpace() == false; return new PublishedSnapshot(this, preview); @@ -1491,7 +1491,7 @@ namespace Umbraco.Web.PublishedCache.NuCache UrlSegment = content.GetUrlSegment(_urlSegmentProviders) }; - var serialized = serializer.Serialize(ReadOnlyContentBaseAdapter.Create(content), contentCacheData,published); + var serialized = serializer.Serialize(ReadOnlyContentBaseAdapter.Create(content), contentCacheData, published); var dto = new ContentNuDto { From 2a6bdf73300b358ef5be2b1e4b3a480d2e790ef8 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 12 Jul 2021 10:48:21 +0200 Subject: [PATCH 286/289] Fixes umbraco/Umbraco-CMS#10402 We added an overload that would conflict for people with existing templates, that was a bit too premature. Disabled for now with a note to look into it for v9 again. --- src/Umbraco.Web/UrlHelperRenderExtensions.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web/UrlHelperRenderExtensions.cs b/src/Umbraco.Web/UrlHelperRenderExtensions.cs index 2c547c841e..33e657e7cf 100644 --- a/src/Umbraco.Web/UrlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/UrlHelperRenderExtensions.cs @@ -281,14 +281,15 @@ namespace Umbraco.Web return CreateHtmlString(url, htmlEncode); } - public static IHtmlString GetCropUrl(this UrlHelper urlHelper, ImageCropperValue imageCropperValue, string cropAlias, bool htmlEncode = true) - { - if (imageCropperValue == null || string.IsNullOrEmpty(imageCropperValue.Src)) return EmptyHtmlString; - - var url = imageCropperValue.Src.GetCropUrl(imageCropperValue, cropAlias: cropAlias, useCropDimensions: true); - - return CreateHtmlString(url, htmlEncode); - } + // TODO: enable again in v9 and make sure to document that `@Url.GetCropUrl(Model.Property, cropAlias: "Featured")` needs to be updated - see https://github.com/umbraco/Umbraco-CMS/pull/10527 for alternatives + // public static IHtmlString GetCropUrl(this UrlHelper urlHelper, ImageCropperValue imageCropperValue, string cropAlias, bool htmlEncode = true) + // { + // if (imageCropperValue == null || string.IsNullOrEmpty(imageCropperValue.Src)) return EmptyHtmlString; + // + // var url = imageCropperValue.Src.GetCropUrl(imageCropperValue, cropAlias: cropAlias, useCropDimensions: true); + // + // return CreateHtmlString(url, htmlEncode); + // } public static IHtmlString GetCropUrl(this UrlHelper urlHelper, ImageCropperValue imageCropperValue, From 87a7e84cbabdf5f692e52f2d73c6b474092bfaab Mon Sep 17 00:00:00 2001 From: Jan Skovgaard <1932158+BatJan@users.noreply.github.com> Date: Tue, 13 Jul 2021 09:57:20 +0200 Subject: [PATCH 287/289] Bugfix: #10414 - Validation message doesn't disappear once the issue is fixed (#10581) * Add missing focus styling * Fix issue where validation message does not disappear when all chars are removed Co-authored-by: BatJan Co-authored-by: Jan Skovgaard Olsen --- .../src/views/propertyeditors/textbox/textbox.controller.js | 4 ++++ .../src/views/propertyeditors/textbox/textbox.html | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js index b7c740e749..3e4539c6ae 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js @@ -31,6 +31,10 @@ function textboxController($scope, validationMessageService) { checkLengthVadility(); $scope.nearMaxLimit = $scope.validLength && $scope.charsCount > Math.max($scope.maxChars*.8, $scope.maxChars-25); } + else { + $scope.charsCount = 0; + checkLengthVadility(); + } } $scope.model.onValueChanged = $scope.change; $scope.change(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html index 5e135ea7d9..1f1131c43f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html @@ -10,7 +10,7 @@ aria-required="{{model.validation.mandatory}}" aria-invalid="False" ng-trim="false" - ng-keyup="change()" /> + ng-change="change()" />

    {{model.label}} {{textboxFieldForm.textbox.errorMsg}}

    From 8ebb37a5963c020bd49f1bda940efd6776f069fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 9 Jul 2021 11:29:01 +0200 Subject: [PATCH 288/289] add vm.model.onValueChanged callback --- .../umbMediaPicker3PropertyEditor.component.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js index 96f3126288..02b9f0b928 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js @@ -98,6 +98,10 @@ vm.model.value.forEach(mediaEntry => updateMediaEntryData(mediaEntry)); + // set the onValueChanged callback, this will tell us if the media picker model changed on the server + // once the data is submitted. If so we need to re-initialize + vm.model.onValueChanged = onServerValueChanged; + userService.getCurrentUser().then(function (userData) { if (!vm.model.config.startNodeId) { @@ -120,6 +124,15 @@ }; + function onServerValueChanged(newVal, oldVal) { + if(newVal === null || !Array.isArray(newVal)) { + newVal = []; + vm.model.value = newVal; + } + + vm.model.value.forEach(mediaEntry => updateMediaEntryData(mediaEntry)); + } + function setDirty() { if (vm.propertyForm) { vm.propertyForm.$setDirty(); @@ -259,7 +272,7 @@ function setActiveMedia(mediaEntryOrNull) { vm.activeMediaEntry = mediaEntryOrNull; } - + function editMedia(mediaEntry, options, $event) { if($event) From 3def2ee2ad2897e1d1c6d9b1d077e4c4652d01d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 9 Jul 2021 11:29:01 +0200 Subject: [PATCH 289/289] add vm.model.onValueChanged callback (cherry picked from commit 8ebb37a5963c020bd49f1bda940efd6776f069fc) # Conflicts: # src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js --- .../umbMediaPicker3PropertyEditor.component.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js index 675381d46e..3ad3309085 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js @@ -93,6 +93,10 @@ vm.model.value.forEach(mediaEntry => updateMediaEntryData(mediaEntry)); + // set the onValueChanged callback, this will tell us if the media picker model changed on the server + // once the data is submitted. If so we need to re-initialize + vm.model.onValueChanged = onServerValueChanged; + userService.getCurrentUser().then(function (userData) { if (!vm.model.config.startNodeId) { @@ -115,6 +119,15 @@ }; + function onServerValueChanged(newVal, oldVal) { + if(newVal === null || !Array.isArray(newVal)) { + newVal = []; + vm.model.value = newVal; + } + + vm.model.value.forEach(mediaEntry => updateMediaEntryData(mediaEntry)); + } + function setDirty() { if (vm.propertyForm) { vm.propertyForm.$setDirty();