From e1b4aebb0faa0fe479983d5940605f9e5c00296a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 22 Aug 2023 11:27:16 +0200 Subject: [PATCH 1/3] directly render labels without angularJS template code (#14700) --- .../src/common/services/blockeditormodelobject.service.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 20661d5d1c..606d16ad2d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -644,6 +644,12 @@ blockObject.index = 0; if (blockObject.config.label && blockObject.config.label !== "" && blockObject.config.unsupported !== true) { + + // If the label does not contain any AngularJS template, then the MutationObserver wont give us any updates. To ensure labels without angular JS template code, we will just set the label directly for ones without '{{': + if(blockObject.config.label.indexOf("{{") === -1) { + blockObject.label = blockObject.config.label; + } + var labelElement = $('
', { text: blockObject.config.label}); var observer = new MutationObserver(function(mutations) { @@ -673,6 +679,7 @@ this.__labelScope = Object.assign(this.__labelScope, labelVars); $compile(labelElement.contents())(this.__labelScope); + }.bind(blockObject) } else { blockObject.__renderLabel = function() {}; From 9fd0b1986c39344f8f7331b5c8b69cca76d228d4 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 22 Aug 2023 12:28:01 +0100 Subject: [PATCH 2/3] Updated JSON schema reference for Umbraco Forms. (#14701) --- src/JsonSchema/JsonSchema.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonSchema/JsonSchema.csproj b/src/JsonSchema/JsonSchema.csproj index edf62516a5..07a29835de 100644 --- a/src/JsonSchema/JsonSchema.csproj +++ b/src/JsonSchema/JsonSchema.csproj @@ -13,6 +13,6 @@ - + From 3c37653012d1d4e43c74c605cdd5b786c51a5026 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 23 Aug 2023 10:09:43 +0200 Subject: [PATCH 3/3] Fix exceptions in Slider and Tags property value converters (#13782) * Fix IndexOutOfRangeException when converting single value to range in SliderValueConverter * Fix NullReferenceException while deserializing empty value in TagsValueConverter * Use invariant decimal parsing * Handle converting from slider to single value * Fix parsing range as single value * Make Handle methods autonomous --------- Co-authored-by: nikolajlauridsen --- .../Cache/DataTypeCacheRefresher.cs | 4 - .../ValueConverters/SliderValueConverter.cs | 127 ++++++++++++------ .../ValueConverters/TagsValueConverter.cs | 78 +++++------ 3 files changed, 125 insertions(+), 84 deletions(-) diff --git a/src/Umbraco.Core/Cache/DataTypeCacheRefresher.cs b/src/Umbraco.Core/Cache/DataTypeCacheRefresher.cs index ea661c5498..394630fa64 100644 --- a/src/Umbraco.Core/Cache/DataTypeCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/DataTypeCacheRefresher.cs @@ -88,10 +88,6 @@ public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase _publishedSnapshotService.Notify(payloads)); diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs index 76f5b62265..14e80952f4 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Globalization; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Services; @@ -6,74 +6,123 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; +/// +/// The slider property value converter. +/// +/// [DefaultPropertyValueConverter] public class SliderValueConverter : PropertyValueConverterBase { - private static readonly ConcurrentDictionary Storages = new(); - private readonly IDataTypeService _dataTypeService; + /// + /// Initializes a new instance of the class. + /// + public SliderValueConverter() + { } - public SliderValueConverter(IDataTypeService dataTypeService) => _dataTypeService = - dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); + /// + /// Initializes a new instance of the class. + /// + /// The data type service. + [Obsolete("The IDataTypeService is not used anymore. This constructor will be removed in a future version.")] + public SliderValueConverter(IDataTypeService dataTypeService) + { } - public static void ClearCaches() => Storages.Clear(); + /// + /// Clears the data type configuration caches. + /// + [Obsolete("Caching of data type configuration is not done anymore. This method will be removed in a future version.")] + public static void ClearCaches() + { } + /// public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Slider); + /// public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => IsRangeDataType(propertyType.DataType.Id) ? typeof(Range) : typeof(decimal); + => IsRange(propertyType) ? typeof(Range) : typeof(decimal); + /// public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; + /// public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) { - if (source == null) + bool isRange = IsRange(propertyType); + + var sourceString = source?.ToString(); + + return isRange + ? HandleRange(sourceString) + : HandleDecimal(sourceString); + } + + private static Range HandleRange(string? sourceString) + { + if (sourceString is null) { - return null; + return new Range(); } - if (IsRangeDataType(propertyType.DataType.Id)) - { - var rangeRawValues = source.ToString()!.Split(Constants.CharArrays.Comma); - Attempt minimumAttempt = rangeRawValues[0].TryConvertTo(); - Attempt maximumAttempt = rangeRawValues[1].TryConvertTo(); + string[] rangeRawValues = sourceString.Split(Constants.CharArrays.Comma); - if (minimumAttempt.Success && maximumAttempt.Success) + if (TryParseDecimal(rangeRawValues[0], out var minimum)) + { + if (rangeRawValues.Length == 1) { - return new Range { Maximum = maximumAttempt.Result, Minimum = minimumAttempt.Result }; + // Configuration is probably changed from single to range, return range with same min/max + return new Range + { + Minimum = minimum, + Maximum = minimum + }; + } + + if (rangeRawValues.Length == 2 && TryParseDecimal(rangeRawValues[1], out var maximum)) + { + return new Range + { + Minimum = minimum, + Maximum = maximum + }; } } - Attempt valueAttempt = source.ToString().TryConvertTo(); - if (valueAttempt.Success) + return new Range(); + } + + private static decimal HandleDecimal(string? sourceString) + { + if (string.IsNullOrEmpty(sourceString)) { - return valueAttempt.Result; + return default; } - // Something failed in the conversion of the strings to decimals - return null; + // This used to be a range slider, so we'll assign the minimum value as the new value + if (sourceString.Contains(',')) + { + var minimumValueRepresentation = sourceString.Split(Constants.CharArrays.Comma)[0]; + + if (TryParseDecimal(minimumValueRepresentation, out var minimum)) + { + return minimum; + } + } + else if (TryParseDecimal(sourceString, out var value)) + { + return value; + } + + return default; } /// - /// Discovers if the slider is set to range mode. + /// Helper method for parsing a double consistently /// - /// - /// The data type id. - /// - /// - /// The . - /// - private bool IsRangeDataType(int dataTypeId) => + private static bool TryParseDecimal(string? representation, out decimal value) + => decimal.TryParse(representation, NumberStyles.Number, CultureInfo.InvariantCulture, out value); - // GetPreValuesCollectionByDataTypeId is cached at repository level; - // still, the collection is deep-cloned so this is kinda expensive, - // better to cache here + trigger refresh in DataTypeCacheRefresher - // TODO: this is cheap now, remove the caching - Storages.GetOrAdd(dataTypeId, id => - { - IDataType? dataType = _dataTypeService.GetDataType(id); - SliderConfiguration? configuration = dataType?.ConfigurationAs(); - return configuration?.EnableRange ?? false; - }); + private static bool IsRange(IPublishedPropertyType propertyType) + => propertyType.DataType.ConfigurationAs()?.EnableRange == true; } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs index 3afc5a6596..2dd1c1d56e 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Serialization; @@ -7,69 +6,66 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; +/// +/// The tags property value converter. +/// +/// [DefaultPropertyValueConverter] public class TagsValueConverter : PropertyValueConverterBase { - private static readonly ConcurrentDictionary Storages = new(); - private readonly IDataTypeService _dataTypeService; private readonly IJsonSerializer _jsonSerializer; + /// + /// Initializes a new instance of the class. + /// + /// The JSON serializer. + /// jsonSerializer + public TagsValueConverter(IJsonSerializer jsonSerializer) + => _jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer)); + + /// + /// Initializes a new instance of the class. + /// + /// The data type service. + /// The JSON serializer. + [Obsolete("The IDataTypeService is not used anymore. This constructor will be removed in a future version.")] public TagsValueConverter(IDataTypeService dataTypeService, IJsonSerializer jsonSerializer) - { - _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); - _jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer)); - } + : this(jsonSerializer) + { } - public static void ClearCaches() => Storages.Clear(); + /// + /// Clears the data type configuration caches. + /// + [Obsolete("Caching of data type configuration is not done anymore. This method will be removed in a future version.")] + public static void ClearCaches() + { } + /// public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Tags); + /// public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof(IEnumerable); + /// public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; + /// public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) { - if (source == null) + string? sourceString = source?.ToString(); + if (string.IsNullOrEmpty(sourceString)) { return Array.Empty(); } - // if Json storage type deserialize and return as string array - if (JsonStorageType(propertyType.DataType.Id)) - { - var array = source.ToString() is not null - ? _jsonSerializer.Deserialize(source.ToString()!) - : null; - return array ?? Array.Empty(); - } - - // Otherwise assume CSV storage type and return as string array - return source.ToString()?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); + return IsJson(propertyType) + ? _jsonSerializer.Deserialize(sourceString) ?? Array.Empty() + : sourceString.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); } - public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview) => (string[]?)source; - - /// - /// Discovers if the tags data type is storing its data in a Json format - /// - /// - /// The data type id. - /// - /// - /// The . - /// - private bool JsonStorageType(int dataTypeId) => - - // GetDataType(id) is cached at repository level; still, there is some - // deep-cloning involved (expensive) - better cache here + trigger - // refresh in DataTypeCacheRefresher - Storages.GetOrAdd(dataTypeId, id => - { - TagConfiguration? configuration = _dataTypeService.GetDataType(id)?.ConfigurationAs(); - return configuration?.StorageType == TagsStorageType.Json; - }); + private static bool IsJson(IPublishedPropertyType propertyType) + => propertyType.DataType.ConfigurationAs()?.StorageType == TagsStorageType.Json; }