Merge branch 'dev' of https://github.com/abpframework/abp into feat/5026

pull/5333/head
mehmet-erim 5 years ago
commit 9f29468dbc

@ -170,10 +170,14 @@ namespace FeaturesDemo
> ABP automatically discovers this class and registers the features. No additional configuration required.
> This class is generally created in the `Application.Contracts` project of your solution.
* In the `Define` method, you first need to add a **feature group** for your application/module or get an existing group then add **features** to this group.
* First feature, named `MyApp.PdfReporting`, is a `boolean` feature with `false` as the default value.
* Second feature, named `MyApp.MaxProductCount`, is a numeric feature with `10` as the default value.
Default value is used if there is no other value set for the current user/tenant.
### Other Feature Properties
While these minimal definitions are enough to make the feature system working, you can specify the **optional properties** for the features;
@ -339,15 +343,105 @@ See the [features](Features.md) document for the Angular UI.
## Feature Management
TODO
Feature management is normally done by an admin user using the feature management modal:
## Advanced Topics
![features-modal](images/features-modal.png)
This modal is available on the related entities, like tenants in a multi-tenant application. To open it, navigate to the **Tenant Management** page (for a multi-tenant application), click to the **Actions** button left to the Tenant and select the **Features** action.
If you need to manage features by code, inject the `IFeatureManager` service.
**Example: Enable PDF reporting for a tenant**
```csharp
public class MyService : ITransientDependency
{
private readonly IFeatureManager _featureManager;
public MyService(IFeatureManager featureManager)
{
_featureManager = featureManager;
}
TODO
public async Task EnablePdfReporting(Guid tenantId)
{
await _featureManager.SetForTenantAsync(
tenantId,
"MyApp.PdfReporting",
true.ToString()
);
}
}
```
`IFeatureManager` is defined by the Feature Management module. It comes pre-installed with the application startup template. See the [feature management module documentation](Modules/Feature-Management.md) for more information.
## Advanced Topics
### Feature Value Providers
TODO
Feature system is extensible. Any class derived from `FeatureValueProvider` (or implements `IFeatureValueProvider`) can contribute to the feature system. A value provider is responsible to **obtain the current value** of a given feature.
Feature value providers are **executed one by one**. If one of them return a non-null value, then this feature value is used and the other providers are not executed.
There are three pre-defined value providers, executed by the given order:
* `TenantFeatureValueProvider` tries to get if the feature value is explicitly set for the **current tenant**.
* `EditionFeatureValueProvider` tries to get the feature value for the current edition. Edition Id is obtained from the current principal identity (`ICurrentPrincipalAccessor`) with the claim name `editionid` (a constant defined as`AbpClaimTypes.EditionId`). Editions are not implemented for the [tenant management](Modules/Tenant-Management.md) module. You can implement it yourself or consider to use the [SaaS module](https://commercial.abp.io/modules/Volo.Saas) of the ABP Commercial.
* `DefaultValueFeatureValueProvider` gets the default value of the feature.
You can write your own provider by inheriting the `FeatureValueProvider`.
**Example: Enable all features for a user with "SystemAdmin" as a "User_Type" claim value**
```csharp
using System.Threading.Tasks;
using Volo.Abp.Features;
using Volo.Abp.Security.Claims;
using Volo.Abp.Validation.StringValues;
namespace FeaturesDemo
{
public class SystemAdminFeatureValueProvider : FeatureValueProvider
{
public override string Name => "SA";
private readonly ICurrentPrincipalAccessor _currentPrincipalAccessor;
public SystemAdminFeatureValueProvider(
IFeatureStore featureStore,
ICurrentPrincipalAccessor currentPrincipalAccessor)
: base(featureStore)
{
_currentPrincipalAccessor = currentPrincipalAccessor;
}
public override Task<string> GetOrNullAsync(FeatureDefinition feature)
{
if (feature.ValueType is ToggleStringValueType &&
_currentPrincipalAccessor.Principal?.FindFirst("User_Type")?.Value == "SystemAdmin")
{
return Task.FromResult("true");
}
return null;
}
}
}
```
If a provider returns `null`, then the next provider is executed.
Once a provider is defined, it should be added to the `AbpFeatureOptions` as shown below:
```csharp
Configure<AbpFeatureOptions>(options =>
{
options.ValueProviders.Add<SystemAdminFeatureValueProvider>();
});
```
Use this code inside the `ConfigureServices` of your [module](Module-Development-Basics.md) class.
### Feature Store

@ -6,9 +6,21 @@ Explore the navigation menu to deep dive in the documentation.
## Getting Started
The easiest way to start a new web application with the ABP Framework is to use the [getting started](Getting-Started.md) tutorial.
The easiest way to start a new web application with the ABP Framework is to use the [getting started](Getting-Started.md) guide.
Then you can continue with the [web application development tutorial](Tutorials/Part-1.md).
## Tutorials / Articles
### Web Application Development
[Web application development tutorial](Tutorials/Part-1.md) is a complete tutorial to develop a full stack application using the ABP Framework.
### ABP Community Articles
See also the [ABP Community](https://community.abp.io/) articles.
## Samples
See the [sample projects](Samples/Index.md) built with the ABP Framework.
## Source Code

@ -1,3 +1,5 @@
# Feature Management Module
> This module implements the `IFeatureStore` to store and manage feature values in a database. See the [Features System document](../Features.md) to understand the features first.
TODO

@ -1,3 +1,5 @@
# Permission Management Module
This module implements the `IPermissionStore` to store and manage feature values in a database. See the [Authorization document](../Authorization.md) to understand the authorization and permission systems first.
TODO

@ -369,3 +369,6 @@ options.AddDomainTenantResolver("{0}.mydomain.com");
options.AddDomainTenantResolver("{0}.com");
````
## See Also
* [Features](Features.md)

@ -170,6 +170,10 @@
"text": "Settings",
"path": "Settings.md"
},
{
"text": "Features",
"path": "Features.md"
},
{
"text": "Data Filtering",
"path": "Data-Filtering.md"

@ -47,7 +47,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.S
output.PreContent.AppendHtml($"<div class=\"col\"><h4 class=\"card-title my-1\">{Title}</h4></div>");
output.PreContent.AppendHtml("<div class=\"col-auto\">");
output.PreContent.AppendHtml("<nav>");
output.PreContent.AppendHtml("<div class=\"nav nav-tabs nav-pills\" role=\"tablist\">");
output.PreContent.AppendHtml("<div class=\"nav nav-pills\" role=\"tablist\">");
output.PreContent.AppendHtml($"<a class=\"nav-item nav-link active\" id=\"nav-preview-tab-{previewId}\" data-toggle=\"tab\" href=\"#nav-preview-{previewId}\" role=\"tab\" aria-controls=\"nav-preview-{previewId}\" aria-selected=\"true\">Preview</a>");
output.PreContent.AppendHtml($"<a class=\"nav-item nav-link\" id=\"nav-code-tab-{codeBlockId}\" data-toggle=\"tab\" href=\"#nav-code-{codeBlockId}\" role=\"tab\" aria-controls=\"nav-code-{codeBlockId}\" aria-selected=\"false\">Code</a>");
output.PreContent.AppendHtml("</div>");
@ -66,7 +66,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.S
/* CodeBlock tabs */
output.PostContent.AppendHtml($"<ul class=\"nav nav-tabs mb-3\" id=\"code-block-tab-{codeBlockTabId}\" role=\"tablist\">");
output.PostContent.AppendHtml($"<ul class=\"nav nav-tabs\" id=\"code-block-tab-{codeBlockTabId}\" role=\"tablist\">");
output.PostContent.AppendHtml("<li class=\"nav-item\">");
output.PostContent.AppendHtml($"<a class=\"nav-link active\" id=\"tag-helper-tab-{tagHelperCodeBlockId}\" data-toggle=\"pill\" href=\"#tag-helper-{tagHelperCodeBlockId}\" role=\"tab\" aria-controls=\"tag-helper-{tagHelperCodeBlockId}\" aria-selected=\"true\">Abp Tag Helper</a>");
output.PostContent.AppendHtml("</li>");
@ -77,7 +77,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.S
output.PostContent.AppendHtml($"<div class=\"tab-content\" id=\"code-block-tabContent-{codeBlockTabId}\">");
output.PostContent.AppendHtml($"<div class=\"tab-pane fade show active\" id=\"tag-helper-{tagHelperCodeBlockId}\" role=\"tabpanel\" aria-labelledby=\"tag-helper-tab-{tagHelperCodeBlockId}\">");
output.PostContent.AppendHtml("<pre>");
output.PostContent.AppendHtml("<pre class=\"p-4\">");
output.PostContent.AppendHtml("<code>");
output.PostContent.Append(GetRawDemoSource());
output.PostContent.AppendHtml("</code>");
@ -85,7 +85,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.S
output.PostContent.AppendHtml("</div>");
output.PostContent.AppendHtml($"<div class=\"tab-pane fade\" id=\"bootstrap-{bootstrapCodeBlockId}\" role=\"tabpanel\" aria-labelledby=\"bootstrap-tab-{bootstrapCodeBlockId}\">");
output.PostContent.AppendHtml("<pre>");
output.PostContent.AppendHtml("<pre class=\"p-4\">");
output.PostContent.AppendHtml("<code>");
output.PostContent.Append(content.GetContent());
output.PostContent.AppendHtml("</code>");

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

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

@ -175,7 +175,7 @@
</div>
</div>
}
@if (comment.Replies.Count >= 5)
@if (comment.Replies.Count >= 3)
{
<div class="text-right mt-4">
@if (CurrentUser.IsAuthenticated)

@ -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(".", "_");
}
}
}

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

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

@ -6,7 +6,7 @@ namespace Volo.Abp.FeatureManagement
{
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);
}

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Features;
namespace Volo.Abp.FeatureManagement
@ -24,35 +25,49 @@ namespace Volo.Abp.FeatureManagement
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);
var featureDefinitions = FeatureDefinitionManager.GetAll();
var features = new List<FeatureDto>();
var result = new GetFeatureListResultDto
{
Groups = new List<FeatureGroupDto>()
};
foreach (var featureDefinition in featureDefinitions)
foreach (var group in FeatureDefinitionManager.GetGroups())
{
var feature = await FeatureManager.GetOrNullWithProviderAsync(featureDefinition.Name, providerName, providerKey);
features.Add(new FeatureDto
var groupDto = new FeatureGroupDto
{
Name = group.Name,
DisplayName = group.DisplayName.Localize(StringLocalizerFactory),
Features = new List<FeatureDto>()
};
foreach (var featureDefinition in group.GetFeaturesWithChildren())
{
Name = featureDefinition.Name,
DisplayName = featureDefinition.DisplayName?.Localize(StringLocalizerFactory),
ValueType = featureDefinition.ValueType,
Description = featureDefinition.Description?.Localize(StringLocalizerFactory),
ParentName = featureDefinition.Parent?.Name,
Value = feature.Value,
Provider = new FeatureProviderDto
var feature = await FeatureManager.GetOrNullWithProviderAsync(featureDefinition.Name, providerName, providerKey);
groupDto.Features.Add(new FeatureDto
{
Name = feature.Provider?.Name,
Key = feature.Provider?.Key
}
});
}
Name = featureDefinition.Name,
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)

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

@ -4,7 +4,6 @@
@using Volo.Abp.FeatureManagement.Localization
@using Volo.Abp.Validation.StringValues
@using Volo.Abp.FeatureManagement.Web.Pages.FeatureManagement
@using Volo.Abp.Features
@model FeatureManagementModal
@inject IHtmlLocalizer<AbpFeatureManagementResource> L
@{
@ -13,53 +12,67 @@
<form method="post" asp-page="/FeatureManagement/FeatureManagementModal" data-script-class="abp.modals.FeatureManagement">
<abp-modal size="Large">
<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">
<input asp-for="@Model.ProviderKey" />
<input asp-for="@Model.ProviderName" />
@for (var i = 0; i < Model.FeatureListDto.Features.Count; i++)
{
var feature = Model.FeatureListDto.Features[i];
var disabled = Model.IsDisabled(feature.Provider.Name);
<div class="mt-2" style="padding-left: @(feature.Depth * 20)px">
<input asp-for="@Model.ProviderKey"/>
<input asp-for="@Model.ProviderName"/>
<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++)
{
<abp-tab title="@Model.FeatureListResultDto.Groups[i].DisplayName" name="v-pills-tab-@Model.FeatureListResultDto.Groups[i].GetNormalizedGroupName()">
<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="Features[@i].Type" value="@feature.ValueType?.Name" hidden />
@if (feature.ValueType is FreeTextStringValueType)
{
<input type="text" name="Features[@i].Name" value="@feature.Name" hidden />
<input disabled="@disabled" type="text" name="Features[@i].Value" value="@feature.Value" />
}
@if (feature.ValueType is SelectionStringValueType)
{
<input type="text" name="Features[@i].Name" value="@feature.Name" hidden />
<select disabled="@disabled" name="Features[@i].Value">
@foreach (var item in (feature.ValueType as SelectionStringValueType).ItemSource.Items)
{
if (item.Value == feature.Value)
{
<option value="@item.Value" selected="selected"> @L.GetString(item.DisplayText.Name) </option>
}
else
{
<option value="@item.Value"> @L.GetString(item.DisplayText.Name) </option>
<input type="text" name="FeatureGroups[@i].Features[@j].ProviderName" value="@feature.Provider.Name" hidden/>
<input type="text" name="FeatureGroups[@i].Features[@j].Type" value="@feature.ValueType?.Name" hidden/>
@if (feature.ValueType is FreeTextStringValueType)
{
<input type="text" name="FeatureGroups[@i].Features[@j].Name" value="@feature.Name" hidden/>
<input disabled="@disabled" type="text" name="FeatureGroups[@i].Features[@j].Value" value="@feature.Value"/>
}
@if (feature.ValueType is SelectionStringValueType)
{
<input type="text" name="FeatureGroups[@i].Features[@j].Name" value="@feature.Name" hidden/>
<select disabled="@disabled" name="FeatureGroups[@i].Features[@j].Value">
@foreach (var item in (feature.ValueType as SelectionStringValueType).ItemSource.Items)
{
if (item.Value == feature.Value)
{
<option value="@item.Value" selected="selected"> @L.GetString(item.DisplayText.Name) </option>
}
else
{
<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>
}
}
</select>
}
@if (feature.ValueType is ToggleStringValueType)
{
<input type="text" name="Features[@i].Name" value="@feature.Name" hidden />
<input disabled="@disabled" type="checkbox" class="FeatureValueCheckbox" name="Features[@i].BoolValue" value="@feature.Value"
@Html.Raw(feature.Value == "True" ? "checked" : "") />
}
</div>
}
</div>
</div>
</abp-tab>
}
</abp-tabs>
</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
{
@ -69,4 +82,3 @@
}
</abp-modal>
</form>

@ -22,9 +22,9 @@ namespace Volo.Abp.FeatureManagement.Web.Pages.FeatureManagement
public string ProviderKey { get; set; }
[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; }
@ -35,16 +35,20 @@ namespace Volo.Abp.FeatureManagement.Web.Pages.FeatureManagement
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()
{
var features = new UpdateFeaturesDto
{
Features = Features.Select(f => new UpdateFeatureDto
Features = FeatureGroups.SelectMany(g => g.Features).Select(f => new UpdateFeatureDto
{
Name = f.Name,
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 class FeatureGroupViewModel
{
public List<FeatureViewModel> Features { get; set; }
}
public class FeatureViewModel
{
public string Name { get; set; }

@ -41,7 +41,7 @@ namespace Volo.Abp.FeatureManagement
TestEditionIds.Regular.ToString());
featureList.ShouldNotBeNull();
featureList.Features.ShouldContain(feature => feature.Name == TestFeatureDefinitionProvider.SocialLogins);
featureList.Groups.SelectMany(g =>g .Features).ShouldContain(feature => feature.Name == TestFeatureDefinitionProvider.SocialLogins);
}
[Fact]
@ -63,7 +63,7 @@ namespace Volo.Abp.FeatureManagement
});
(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.Value == false.ToString().ToLowerInvariant())
.ShouldBeTrue();

@ -20,38 +20,46 @@ namespace Volo.Abp.FeatureManagement
[Fact]
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 SelectionStringValueType
{
ItemSource = new StaticSelectionStringValueItemSource(
new LocalizableSelectionStringValueItem
new FeatureDto
{
ValueType = new FreeTextStringValueType
{
Value = "TestValue",
DisplayText = new LocalizableStringInfo("TestResourceName", "TestName")
}),
Validator = new AlwaysValidValueValidator()
}
},
new FeatureDto
{
ValueType = new ToggleStringValueType
{
Validator = new NumericValueValidator
Validator = new BooleanValueValidator()
}
},
new FeatureDto
{
MaxValue = 1000,
MinValue = 10
ValueType = new SelectionStringValueType
{
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 featureListDto2 = _jsonSerializer.Deserialize<FeatureListDto>(serialized);
var featureListDto2 = _jsonSerializer.Deserialize<GetFeatureListResultDto>(serialized);
featureListDto2.Features[0].ValueType.ShouldBeOfType<FreeTextStringValueType>();
featureListDto2.Features[0].ValueType.Validator.ShouldBeOfType<BooleanValueValidator>();
featureListDto2.Groups[0].Features[0].ValueType.ShouldBeOfType<FreeTextStringValueType>();
featureListDto2.Groups[0].Features[0].ValueType.Validator.ShouldBeOfType<BooleanValueValidator>();
featureListDto2.Features[1].ValueType.ShouldBeOfType<SelectionStringValueType>();
featureListDto2.Features[1].ValueType.Validator.ShouldBeOfType<AlwaysValidValueValidator>();
featureListDto2.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.ShouldBeOfType<SelectionStringValueType>();
featureListDto2.Groups[0].Features[1].ValueType.Validator.ShouldBeOfType<AlwaysValidValueValidator>();
featureListDto2.Groups[0].Features[1].ValueType.As<SelectionStringValueType>().ItemSource.Items.ShouldBeOfType<LocalizableSelectionStringValueItem[]>();
featureListDto2.Groups[0].Features[1].ValueType.As<SelectionStringValueType>().ItemSource.Items.ShouldContain(x =>
x.Value == "TestValue" && x.DisplayText.ResourceName == "TestResourceName" &&
x.DisplayText.Name == "TestName");
featureListDto2.Features[2].ValueType.ShouldBeOfType<ToggleStringValueType>();
featureListDto2.Features[2].ValueType.Validator.ShouldBeOfType<NumericValueValidator>();
featureListDto2.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.ShouldBeOfType<ToggleStringValueType>();
featureListDto2.Groups[0].Features[2].ValueType.Validator.ShouldBeOfType<NumericValueValidator>();
featureListDto2.Groups[0].Features[2].ValueType.Validator.As<NumericValueValidator>().MaxValue.ShouldBe(1000);
featureListDto2.Groups[0].Features[2].ValueType.Validator.As<NumericValueValidator>().MinValue.ShouldBe(10);
}
}
}

@ -55,6 +55,22 @@ namespace Volo.Abp.Identity
CancellationToken cancellationToken = default
);
Task<List<IdentityRole>> GetUnaddedRolesAsync(
OrganizationUnit organizationUnit,
string sorting = null,
int maxResultCount = int.MaxValue,
int skipCount = 0,
string filter = null,
bool includeDetails = false,
CancellationToken cancellationToken = default
);
Task<int> GetUnaddedRolesCountAsync(
OrganizationUnit organizationUnit,
string filter = null,
CancellationToken cancellationToken = default
);
Task<List<IdentityUser>> GetMembersAsync(
OrganizationUnit organizationUnit,
string sorting = null,
@ -71,6 +87,22 @@ namespace Volo.Abp.Identity
CancellationToken cancellationToken = default
);
Task<List<IdentityUser>> GetUnaddedUsersAsync(
OrganizationUnit organizationUnit,
string sorting = null,
int maxResultCount = int.MaxValue,
int skipCount = 0,
string filter = null,
bool includeDetails = false,
CancellationToken cancellationToken = default
);
Task<int> GetUnaddedUsersCountAsync(
OrganizationUnit organizationUnit,
string filter = null,
CancellationToken cancellationToken = default
);
Task RemoveAllRolesAsync(
OrganizationUnit organizationUnit,
CancellationToken cancellationToken = default

@ -23,16 +23,16 @@ namespace Volo.Abp.Identity
/// <summary>
/// Hierarchical Code of this organization unit.
/// Example: "00001.00042.00005".
/// This is a unique code for a Tenant.
/// This is a unique code for an OrganizationUnit.
/// It's changeable if OU hierarchy is changed.
/// </summary>
public virtual string Code { get; internal set; }
/// <summary>
/// Display name of this role.
/// Display name of this OrganizationUnit.
/// </summary>
public virtual string DisplayName { get; set; }
public virtual string DisplayName { get; set; }
/// <summary>
/// Roles of this OU.
/// </summary>
@ -77,7 +77,7 @@ namespace Volo.Abp.Identity
}
/// <summary>
/// Appends a child code to a parent code.
/// Appends a child code to a parent code.
/// Example: if parentCode = "00001", childCode = "00042" then returns "00001.00042".
/// </summary>
/// <param name="parentCode">Parent code. Can be null or empty if parent is a root.</param>

@ -12,7 +12,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore
{
public class EfCoreOrganizationUnitRepository
: EfCoreRepository<IIdentityDbContext, OrganizationUnit, Guid>,
IOrganizationUnitRepository
IOrganizationUnitRepository
{
public EfCoreOrganizationUnitRepository(
IDbContextProvider<IIdentityDbContext> dbContextProvider)
@ -56,6 +56,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore
.PageBy(skipCount, maxResultCount)
.ToListAsync(GetCancellationToken(cancellationToken));
}
public virtual async Task<List<OrganizationUnit>> GetListAsync(
IEnumerable<Guid> ids,
bool includeDetails = false,
@ -111,6 +112,39 @@ namespace Volo.Abp.Identity.EntityFrameworkCore
return await query.CountAsync(GetCancellationToken(cancellationToken));
}
public virtual async Task<List<IdentityRole>> GetUnaddedRolesAsync(
OrganizationUnit organizationUnit,
string sorting = null,
int maxResultCount = int.MaxValue,
int skipCount = 0,
string filter = null,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
var roleIds = organizationUnit.Roles.Select(r => r.RoleId).ToList();
return await DbContext.Roles
.Where(r => !roleIds.Contains(r.Id))
.IncludeDetails(includeDetails)
.WhereIf(!filter.IsNullOrWhiteSpace(), r => r.Name.Contains(filter))
.OrderBy(sorting ?? nameof(IdentityRole.Name))
.PageBy(skipCount, maxResultCount)
.ToListAsync(cancellationToken);
}
public virtual async Task<int> GetUnaddedRolesCountAsync(
OrganizationUnit organizationUnit,
string filter = null,
CancellationToken cancellationToken = default)
{
var roleIds = organizationUnit.Roles.Select(r => r.RoleId).ToList();
return await DbContext.Roles
.Where(r => !roleIds.Contains(r.Id))
.WhereIf(!filter.IsNullOrWhiteSpace(), r => r.Name.Contains(filter))
.CountAsync(cancellationToken);
}
public virtual async Task<List<IdentityUser>> GetMembersAsync(
OrganizationUnit organizationUnit,
string sorting = null,
@ -118,8 +152,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore
int skipCount = 0,
string filter = null,
bool includeDetails = false,
CancellationToken cancellationToken = default
)
CancellationToken cancellationToken = default)
{
var query = CreateGetMembersFilteredQuery(organizationUnit, filter);
@ -138,6 +171,56 @@ namespace Volo.Abp.Identity.EntityFrameworkCore
return await query.CountAsync(GetCancellationToken(cancellationToken));
}
public virtual async Task<List<IdentityUser>> GetUnaddedUsersAsync(
OrganizationUnit organizationUnit,
string sorting = null,
int maxResultCount = int.MaxValue,
int skipCount = 0,
string filter = null,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
var userIdsInOrganizationUnit = DbContext.Set<IdentityUserOrganizationUnit>()
.Where(uou => uou.OrganizationUnitId == organizationUnit.Id)
.Select(uou => uou.UserId);
var query = DbContext.Users
.Where(u => !userIdsInOrganizationUnit.Contains(u.Id));
if (!filter.IsNullOrWhiteSpace())
{
query = query.Where(u =>
u.UserName.Contains(filter) ||
u.Email.Contains(filter) ||
(u.PhoneNumber != null && u.PhoneNumber.Contains(filter))
);
}
return await query
.IncludeDetails(includeDetails)
.OrderBy(sorting ?? nameof(IdentityUser.Name))
.PageBy(skipCount, maxResultCount)
.ToListAsync(cancellationToken);
}
public virtual async Task<int> GetUnaddedUsersCountAsync(
OrganizationUnit organizationUnit,
string filter = null,
CancellationToken cancellationToken = default)
{
var userIdsInOrganizationUnit = DbContext.Set<IdentityUserOrganizationUnit>()
.Where(uou => uou.OrganizationUnitId == organizationUnit.Id)
.Select(uou => uou.UserId);
return await DbContext.Users
.Where(u => !userIdsInOrganizationUnit.Contains(u.Id))
.WhereIf(!filter.IsNullOrWhiteSpace(), u =>
u.UserName.Contains(filter) ||
u.Email.Contains(filter) ||
(u.PhoneNumber != null && u.PhoneNumber.Contains(filter)))
.CountAsync(cancellationToken);
}
public override IQueryable<OrganizationUnit> WithDetails()
{
return GetQueryable().IncludeDetails();

@ -107,6 +107,38 @@ namespace Volo.Abp.Identity.MongoDB
.CountAsync(cancellationToken);
}
public async Task<List<IdentityRole>> GetUnaddedRolesAsync(
OrganizationUnit organizationUnit,
string sorting = null,
int maxResultCount = int.MaxValue,
int skipCount = 0,
string filter = null,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
var roleIds = organizationUnit.Roles.Select(r => r.RoleId).ToArray();
return await DbContext.Roles.AsQueryable()
.Where(r => !roleIds.Contains(r.Id))
.WhereIf(!filter.IsNullOrWhiteSpace(), r => r.Name.Contains(filter))
.OrderBy(sorting ?? nameof(IdentityRole.Name))
.As<IMongoQueryable<IdentityRole>>()
.PageBy<IdentityRole, IMongoQueryable<IdentityRole>>(skipCount, maxResultCount)
.ToListAsync(cancellationToken);
}
public async Task<int> GetUnaddedRolesCountAsync(
OrganizationUnit organizationUnit,
string filter = null,
CancellationToken cancellationToken = default)
{
var roleIds = organizationUnit.Roles.Select(r => r.RoleId).ToArray();
return await DbContext.Roles.AsQueryable()
.Where(r => !roleIds.Contains(r.Id))
.WhereIf(!filter.IsNullOrWhiteSpace(), r => r.Name.Contains(filter))
.As<IMongoQueryable<IdentityRole>>()
.CountAsync(cancellationToken);
}
public virtual async Task<List<IdentityUser>> GetMembersAsync(
OrganizationUnit organizationUnit,
string sorting = null,
@ -135,6 +167,46 @@ namespace Volo.Abp.Identity.MongoDB
return await query.CountAsync(GetCancellationToken(cancellationToken));
}
public async Task<List<IdentityUser>> GetUnaddedUsersAsync(
OrganizationUnit organizationUnit,
string sorting = null,
int maxResultCount = int.MaxValue,
int skipCount = 0,
string filter = null,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
return await DbContext.Users.AsQueryable()
.Where(u => !u.OrganizationUnits.Any(uou => uou.OrganizationUnitId == organizationUnit.Id))
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(
!filter.IsNullOrWhiteSpace(),
u =>
u.UserName.Contains(filter) ||
u.Email.Contains(filter) ||
(u.PhoneNumber != null && u.PhoneNumber.Contains(filter))
)
.OrderBy(sorting ?? nameof(IdentityUser.UserName))
.As<IMongoQueryable<IdentityUser>>()
.PageBy<IdentityUser, IMongoQueryable<IdentityUser>>(skipCount, maxResultCount)
.ToListAsync(GetCancellationToken(cancellationToken));
}
public async Task<int> GetUnaddedUsersCountAsync(OrganizationUnit organizationUnit, string filter = null,
CancellationToken cancellationToken = default)
{
return await DbContext.Users.AsQueryable()
.Where(u => !u.OrganizationUnits.Any(uou => uou.OrganizationUnitId == organizationUnit.Id))
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(
!filter.IsNullOrWhiteSpace(),
u =>
u.UserName.Contains(filter) ||
u.Email.Contains(filter) ||
(u.PhoneNumber != null && u.PhoneNumber.Contains(filter))
)
.As<IMongoQueryable<IdentityUser>>()
.CountAsync(GetCancellationToken(cancellationToken));
}
public virtual Task RemoveAllRolesAsync(OrganizationUnit organizationUnit, CancellationToken cancellationToken = default)
{
organizationUnit.Roles.Clear();

@ -284,5 +284,43 @@ namespace Volo.Abp.Identity
await uow.CompleteAsync();
}
}
[Fact]
public async Task GetUnaddedUsersOfOrganizationUnitAsync()
{
var ou = await _organizationUnitRepository.GetAsync("OU111", true);
var unaddedUsers = await _organizationUnitRepository.GetUnaddedUsersAsync(ou);
unaddedUsers.ShouldNotContain(u => u.UserName == "john.nash");
unaddedUsers.ShouldContain(u => u.UserName == "administrator");
}
[Fact]
public async Task GetUnaddedRolesOfOrganizationUnitAsync()
{
var ou = await _organizationUnitRepository.GetAsync("OU111", true);
var unaddedRoles = await _organizationUnitRepository.GetUnaddedRolesAsync(ou);
unaddedRoles.ShouldNotContain(u => u.Name == "manager");
unaddedRoles.ShouldNotContain(u => u.Name == "moderator");
unaddedRoles.ShouldContain(u => u.Name.Contains("admin"));
}
[Fact]
public async Task GetUnaddedUsersCountOfOrganizationUnitAsync()
{
var ou = await _organizationUnitRepository.GetAsync("OU111", true);
var count = await _organizationUnitRepository.GetUnaddedUsersCountAsync(ou);
count.ShouldBeGreaterThan(0);
}
[Fact]
public async Task GetUnaddedRolesCountOfOrganizationUnitAsync()
{
var ou = await _organizationUnitRepository.GetAsync("OU111", true);
var count = await _organizationUnitRepository.GetUnaddedRolesCountAsync(ou);
count.ShouldBeGreaterThan(0);
}
}
}

@ -1,4 +1,13 @@
import { TemplateRef, Type } from '@angular/core';
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends Serializable ? DeepPartial<T[P]> : T[P];
};
type Serializable = Record<
string | number | symbol,
string | number | boolean | Record<string | number | symbol, any>
>;
export type InferredInstanceOf<T> = T extends Type<infer U> ? U : never;
export type InferredContextOf<T> = T extends TemplateRef<infer U> ? U : never;

@ -0,0 +1,108 @@
import clone from 'just-clone';
import { take } from 'rxjs/operators';
import { DeepPartial } from '../models';
import { InternalStore } from '../utils';
const mockInitialState = {
foo: {
bar: {
baz: [() => {}],
qux: null as Promise<any>,
},
n: 0,
},
x: '',
a: false,
};
type MockState = typeof mockInitialState;
const patch1: DeepPartial<MockState> = { foo: { bar: { baz: [() => {}] } } };
const expected1: MockState = clone(mockInitialState);
expected1.foo.bar.baz = patch1.foo.bar.baz;
const patch2: DeepPartial<MockState> = { foo: { bar: { qux: Promise.resolve() } } };
const expected2: MockState = clone(mockInitialState);
expected2.foo.bar.qux = patch2.foo.bar.qux;
const patch3: DeepPartial<MockState> = { foo: { n: 1 } };
const expected3: MockState = clone(mockInitialState);
expected3.foo.n = patch3.foo.n;
const patch4: DeepPartial<MockState> = { x: 'X' };
const expected4: MockState = clone(mockInitialState);
expected4.x = patch4.x;
const patch5: DeepPartial<MockState> = { a: true };
const expected5: MockState = clone(mockInitialState);
expected5.a = patch5.a;
describe('Internal Store', () => {
describe('sliceState', () => {
test.each`
selector | expected
${(state: MockState) => state.a} | ${mockInitialState.a}
${(state: MockState) => state.x} | ${mockInitialState.x}
${(state: MockState) => state.foo.n} | ${mockInitialState.foo.n}
${(state: MockState) => state.foo.bar} | ${mockInitialState.foo.bar}
${(state: MockState) => state.foo.bar.baz} | ${mockInitialState.foo.bar.baz}
${(state: MockState) => state.foo.bar.qux} | ${mockInitialState.foo.bar.qux}
`(
'should return observable $expected when selector is $selector',
async ({ selector, expected }) => {
const store = new InternalStore(mockInitialState);
const value = await store
.sliceState(selector)
.pipe(take(1))
.toPromise();
expect(value).toEqual(expected);
},
);
});
describe('patchState', () => {
test.each`
patch | expected
${patch1} | ${expected1}
${patch2} | ${expected2}
${patch3} | ${expected3}
${patch4} | ${expected4}
${patch5} | ${expected5}
`('should set state as $expected when patch is $patch', ({ patch, expected }) => {
const store = new InternalStore(mockInitialState);
store.patch(patch);
expect(store.state).toEqual(expected);
});
});
describe('sliceUpdate', () => {
it('should return slice of update$ based on selector', done => {
const store = new InternalStore(mockInitialState);
const onQux$ = store.sliceUpdate(state => state.foo.bar.qux);
onQux$.pipe(take(1)).subscribe(value => {
expect(value).toEqual(patch2.foo.bar.qux);
done();
});
store.patch(patch1);
store.patch(patch2);
});
});
describe('reset', () => {
it('should reset state to initialState', () => {
const store = new InternalStore(mockInitialState);
store.patch(patch1);
store.reset();
expect(store.state).toEqual(mockInitialState);
});
});
});

@ -6,6 +6,7 @@ export * from './factory-utils';
export * from './form-utils';
export * from './generator-utils';
export * from './initial-utils';
export * from './internal-store-utils';
export * from './lazy-load-utils';
export * from './localization-utils';
export * from './multi-tenancy-utils';

@ -0,0 +1,36 @@
import compare from 'just-compare';
import { BehaviorSubject, Subject } from 'rxjs';
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
import { DeepPartial } from '../models';
import { deepMerge } from './object-utils';
export class InternalStore<State> {
private state$ = new BehaviorSubject<State>(this.initialState);
private update$ = new Subject<DeepPartial<State>>();
get state() {
return this.state$.value;
}
sliceState = <Slice>(
selector: (state: State) => Slice,
compareFn: (s1: Slice, s2: Slice) => boolean = compare,
) => this.state$.pipe(map(selector), distinctUntilChanged(compareFn));
sliceUpdate = <Slice>(
selector: (state: DeepPartial<State>) => Slice,
filterFn = (x: Slice) => x !== undefined,
) => this.update$.pipe(map(selector), filter(filterFn));
constructor(private initialState: State) {}
patch(state: DeepPartial<State>) {
this.state$.next(deepMerge(this.state, state));
this.update$.next(state);
}
reset() {
this.patch(this.initialState);
}
}
Loading…
Cancel
Save