From 867d4059928794cbc11f8e627b8263ad75bd030c Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 4 Mar 2025 16:25:10 +0100 Subject: [PATCH] Provide custom required validator for block list and toggle (#18474) * Provide custom required validator for block list. * Adds a custom required validator for the toggle to ensure the value provided is true. * Remove unnecessary usings * Remove redundant interface and base constructor * Remove unnecessary interface --------- Co-authored-by: mole --- .../Services/PropertyValidationService.cs | 4 +- .../BlockEditorPropertyValueEditor.cs | 11 +++-- .../BlockListPropertyEditorBase.cs | 4 ++ .../BlockValuePropertyValueEditorBase.cs | 1 - .../TrueFalsePropertyEditor.cs | 18 ++++++++- .../BlockListValueRequiredValidator.cs | 40 +++++++++++++++++++ .../TrueFalseValueRequiredValidator.cs | 29 ++++++++++++++ .../BlockListValueRequiredValidatorTests.cs | 35 ++++++++++++++++ .../TrueFalseValueRequiredValidatorTests.cs | 39 ++++++++++++++++++ 9 files changed, 172 insertions(+), 9 deletions(-) create mode 100644 src/Umbraco.Infrastructure/PropertyEditors/Validators/BlockListValueRequiredValidator.cs create mode 100644 src/Umbraco.Infrastructure/PropertyEditors/Validators/TrueFalseValueRequiredValidator.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/BlockListValueRequiredValidatorTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/TrueFalseValueRequiredValidatorTests.cs diff --git a/src/Umbraco.Core/Services/PropertyValidationService.cs b/src/Umbraco.Core/Services/PropertyValidationService.cs index fbb159e135..1bed040ba1 100644 --- a/src/Umbraco.Core/Services/PropertyValidationService.cs +++ b/src/Umbraco.Core/Services/PropertyValidationService.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; @@ -88,7 +88,7 @@ public class PropertyValidationService : IPropertyValidationService { // Retrieve default messages used for required and regex validatation. We'll replace these // if set with custom ones if they've been provided for a given property. - var requiredDefaultMessages = new[] { Constants.Validation.ErrorMessages.Properties.Missing }; + var requiredDefaultMessages = new[] { Constants.Validation.ErrorMessages.Properties.Missing, Constants.Validation.ErrorMessages.Properties.Empty }; var formatDefaultMessages = new[] { Constants.Validation.ErrorMessages.Properties.PatternMismatch }; IDataValueEditor valueEditor = _valueEditorCache.GetValueEditor(editor, dataType); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs index a43921156a..d157b223b3 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs @@ -20,8 +20,6 @@ public abstract class BlockEditorPropertyValueEditor : BlockVal where TValue : BlockValue, new() where TLayout : class, IBlockLayoutItem, new() { - private readonly IJsonSerializer _jsonSerializer; - [Obsolete("Please use the non-obsolete constructor. Will be removed in V16.")] protected BlockEditorPropertyValueEditor( DataEditorAttribute attribute, @@ -52,7 +50,12 @@ public abstract class BlockEditorPropertyValueEditor : BlockVal IIOHelper ioHelper, DataEditorAttribute attribute) : base(propertyEditors, dataTypeConfigurationCache, shortStringHelper, jsonSerializer, dataValueReferenceFactories, blockEditorVarianceHandler, languageService, ioHelper, attribute) => - _jsonSerializer = jsonSerializer; + JsonSerializer = jsonSerializer; + + /// + /// Gets the . + /// + protected IJsonSerializer JsonSerializer { get; } /// public override IEnumerable GetReferences(object? value) @@ -143,6 +146,6 @@ public abstract class BlockEditorPropertyValueEditor : BlockVal MapBlockValueFromEditor(blockEditorData.BlockValue); // return json - return _jsonSerializer.Serialize(blockEditorData.BlockValue); + return JsonSerializer.Serialize(blockEditorData.BlockValue); } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs index 9478c8e805..25d24db6d0 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Infrastructure.PropertyEditors.Validators; using StaticServiceProvider = Umbraco.Cms.Core.DependencyInjection.StaticServiceProvider; namespace Umbraco.Cms.Core.PropertyEditors; @@ -74,6 +75,9 @@ public abstract class BlockListPropertyEditorBase : DataEditor Validators.Add(new MinMaxValidator(BlockEditorValues, textService)); } + /// + public override IValueRequiredValidator RequiredValidator => new BlockListValueRequiredValidator(JsonSerializer); + protected override BlockListValue CreateWithLayout(IEnumerable layout) => new(layout); private class MinMaxValidator : BlockEditorMinMaxValidatorBase diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs index 22e9384bf2..d66e2b2caa 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs @@ -10,7 +10,6 @@ using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TrueFalsePropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/TrueFalsePropertyEditor.cs index 66d1af9c77..df63ee52d2 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/TrueFalsePropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/TrueFalsePropertyEditor.cs @@ -6,11 +6,12 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Infrastructure.PropertyEditors.Validators; namespace Umbraco.Cms.Core.PropertyEditors; /// -/// Represents a checkbox property and parameter editor. +/// Represents a true/false (toggle) property editor. /// [DataEditor( Constants.PropertyEditors.Aliases.Boolean, @@ -29,8 +30,14 @@ public class TrueFalsePropertyEditor : DataEditor protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); + /// + /// Defines the value editor for the true/false (toggle) property editor. + /// internal class TrueFalsePropertyValueEditor : DataValueEditor { + /// + /// Initializes a new instance of the class. + /// public TrueFalsePropertyValueEditor( IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, @@ -40,10 +47,17 @@ public class TrueFalsePropertyEditor : DataEditor { } + /// + public override IValueRequiredValidator RequiredValidator => new TrueFalseValueRequiredValidator(); + + /// public override object? ToEditor(IProperty property, string? culture = null, string? segment = null) => ParsePropertyValue(property.GetValue(culture, segment)); - // NOTE: property editor value type is Integer, which means we need to store the boolean representation as 0 or 1 + /// + /// + /// NOTE: property editor value type is Integer, which means we need to store the boolean representation as 0 or 1. + /// public override object? FromEditor(ContentPropertyData editorValue, object? currentValue) => ParsePropertyValue(editorValue.Value) ? 1 : 0; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/Validators/BlockListValueRequiredValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/Validators/BlockListValueRequiredValidator.cs new file mode 100644 index 0000000000..40d67bb0d2 --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/Validators/BlockListValueRequiredValidator.cs @@ -0,0 +1,40 @@ +using System.ComponentModel.DataAnnotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.PropertyEditors.Validators; +using Umbraco.Cms.Core.Serialization; + +namespace Umbraco.Cms.Infrastructure.PropertyEditors.Validators; + +/// +/// Custom validator for block value required validation. +/// +internal class BlockListValueRequiredValidator : RequiredValidator +{ + private readonly IJsonSerializer _jsonSerializer; + + /// + /// Initializes a new instance of the class. + /// + public BlockListValueRequiredValidator(IJsonSerializer jsonSerializer) => _jsonSerializer = jsonSerializer; + + /// + public override IEnumerable ValidateRequired(object? value, string? valueType) + { + IEnumerable validationResults = base.ValidateRequired(value, valueType); + + if (value is null) + { + return validationResults; + } + + if (_jsonSerializer.TryDeserialize(value, out BlockListValue? blockListValue) && + blockListValue.ContentData.Count == 0 && + blockListValue.Layout.Count == 0) + { + validationResults = validationResults.Append(new ValidationResult(Constants.Validation.ErrorMessages.Properties.Empty, ["value"])); + } + + return validationResults; + } +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/Validators/TrueFalseValueRequiredValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/Validators/TrueFalseValueRequiredValidator.cs new file mode 100644 index 0000000000..3e1cdfd117 --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/Validators/TrueFalseValueRequiredValidator.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.PropertyEditors.Validators; + +namespace Umbraco.Cms.Infrastructure.PropertyEditors.Validators; + +/// +/// Custom validator for true/false (toggle) required validation. +/// +internal class TrueFalseValueRequiredValidator : RequiredValidator +{ + /// + public override IEnumerable ValidateRequired(object? value, string? valueType) + { + IEnumerable validationResults = base.ValidateRequired(value, valueType); + + if (value is null) + { + return validationResults; + } + + if (value is bool valueAsBool && valueAsBool is false) + { + validationResults = validationResults.Append(new ValidationResult(Constants.Validation.ErrorMessages.Properties.Empty, ["value"])); + } + + return validationResults; + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/BlockListValueRequiredValidatorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/BlockListValueRequiredValidatorTests.cs new file mode 100644 index 0000000000..3256f9d15d --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/BlockListValueRequiredValidatorTests.cs @@ -0,0 +1,35 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Text.Json.Nodes; +using NUnit.Framework; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Infrastructure.PropertyEditors.Validators; +using Umbraco.Cms.Infrastructure.Serialization; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; + +[TestFixture] +public class BlockListValueRequiredValidatorTests +{ + [Test] + public void Validates_Empty_Block_List_As_Not_Provided() + { + var validator = new BlockListValueRequiredValidator(new SystemTextJsonSerializer()); + + var value = JsonNode.Parse("{ \"contentData\": [], \"settingsData\": [] }"); + var result = validator.ValidateRequired(value, ValueTypes.Json); + Assert.AreEqual(1, result.Count()); + } + + [Test] + public void Validates_Populated_Block_List_As_Provided() + { + var validator = new BlockListValueRequiredValidator(new SystemTextJsonSerializer()); + + var value = JsonNode.Parse("{ \"contentData\": [ {} ], \"settingsData\": [] }"); + var result = validator.ValidateRequired(value, ValueTypes.Json); + Assert.IsEmpty(result); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/TrueFalseValueRequiredValidatorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/TrueFalseValueRequiredValidatorTests.cs new file mode 100644 index 0000000000..a7dfa7fe7b --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/Validators/TrueFalseValueRequiredValidatorTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using NUnit.Framework; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Infrastructure.PropertyEditors.Validators; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; + +[TestFixture] +public class TrueFalseValueRequiredValidatorTests +{ + [Test] + public void Validates_Null_Value_As_Not_Provided() + { + var validator = new TrueFalseValueRequiredValidator(); + + var result = validator.ValidateRequired(null, ValueTypes.Integer); + Assert.AreEqual(1, result.Count()); + } + + [Test] + public void Validates_False_Value_As_Not_Provided() + { + var validator = new TrueFalseValueRequiredValidator(); + + var result = validator.ValidateRequired(false, ValueTypes.Integer); + Assert.AreEqual(1, result.Count()); + } + + [Test] + public void Validates_True_Value_As_Provided() + { + var validator = new TrueFalseValueRequiredValidator(); + + var result = validator.ValidateRequired(true, ValueTypes.Integer); + Assert.IsEmpty(result); + } +}