Skip to content

Commit 413398a

Browse files
Use data type configuration to determine default value for empty toggle and slider property values (#17854)
* Use data type configuration to determine default value for empty toggle property values. * Added/updated unit tests. * Fixed failing integration tests. * Applied similar default value display for the slider property editor and aligned implementation of true/false with this. * Fixed unit tests. * Removed "duplicate" JsonPropertyName attributes and added a custom TypeInfoResolver for data type configuration so we can re-use the existing ConfigurationField attributes. * Minor cleanup --------- Co-authored-by: nikolajlauridsen <[email protected]>
1 parent b7f4247 commit 413398a

File tree

9 files changed

+192
-41
lines changed

9 files changed

+192
-41
lines changed

src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Text.Json.Serialization;
2+
13
namespace Umbraco.Cms.Core.PropertyEditors;
24

35
/// <summary>
@@ -13,4 +15,10 @@ public class SliderConfiguration
1315

1416
[ConfigurationField("maxVal")]
1517
public decimal MaximumValue { get; set; }
18+
19+
[ConfigurationField("initVal1")]
20+
public decimal InitialValue1 { get; set; }
21+
22+
[ConfigurationField("initVal2")]
23+
public decimal InitialValue2 { get; set; }
1624
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Umbraco.Cms.Core.PropertyEditors;
4+
5+
/// <summary>
6+
/// Represents the configuration for the true/false (toggle) value editor.
7+
/// </summary>
8+
public class TrueFalseConfiguration
9+
{
10+
[ConfigurationField("default")]
11+
public bool InitialState { get; set; }
12+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) Umbraco.
2+
// See LICENSE for more details.
3+
4+
using Umbraco.Cms.Core.IO;
5+
6+
namespace Umbraco.Cms.Core.PropertyEditors;
7+
8+
/// <summary>
9+
/// Represents the configuration editor for the true/false (toggle) value editor.
10+
/// </summary>
11+
public class TrueFalseConfigurationEditor : ConfigurationEditor<TrueFalseConfiguration>
12+
{
13+
public TrueFalseConfigurationEditor(IIOHelper ioHelper)
14+
: base(ioHelper)
15+
{
16+
}
17+
}

src/Umbraco.Core/PropertyEditors/ValueConverters/SliderValueConverter.cs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,29 @@ public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType
4949
/// <inheritdoc />
5050
public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview)
5151
{
52-
bool isRange = IsRange(propertyType);
52+
SliderConfiguration? configuration = propertyType.DataType.ConfigurationAs<SliderConfiguration>();
53+
bool isRange = IsRange(configuration);
5354

5455
var sourceString = source?.ToString();
5556

57+
// If source is null, the returned value depends on the configured initial values.
58+
if (string.IsNullOrEmpty(sourceString))
59+
{
60+
return isRange
61+
? new Range<decimal>
62+
{
63+
Minimum = configuration?.InitialValue1 ?? 0M,
64+
Maximum = configuration?.InitialValue2 ?? 0M
65+
}
66+
: configuration?.InitialValue1 ?? 0M;
67+
}
68+
5669
return isRange
5770
? HandleRange(sourceString)
5871
: HandleDecimal(sourceString);
5972
}
6073

61-
private static Range<decimal> HandleRange(string? sourceString)
74+
private static Range<decimal> HandleRange(string sourceString)
6275
{
6376
if (sourceString is null)
6477
{
@@ -92,13 +105,8 @@ private static Range<decimal> HandleRange(string? sourceString)
92105
return new Range<decimal>();
93106
}
94107

95-
private static decimal HandleDecimal(string? sourceString)
108+
private static decimal HandleDecimal(string sourceString)
96109
{
97-
if (string.IsNullOrEmpty(sourceString))
98-
{
99-
return default;
100-
}
101-
102110
// This used to be a range slider, so we'll assign the minimum value as the new value
103111
if (sourceString.Contains(','))
104112
{
@@ -124,5 +132,8 @@ private static bool TryParseDecimal(string? representation, out decimal value)
124132
=> decimal.TryParse(representation, NumberStyles.Number, CultureInfo.InvariantCulture, out value);
125133

126134
private static bool IsRange(IPublishedPropertyType propertyType)
127-
=> propertyType.DataType.ConfigurationAs<SliderConfiguration>()?.EnableRange == true;
135+
=> IsRange(propertyType.DataType.ConfigurationAs<SliderConfiguration>());
136+
137+
private static bool IsRange(SliderConfiguration? configuration)
138+
=> configuration?.EnableRange == true;
128139
}

src/Umbraco.Core/PropertyEditors/ValueConverters/YesNoValueConverter.cs

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,13 @@ public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
1414
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
1515
=> PropertyCacheLevel.Element;
1616

17-
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview)
17+
public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview)
1818
{
19+
if (source is null)
20+
{
21+
return null;
22+
}
23+
1924
// in xml a boolean is: string
2025
// in the database a boolean is: string "1" or "0" or empty
2126
// typically the converter does not need to handle anything else ("true"...)
@@ -35,23 +40,36 @@ public override object ConvertSourceToIntermediate(IPublishedElement owner, IPub
3540
return bool.TryParse(s, out var result) && result;
3641
}
3742

38-
if (source is int)
43+
if (source is int sourceAsInt)
3944
{
40-
return (int)source == 1;
45+
return sourceAsInt == 1;
4146
}
4247

4348
// this is required for correct true/false handling in nested content elements
44-
if (source is long)
49+
if (source is long sourceAsLong)
4550
{
46-
return (long)source == 1;
51+
return sourceAsLong == 1;
4752
}
4853

49-
if (source is bool)
54+
if (source is bool sourceAsBoolean)
5055
{
51-
return (bool)source;
56+
return sourceAsBoolean;
5257
}
5358

54-
// default value is: false
59+
// false for any other value
5560
return false;
5661
}
62+
63+
/// <inheritdoc />
64+
public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview)
65+
{
66+
// If source is null, whether we return true or false depends on the configured default value (initial state).
67+
if (source is null)
68+
{
69+
TrueFalseConfiguration? configuration = propertyType.DataType.ConfigurationAs<TrueFalseConfiguration>();
70+
return configuration?.InitialState ?? false;
71+
}
72+
73+
return (bool)source;
74+
}
5775
}

src/Umbraco.Infrastructure/PropertyEditors/TrueFalsePropertyEditor.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Umbraco.
22
// See LICENSE for more details.
33

4+
using Microsoft.Extensions.DependencyInjection;
5+
using Umbraco.Cms.Core.DependencyInjection;
46
using Umbraco.Cms.Core.IO;
57
using Umbraco.Cms.Core.Models;
68
using Umbraco.Cms.Core.Models.Editors;
@@ -18,17 +20,37 @@ namespace Umbraco.Cms.Core.PropertyEditors;
1820
ValueEditorIsReusable = true)]
1921
public class TrueFalsePropertyEditor : DataEditor
2022
{
23+
private readonly IIOHelper _ioHelper;
24+
2125
/// <summary>
2226
/// Initializes a new instance of the <see cref="TrueFalsePropertyEditor" /> class.
2327
/// </summary>
28+
[Obsolete("Please use the constructor taking all parameters. This constructor will be removed in V17.")]
2429
public TrueFalsePropertyEditor(IDataValueEditorFactory dataValueEditorFactory)
30+
: this(
31+
dataValueEditorFactory,
32+
StaticServiceProvider.Instance.GetRequiredService<IIOHelper>())
33+
{
34+
}
35+
36+
/// <summary>
37+
/// Initializes a new instance of the <see cref="TrueFalsePropertyEditor" /> class.
38+
/// </summary>
39+
public TrueFalsePropertyEditor(IDataValueEditorFactory dataValueEditorFactory, IIOHelper ioHelper)
2540
: base(dataValueEditorFactory)
26-
=> SupportsReadOnly = true;
41+
{
42+
_ioHelper = ioHelper;
43+
SupportsReadOnly = true;
44+
}
2745

2846
/// <inheritdoc />
2947
protected override IDataValueEditor CreateValueEditor()
3048
=> DataValueEditorFactory.Create<TrueFalsePropertyValueEditor>(Attribute!);
3149

50+
/// <inheritdoc />
51+
protected override IConfigurationEditor CreateConfigurationEditor() =>
52+
new TrueFalseConfigurationEditor(_ioHelper);
53+
3254
internal class TrueFalsePropertyValueEditor : DataValueEditor
3355
{
3456
public TrueFalsePropertyValueEditor(

src/Umbraco.Infrastructure/Serialization/SystemTextConfigurationEditorJsonSerializer.cs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System.Text.Json;
22
using System.Text.Json.Serialization;
3+
using System.Text.Json.Serialization.Metadata;
4+
using Umbraco.Cms.Core.PropertyEditors;
35
using Umbraco.Cms.Core.Serialization;
46

57
namespace Umbraco.Cms.Infrastructure.Serialization;
@@ -16,8 +18,9 @@ public SystemTextConfigurationEditorJsonSerializer()
1618
=> _jsonSerializerOptions = new JsonSerializerOptions()
1719
{
1820
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
19-
// in some cases, configs aren't camel cased in the DB, so we have to resort to case insensitive
20-
// property name resolving when creating configuration objects (deserializing DB configs)
21+
22+
// In some cases, configs aren't camel cased in the DB, so we have to resort to case insensitive
23+
// property name resolving when creating configuration objects (deserializing DB configs).
2124
PropertyNameCaseInsensitive = true,
2225
NumberHandling = JsonNumberHandling.AllowReadingFromString,
2326
Converters =
@@ -26,9 +29,42 @@ public SystemTextConfigurationEditorJsonSerializer()
2629
new JsonObjectConverter(),
2730
new JsonUdiConverter(),
2831
new JsonUdiRangeConverter(),
29-
new JsonBooleanConverter()
30-
}
32+
new JsonBooleanConverter(),
33+
},
34+
35+
// Properties of data type configuration objects are annotated with [ConfigurationField] attributes
36+
// that provide the serialized name. Rather than decorating them as well with [JsonPropertyName] attributes
37+
// when they differ from the property name, we'll define a custom type info resolver to use the
38+
// existing attribute.
39+
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
40+
.WithAddedModifier(UseAttributeConfiguredPropertyNames()),
3141
};
3242

3343
protected override JsonSerializerOptions JsonSerializerOptions => _jsonSerializerOptions;
44+
45+
/// <summary>
46+
/// A custom action used to provide property names when they are overridden by
47+
/// <see cref="ConfigurationField"/> attributes.
48+
/// </summary>
49+
/// <remarks>
50+
/// Hat-tip: https://stackoverflow.com/a/78063664
51+
/// </remarks>
52+
private static Action<JsonTypeInfo> UseAttributeConfiguredPropertyNames() => typeInfo =>
53+
{
54+
if (typeInfo.Kind is not JsonTypeInfoKind.Object)
55+
{
56+
return;
57+
}
58+
59+
foreach (JsonPropertyInfo property in typeInfo.Properties)
60+
{
61+
if (property.AttributeProvider?.GetCustomAttributes(typeof(ConfigurationFieldAttribute), true) is { } attributes)
62+
{
63+
foreach (ConfigurationFieldAttribute attribute in attributes)
64+
{
65+
property.Name = attribute.Key;
66+
}
67+
}
68+
}
69+
};
3470
}

tests/Umbraco.Tests.Common/Builders/DataTypeBuilder.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ public override DataType Build()
137137
var sortOrder = _sortOrder ?? 0;
138138
var serializer = new SystemTextConfigurationEditorJsonSerializer();
139139

140-
return new DataType(editor, serializer, parentId)
140+
var dataType = new DataType(editor, serializer, parentId)
141141
{
142142
Id = id,
143143
Key = key,
@@ -152,5 +152,7 @@ public override DataType Build()
152152
DatabaseType = databaseType,
153153
SortOrder = sortOrder
154154
};
155+
156+
return dataType;
155157
}
156158
}

tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/PropertyEditorValueConverterTests.cs

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
1010
using Umbraco.Cms.Core.Strings;
1111
using Umbraco.Cms.Infrastructure.Serialization;
12+
using Umbraco.Cms.Tests.Common.Builders;
1213

1314
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors;
1415

@@ -46,30 +47,54 @@ public void CanConvertDatePickerPropertyEditor(string date, bool expected)
4647
}
4748
}
4849

49-
[TestCase("TRUE", true)]
50-
[TestCase("True", true)]
51-
[TestCase("true", true)]
52-
[TestCase("1", true)]
53-
[TestCase(1, true)]
54-
[TestCase(true, true)]
55-
[TestCase("FALSE", false)]
56-
[TestCase("False", false)]
57-
[TestCase("false", false)]
58-
[TestCase("0", false)]
59-
[TestCase(0, false)]
60-
[TestCase(false, false)]
61-
[TestCase("", false)]
62-
[TestCase(null, false)]
63-
[TestCase("blah", false)]
64-
public void CanConvertYesNoPropertyEditor(object value, bool expected)
50+
[TestCase("TRUE", null, true)]
51+
[TestCase("True", null, true)]
52+
[TestCase("true", null, true)]
53+
[TestCase("1", null, true)]
54+
[TestCase(1, null, true)]
55+
[TestCase(true, null, true)]
56+
[TestCase("FALSE", null, false)]
57+
[TestCase("False", null, false)]
58+
[TestCase("false", null, false)]
59+
[TestCase("0", null, false)]
60+
[TestCase(0, null, false)]
61+
[TestCase(false, null, false)]
62+
[TestCase("", null, false)]
63+
[TestCase("blah", null, false)]
64+
[TestCase(null, false, false)]
65+
[TestCase(null, true, true)]
66+
public void CanConvertTrueFalsePropertyEditor(object value, bool initialStateConfigurationValue, bool expected)
6567
{
68+
var publishedDataType = CreatePublishedDataType(initialStateConfigurationValue);
69+
70+
var publishedPropertyTypeMock = new Mock<IPublishedPropertyType>();
71+
publishedPropertyTypeMock
72+
.SetupGet(p => p.DataType)
73+
.Returns(publishedDataType);
74+
6675
var converter = new YesNoValueConverter();
67-
var result =
68-
converter.ConvertSourceToIntermediate(null, null, value, false); // does not use type for conversion
76+
var intermediateResult = converter.ConvertSourceToIntermediate(null, publishedPropertyTypeMock.Object, value, false);
77+
var result = converter.ConvertIntermediateToObject(null, publishedPropertyTypeMock.Object, PropertyCacheLevel.Element, intermediateResult, false);
6978

7079
Assert.AreEqual(expected, result);
7180
}
7281

82+
private static PublishedDataType CreatePublishedDataType(bool initialStateConfigurationValue)
83+
{
84+
var dataTypeConfiguration = new TrueFalseConfiguration
85+
{
86+
InitialState = initialStateConfigurationValue
87+
};
88+
89+
var dateTypeMock = new Mock<IDataType>();
90+
dateTypeMock.SetupGet(x => x.Id).Returns(1000);
91+
dateTypeMock.SetupGet(x => x.EditorAlias).Returns(global::Umbraco.Cms.Core.Constants.PropertyEditors.Aliases.Boolean);
92+
dateTypeMock.SetupGet(x => x.EditorUiAlias).Returns("Umb.PropertyEditorUi.Toggle");
93+
dateTypeMock.SetupGet(x => x.ConfigurationObject).Returns(dataTypeConfiguration);
94+
95+
return new PublishedDataType(dateTypeMock.Object.Id, dateTypeMock.Object.EditorAlias, dateTypeMock.Object.EditorUiAlias, new Lazy<object>(() => dataTypeConfiguration));
96+
}
97+
7398
[TestCase("[\"apples\"]", new[] { "apples" })]
7499
[TestCase("[\"apples\",\"oranges\"]", new[] { "apples", "oranges" })]
75100
[TestCase("[\"apples\",\"oranges\",\"pears\"]", new[] { "apples", "oranges", "pears" })]

0 commit comments

Comments
 (0)