Browse Source

Merge pull request #5334 from abpframework/liangshiwei/featuremanagement

Group features on the feature management modal
pull/5341/head
Halil İbrahim Kalkan 6 years ago
committed by GitHub
parent
commit
f64c04c2c5
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionManager.cs
  2. 4
      framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionManager.cs
  3. 18
      modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeatureGroupDto.cs
  4. 9
      modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeatureListDto.cs
  5. 9
      modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/GetFeatureListResultDto.cs
  6. 2
      modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/IFeatureAppService.cs
  7. 55
      modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs
  8. 2
      modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/FeaturesController.cs
  9. 98
      modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml
  10. 19
      modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml.cs
  11. 4
      modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/FeatureAppService_Tests.cs
  12. 82
      modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/StringValueJsonConverter_Tests.cs

9
framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionManager.cs

@ -62,6 +62,11 @@ namespace Volo.Abp.Features
return FeatureDefinitions.GetOrDefault(name); return FeatureDefinitions.GetOrDefault(name);
} }
public IReadOnlyList<FeatureGroupDefinition> GetGroups()
{
return FeatureGroupDefinitions.Values.ToImmutableList();
}
protected virtual Dictionary<string, FeatureDefinition> CreateFeatureDefinitions() protected virtual Dictionary<string, FeatureDefinition> CreateFeatureDefinitions()
{ {
var features = new Dictionary<string, FeatureDefinition>(); var features = new Dictionary<string, FeatureDefinition>();
@ -78,7 +83,7 @@ namespace Volo.Abp.Features
} }
protected virtual void AddFeatureToDictionaryRecursively( protected virtual void AddFeatureToDictionaryRecursively(
Dictionary<string, FeatureDefinition> features, Dictionary<string, FeatureDefinition> features,
FeatureDefinition feature) FeatureDefinition feature)
{ {
if (features.ContainsKey(feature.Name)) if (features.ContainsKey(feature.Name))
@ -114,4 +119,4 @@ namespace Volo.Abp.Features
return context.Groups; return context.Groups;
} }
} }
} }

4
framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionManager.cs

@ -11,5 +11,7 @@ namespace Volo.Abp.Features
IReadOnlyList<FeatureDefinition> GetAll(); IReadOnlyList<FeatureDefinition> GetAll();
FeatureDefinition GetOrNull(string name); FeatureDefinition GetOrNull(string name);
IReadOnlyList<FeatureGroupDefinition> GetGroups();
} }
} }

18
modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeatureGroupDto.cs

@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace Volo.Abp.FeatureManagement
{
public class FeatureGroupDto
{
public string Name { get; set; }
public string DisplayName { get; set; }
public List<FeatureDto> Features { get; set; }
public string GetNormalizedGroupName()
{
return Name.Replace(".", "_");
}
}
}

9
modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeatureListDto.cs

@ -1,9 +0,0 @@
using System.Collections.Generic;
namespace Volo.Abp.FeatureManagement
{
public class FeatureListDto
{
public List<FeatureDto> Features { get; set; }
}
}

9
modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/GetFeatureListResultDto.cs

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace Volo.Abp.FeatureManagement
{
public class GetFeatureListResultDto
{
public List<FeatureGroupDto> Groups { get; set; }
}
}

2
modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/IFeatureAppService.cs

@ -6,7 +6,7 @@ namespace Volo.Abp.FeatureManagement
{ {
public interface IFeatureAppService : IApplicationService public interface IFeatureAppService : IApplicationService
{ {
Task<FeatureListDto> GetAsync([NotNull] string providerName, [NotNull] string providerKey); Task<GetFeatureListResultDto> GetAsync([NotNull] string providerName, [NotNull] string providerKey);
Task UpdateAsync([NotNull] string providerName, [NotNull] string providerKey, UpdateFeaturesDto input); Task UpdateAsync([NotNull] string providerName, [NotNull] string providerKey, UpdateFeaturesDto input);
} }

55
modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Features; using Volo.Abp.Features;
namespace Volo.Abp.FeatureManagement namespace Volo.Abp.FeatureManagement
@ -24,35 +25,49 @@ namespace Volo.Abp.FeatureManagement
Options = options.Value; Options = options.Value;
} }
public virtual async Task<FeatureListDto> GetAsync([NotNull] string providerName, [NotNull] string providerKey) public virtual async Task<GetFeatureListResultDto> GetAsync([NotNull] string providerName, [NotNull] string providerKey)
{ {
await CheckProviderPolicy(providerName); await CheckProviderPolicy(providerName);
var featureDefinitions = FeatureDefinitionManager.GetAll(); var result = new GetFeatureListResultDto
var features = new List<FeatureDto>(); {
Groups = new List<FeatureGroupDto>()
};
foreach (var featureDefinition in featureDefinitions) foreach (var group in FeatureDefinitionManager.GetGroups())
{ {
var feature = await FeatureManager.GetOrNullWithProviderAsync(featureDefinition.Name, providerName, providerKey); var groupDto = new FeatureGroupDto
features.Add(new FeatureDto {
Name = group.Name,
DisplayName = group.DisplayName.Localize(StringLocalizerFactory),
Features = new List<FeatureDto>()
};
foreach (var featureDefinition in group.GetFeaturesWithChildren())
{ {
Name = featureDefinition.Name, var feature = await FeatureManager.GetOrNullWithProviderAsync(featureDefinition.Name, providerName, providerKey);
DisplayName = featureDefinition.DisplayName?.Localize(StringLocalizerFactory), groupDto.Features.Add(new FeatureDto
ValueType = featureDefinition.ValueType,
Description = featureDefinition.Description?.Localize(StringLocalizerFactory),
ParentName = featureDefinition.Parent?.Name,
Value = feature.Value,
Provider = new FeatureProviderDto
{ {
Name = feature.Provider?.Name, Name = featureDefinition.Name,
Key = feature.Provider?.Key DisplayName = featureDefinition.DisplayName?.Localize(StringLocalizerFactory),
} ValueType = featureDefinition.ValueType,
}); Description = featureDefinition.Description?.Localize(StringLocalizerFactory),
} ParentName = featureDefinition.Parent?.Name,
Value = feature.Value,
Provider = new FeatureProviderDto
{
Name = feature.Provider?.Name,
Key = feature.Provider?.Key
}
});
}
SetFeatureDepth(groupDto.Features, providerName, providerKey);
SetFeatureDepth(features, providerName, providerKey); result.Groups.Add(groupDto);
}
return new FeatureListDto { Features = features }; return result;
} }
public virtual async Task UpdateAsync([NotNull] string providerName, [NotNull] string providerKey, UpdateFeaturesDto input) public virtual async Task UpdateAsync([NotNull] string providerName, [NotNull] string providerKey, UpdateFeaturesDto input)

2
modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/FeaturesController.cs

@ -17,7 +17,7 @@ namespace Volo.Abp.FeatureManagement
} }
[HttpGet] [HttpGet]
public virtual Task<FeatureListDto> GetAsync(string providerName, string providerKey) public virtual Task<GetFeatureListResultDto> GetAsync(string providerName, string providerKey)
{ {
return FeatureAppService.GetAsync(providerName, providerKey); return FeatureAppService.GetAsync(providerName, providerKey);
} }

98
modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml

@ -4,7 +4,6 @@
@using Volo.Abp.FeatureManagement.Localization @using Volo.Abp.FeatureManagement.Localization
@using Volo.Abp.Validation.StringValues @using Volo.Abp.Validation.StringValues
@using Volo.Abp.FeatureManagement.Web.Pages.FeatureManagement @using Volo.Abp.FeatureManagement.Web.Pages.FeatureManagement
@using Volo.Abp.Features
@model FeatureManagementModal @model FeatureManagementModal
@inject IHtmlLocalizer<AbpFeatureManagementResource> L @inject IHtmlLocalizer<AbpFeatureManagementResource> L
@{ @{
@ -13,53 +12,67 @@
<form method="post" asp-page="/FeatureManagement/FeatureManagementModal" data-script-class="abp.modals.FeatureManagement"> <form method="post" asp-page="/FeatureManagement/FeatureManagementModal" data-script-class="abp.modals.FeatureManagement">
<abp-modal size="Large"> <abp-modal size="Large">
<abp-modal-header title="@(L["Features"].Value)"></abp-modal-header> <abp-modal-header title="@(L["Features"].Value)"></abp-modal-header>
@if (Model.FeatureListDto?.Features != null && Model.FeatureListDto.Features.Any()) @if (Model.FeatureListResultDto != null && Model.FeatureListResultDto.Groups.Any())
{ {
<abp-modal-body class="ml-4"> <abp-modal-body class="ml-4">
<input asp-for="@Model.ProviderKey" /> <input asp-for="@Model.ProviderKey"/>
<input asp-for="@Model.ProviderName" /> <input asp-for="@Model.ProviderName"/>
@for (var i = 0; i < Model.FeatureListDto.Features.Count; i++) <abp-tabs name="FeaturesTabs" tab-style="PillVertical" vertical-header-size="_4" class="custom-scroll-container">
{ @for (var i = 0; i < Model.FeatureListResultDto.Groups.Count; i++)
var feature = Model.FeatureListDto.Features[i]; {
var disabled = Model.IsDisabled(feature.Provider.Name); <abp-tab title="@Model.FeatureListResultDto.Groups[i].DisplayName" name="v-pills-tab-@Model.FeatureListResultDto.Groups[i].GetNormalizedGroupName()">
<div class="mt-2" style="padding-left: @(feature.Depth * 20)px"> <h4>@Model.FeatureListResultDto.Groups[i].DisplayName</h4>
<hr class="mt-2 mb-3"/>
<div class="custom-scroll-content">
<div class="pl-1 pt-1">
@for (var j = 0; j < Model.FeatureListResultDto.Groups[i].Features.Count; j++)
{
var feature = Model.FeatureListResultDto.Groups[i].Features[j];
var disabled = Model.IsDisabled(feature.Provider.Name);
<div class="mt-2" style="padding-left: @(feature.Depth * 20)px">
<spam class="mr-2">@feature.DisplayName @(disabled ? $"({feature.Provider.Name})" : "")</spam> <spam class="mr-2">@feature.DisplayName @(disabled ? $"({feature.Provider.Name})" : "")</spam>
<input type="text" name="Features[@i].ProviderName" value="@feature.Provider.Name" hidden /> <input type="text" name="FeatureGroups[@i].Features[@j].ProviderName" value="@feature.Provider.Name" hidden/>
<input type="text" name="Features[@i].Type" value="@feature.ValueType?.Name" hidden /> <input type="text" name="FeatureGroups[@i].Features[@j].Type" value="@feature.ValueType?.Name" hidden/>
@if (feature.ValueType is FreeTextStringValueType) @if (feature.ValueType is FreeTextStringValueType)
{ {
<input type="text" name="Features[@i].Name" value="@feature.Name" hidden /> <input type="text" name="FeatureGroups[@i].Features[@j].Name" value="@feature.Name" hidden/>
<input disabled="@disabled" type="text" name="Features[@i].Value" value="@feature.Value" /> <input disabled="@disabled" type="text" name="FeatureGroups[@i].Features[@j].Value" value="@feature.Value"/>
} }
@if (feature.ValueType is SelectionStringValueType) @if (feature.ValueType is SelectionStringValueType)
{ {
<input type="text" name="Features[@i].Name" value="@feature.Name" hidden /> <input type="text" name="FeatureGroups[@i].Features[@j].Name" value="@feature.Name" hidden/>
<select disabled="@disabled" name="Features[@i].Value"> <select disabled="@disabled" name="FeatureGroups[@i].Features[@j].Value">
@foreach (var item in (feature.ValueType as SelectionStringValueType).ItemSource.Items) @foreach (var item in (feature.ValueType as SelectionStringValueType).ItemSource.Items)
{ {
if (item.Value == feature.Value) if (item.Value == feature.Value)
{ {
<option value="@item.Value" selected="selected"> @L.GetString(item.DisplayText.Name) </option> <option value="@item.Value" selected="selected"> @L.GetString(item.DisplayText.Name) </option>
} }
else else
{ {
<option value="@item.Value"> @L.GetString(item.DisplayText.Name) </option> <option value="@item.Value"> @L.GetString(item.DisplayText.Name) </option>
}
}
</select>
}
@if (feature.ValueType is ToggleStringValueType)
{
<input type="text" name="FeatureGroups[@i].Features[@j].Name" value="@feature.Name" hidden/>
<input disabled="@disabled" type="checkbox" class="FeatureValueCheckbox" name="FeatureGroups[@i].Features[@j].BoolValue" value="@feature.Value"
@Html.Raw(feature.Value == "True" ? "checked" : "")/>
}
</div>
} }
} </div>
</select> </div>
} </abp-tab>
@if (feature.ValueType is ToggleStringValueType) }
{
<input type="text" name="Features[@i].Name" value="@feature.Name" hidden /> </abp-tabs>
<input disabled="@disabled" type="checkbox" class="FeatureValueCheckbox" name="Features[@i].BoolValue" value="@feature.Value"
@Html.Raw(feature.Value == "True" ? "checked" : "") />
}
</div>
}
</abp-modal-body> </abp-modal-body>
<abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer> <abp-modal-footer buttons="@(AbpModalButtons.Cancel | AbpModalButtons.Save)"></abp-modal-footer>
} }
else else
{ {
@ -69,4 +82,3 @@
} }
</abp-modal> </abp-modal>
</form> </form>

19
modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml.cs

@ -22,9 +22,9 @@ namespace Volo.Abp.FeatureManagement.Web.Pages.FeatureManagement
public string ProviderKey { get; set; } public string ProviderKey { get; set; }
[BindProperty] [BindProperty]
public List<FeatureViewModel> Features { get; set; } public List<FeatureGroupViewModel> FeatureGroups { get; set; }
public FeatureListDto FeatureListDto { get; set; } public GetFeatureListResultDto FeatureListResultDto { get; set; }
protected IFeatureAppService FeatureAppService { get; } protected IFeatureAppService FeatureAppService { get; }
@ -35,16 +35,20 @@ namespace Volo.Abp.FeatureManagement.Web.Pages.FeatureManagement
FeatureAppService = featureAppService; FeatureAppService = featureAppService;
} }
public virtual async Task OnGetAsync() public virtual async Task<IActionResult> OnGetAsync()
{ {
FeatureListDto = await FeatureAppService.GetAsync(ProviderName, ProviderKey); ValidateModel();
FeatureListResultDto = await FeatureAppService.GetAsync(ProviderName, ProviderKey);
return Page();
} }
public virtual async Task<IActionResult> OnPostAsync() public virtual async Task<IActionResult> OnPostAsync()
{ {
var features = new UpdateFeaturesDto var features = new UpdateFeaturesDto
{ {
Features = Features.Select(f => new UpdateFeatureDto Features = FeatureGroups.SelectMany(g => g.Features).Select(f => new UpdateFeatureDto
{ {
Name = f.Name, Name = f.Name,
Value = f.Type == nameof(ToggleStringValueType) ? f.BoolValue.ToString() : f.Value Value = f.Type == nameof(ToggleStringValueType) ? f.BoolValue.ToString() : f.Value
@ -68,6 +72,11 @@ namespace Volo.Abp.FeatureManagement.Web.Pages.FeatureManagement
public string ProviderKey { get; set; } public string ProviderKey { get; set; }
} }
public class FeatureGroupViewModel
{
public List<FeatureViewModel> Features { get; set; }
}
public class FeatureViewModel public class FeatureViewModel
{ {
public string Name { get; set; } public string Name { get; set; }

4
modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/FeatureAppService_Tests.cs

@ -41,7 +41,7 @@ namespace Volo.Abp.FeatureManagement
TestEditionIds.Regular.ToString()); TestEditionIds.Regular.ToString());
featureList.ShouldNotBeNull(); featureList.ShouldNotBeNull();
featureList.Features.ShouldContain(feature => feature.Name == TestFeatureDefinitionProvider.SocialLogins); featureList.Groups.SelectMany(g =>g .Features).ShouldContain(feature => feature.Name == TestFeatureDefinitionProvider.SocialLogins);
} }
[Fact] [Fact]
@ -63,7 +63,7 @@ namespace Volo.Abp.FeatureManagement
}); });
(await _featureAppService.GetAsync(EditionFeatureValueProvider.ProviderName, (await _featureAppService.GetAsync(EditionFeatureValueProvider.ProviderName,
TestEditionIds.Regular.ToString())).Features.Any(x => TestEditionIds.Regular.ToString())).Groups.SelectMany(g => g.Features).Any(x =>
x.Name == TestFeatureDefinitionProvider.SocialLogins && x.Name == TestFeatureDefinitionProvider.SocialLogins &&
x.Value == false.ToString().ToLowerInvariant()) x.Value == false.ToString().ToLowerInvariant())
.ShouldBeTrue(); .ShouldBeTrue();

82
modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/StringValueJsonConverter_Tests.cs

@ -20,38 +20,46 @@ namespace Volo.Abp.FeatureManagement
[Fact] [Fact]
public void Should_Serialize_And_Deserialize() public void Should_Serialize_And_Deserialize()
{ {
var featureListDto = new FeatureListDto var featureListDto = new GetFeatureListResultDto
{ {
Features = new List<FeatureDto> Groups = new List<FeatureGroupDto>
{ {
new FeatureDto new FeatureGroupDto
{ {
ValueType = new FreeTextStringValueType Name = "MyGroup",
DisplayName = "MyGroup",
Features = new List<FeatureDto>
{ {
Validator = new BooleanValueValidator() new FeatureDto
} {
}, ValueType = new FreeTextStringValueType
new FeatureDto
{
ValueType = new SelectionStringValueType
{
ItemSource = new StaticSelectionStringValueItemSource(
new LocalizableSelectionStringValueItem
{ {
Value = "TestValue", Validator = new BooleanValueValidator()
DisplayText = new LocalizableStringInfo("TestResourceName", "TestName") }
}), },
Validator = new AlwaysValidValueValidator() new FeatureDto
}
},
new FeatureDto
{
ValueType = new ToggleStringValueType
{
Validator = new NumericValueValidator
{ {
MaxValue = 1000, ValueType = new SelectionStringValueType
MinValue = 10 {
ItemSource = new StaticSelectionStringValueItemSource(
new LocalizableSelectionStringValueItem
{
Value = "TestValue",
DisplayText = new LocalizableStringInfo("TestResourceName", "TestName")
}),
Validator = new AlwaysValidValueValidator()
}
},
new FeatureDto
{
ValueType = new ToggleStringValueType
{
Validator = new NumericValueValidator
{
MaxValue = 1000,
MinValue = 10
}
}
} }
} }
} }
@ -59,22 +67,22 @@ namespace Volo.Abp.FeatureManagement
}; };
var serialized = _jsonSerializer.Serialize(featureListDto, indented: true); var serialized = _jsonSerializer.Serialize(featureListDto, indented: true);
var featureListDto2 = _jsonSerializer.Deserialize<FeatureListDto>(serialized); var featureListDto2 = _jsonSerializer.Deserialize<GetFeatureListResultDto>(serialized);
featureListDto2.Features[0].ValueType.ShouldBeOfType<FreeTextStringValueType>(); featureListDto2.Groups[0].Features[0].ValueType.ShouldBeOfType<FreeTextStringValueType>();
featureListDto2.Features[0].ValueType.Validator.ShouldBeOfType<BooleanValueValidator>(); featureListDto2.Groups[0].Features[0].ValueType.Validator.ShouldBeOfType<BooleanValueValidator>();
featureListDto2.Features[1].ValueType.ShouldBeOfType<SelectionStringValueType>(); featureListDto2.Groups[0].Features[1].ValueType.ShouldBeOfType<SelectionStringValueType>();
featureListDto2.Features[1].ValueType.Validator.ShouldBeOfType<AlwaysValidValueValidator>(); featureListDto2.Groups[0].Features[1].ValueType.Validator.ShouldBeOfType<AlwaysValidValueValidator>();
featureListDto2.Features[1].ValueType.As<SelectionStringValueType>().ItemSource.Items.ShouldBeOfType<LocalizableSelectionStringValueItem[]>(); featureListDto2.Groups[0].Features[1].ValueType.As<SelectionStringValueType>().ItemSource.Items.ShouldBeOfType<LocalizableSelectionStringValueItem[]>();
featureListDto2.Features[1].ValueType.As<SelectionStringValueType>().ItemSource.Items.ShouldContain(x => featureListDto2.Groups[0].Features[1].ValueType.As<SelectionStringValueType>().ItemSource.Items.ShouldContain(x =>
x.Value == "TestValue" && x.DisplayText.ResourceName == "TestResourceName" && x.Value == "TestValue" && x.DisplayText.ResourceName == "TestResourceName" &&
x.DisplayText.Name == "TestName"); x.DisplayText.Name == "TestName");
featureListDto2.Features[2].ValueType.ShouldBeOfType<ToggleStringValueType>(); featureListDto2.Groups[0].Features[2].ValueType.ShouldBeOfType<ToggleStringValueType>();
featureListDto2.Features[2].ValueType.Validator.ShouldBeOfType<NumericValueValidator>(); featureListDto2.Groups[0].Features[2].ValueType.Validator.ShouldBeOfType<NumericValueValidator>();
featureListDto2.Features[2].ValueType.Validator.As<NumericValueValidator>().MaxValue.ShouldBe(1000); featureListDto2.Groups[0].Features[2].ValueType.Validator.As<NumericValueValidator>().MaxValue.ShouldBe(1000);
featureListDto2.Features[2].ValueType.Validator.As<NumericValueValidator>().MinValue.ShouldBe(10); featureListDto2.Groups[0].Features[2].ValueType.Validator.As<NumericValueValidator>().MinValue.ShouldBe(10);
} }
} }
} }

Loading…
Cancel
Save