From 70bfb0fafbabc4c5d27514a055b697efa66933ea Mon Sep 17 00:00:00 2001 From: maliming <6908465+maliming@users.noreply.github.com> Date: Tue, 1 Sep 2020 23:02:35 +0800 Subject: [PATCH 01/86] Add IsAvailableToHost & HostFeatureValueProvider & HostFeatureManagementProvider. --- .../Volo/Abp/Features/AbpFeaturesModule.cs | 1 + .../Volo/Abp/Features/FeatureDefinition.cs | 30 ++++++---- .../Abp/Features/HostFeatureValueProvider.cs | 30 ++++++++++ .../AbpFeatureManagementDomainModule.cs | 3 +- .../HostFeatureManagementProvider.cs | 59 +++++++++++++++++++ 5 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 framework/src/Volo.Abp.Features/Volo/Abp/Features/HostFeatureValueProvider.cs create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/HostFeatureManagementProvider.cs diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/AbpFeaturesModule.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/AbpFeaturesModule.cs index f30017feed..7db32231e0 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/AbpFeaturesModule.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/AbpFeaturesModule.cs @@ -26,6 +26,7 @@ namespace Volo.Abp.Features context.Services.Configure(options => { options.ValueProviders.Add(); + options.ValueProviders.Add(); options.ValueProviders.Add(); options.ValueProviders.Add(); }); diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinition.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinition.cs index bd79480326..b2b11f0a18 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinition.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinition.cs @@ -51,6 +51,12 @@ namespace Volo.Abp.Features /// public bool IsVisibleToClients { get; set; } + /// + /// Can host use this feature. + /// Default: true. + /// + public bool IsAvailableToHost { get; set; } + /// /// A list of allowed providers to get/set value of this feature. /// An empty list indicates that all providers are allowed. @@ -93,7 +99,8 @@ namespace Volo.Abp.Features ILocalizableString displayName = null, ILocalizableString description = null, IStringValueType valueType = null, - bool isVisibleToClients = true) + bool isVisibleToClients = true, + bool isAvailableToHost = true) { Name = name; DefaultValue = defaultValue; @@ -101,6 +108,7 @@ namespace Volo.Abp.Features Description = description; ValueType = valueType; IsVisibleToClients = isVisibleToClients; + IsAvailableToHost = isAvailableToHost; Properties = new Dictionary(); AllowedProviders = new List(); @@ -136,20 +144,22 @@ namespace Volo.Abp.Features /// /// Returns a newly created child feature public FeatureDefinition CreateChild( - string name, - string defaultValue = null, - ILocalizableString displayName = null, + string name, + string defaultValue = null, + ILocalizableString displayName = null, ILocalizableString description = null, IStringValueType valueType = null, - bool isVisibleToClients = true) + bool isVisibleToClients = true, + bool isAvailableToHost = true) { var feature = new FeatureDefinition( - name, - defaultValue, - displayName, + name, + defaultValue, + displayName, description, valueType, - isVisibleToClients) + isVisibleToClients, + isAvailableToHost) { Parent = this }; @@ -175,4 +185,4 @@ namespace Volo.Abp.Features return $"[{nameof(FeatureDefinition)}: {Name}]"; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/HostFeatureValueProvider.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/HostFeatureValueProvider.cs new file mode 100644 index 0000000000..d895813e6f --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/HostFeatureValueProvider.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.Features +{ + public class HostFeatureValueProvider : FeatureValueProvider + { + public const string ProviderName = "H"; + + public override string Name => ProviderName; + + protected ICurrentTenant CurrentTenant { get; } + + public HostFeatureValueProvider(IFeatureStore featureStore, ICurrentTenant currentTenant) + : base(featureStore) + { + CurrentTenant = currentTenant; + } + + public override async Task GetOrNullAsync(FeatureDefinition feature) + { + if (CurrentTenant.Id.HasValue || !feature.IsAvailableToHost) + { + return null; + } + + return await FeatureStore.GetOrNullAsync(feature.Name, Name, null); + } + } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs index e8e961d3b4..17e0b9fc04 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs @@ -1,10 +1,8 @@ using Volo.Abp.Caching; using Volo.Abp.FeatureManagement.Localization; using Volo.Abp.Features; -using Volo.Abp.Localization; using Volo.Abp.Localization.ExceptionHandling; using Volo.Abp.Modularity; -using Volo.Abp.VirtualFileSystem; namespace Volo.Abp.FeatureManagement { @@ -20,6 +18,7 @@ namespace Volo.Abp.FeatureManagement Configure(options => { options.Providers.Add(); + options.Providers.Add(); options.Providers.Add(); //TODO: Should be moved to the Tenant Management module diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/HostFeatureManagementProvider.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/HostFeatureManagementProvider.cs new file mode 100644 index 0000000000..d40d2dfdec --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/HostFeatureManagementProvider.cs @@ -0,0 +1,59 @@ +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Features; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.FeatureManagement +{ + public class HostFeatureManagementProvider : FeatureManagementProvider, ITransientDependency + { + public override string Name => HostFeatureValueProvider.ProviderName; + + protected ICurrentTenant CurrentTenant { get; } + + public HostFeatureManagementProvider( + IFeatureManagementStore store, + ICurrentTenant currentTenant) + : base(store) + { + CurrentTenant = currentTenant; + } + + public override async Task GetOrNullAsync(FeatureDefinition feature, string providerKey) + { + if (IsHostSide(feature)) + { + return await Store.GetOrNullAsync(feature.Name, Name, NormalizeProviderKey(providerKey)); + } + + return null; + } + + public override async Task SetAsync(FeatureDefinition feature, string value, string providerKey) + { + if (IsHostSide(feature)) + { + await Store.SetAsync(feature.Name, value, Name, NormalizeProviderKey(providerKey)); + } + } + + public override async Task ClearAsync(FeatureDefinition feature, string providerKey) + { + if (IsHostSide(feature)) + { + await Store.DeleteAsync(feature.Name, Name, NormalizeProviderKey(providerKey)); + } + } + + protected override string NormalizeProviderKey(string providerKey) + { + return null; + } + + //TODO: Should throw an ex when there is not in the host side? + protected virtual bool IsHostSide(FeatureDefinition feature) + { + return feature.IsAvailableToHost && CurrentTenant.Id == null; + } + } +} From 7b09b7e5926db9f80b3f7d14c8b9ac08ad1caf88 Mon Sep 17 00:00:00 2001 From: maliming <6908465+maliming@users.noreply.github.com> Date: Wed, 2 Sep 2020 15:31:42 +0800 Subject: [PATCH 02/86] Manage Host features. --- .../FeatureManagementPermissions.cs | 16 +++++++ .../FeaturePermissionDefinitionProvider.cs | 28 +++++++++++ .../FeatureManagement/IFeatureAppService.cs | 8 +++- .../FeatureManagement/FeatureAppService.cs | 46 +++++++++++++++++++ .../Localization/Domain/en.json | 7 ++- .../Localization/Domain/tr.json | 7 ++- .../Localization/Domain/zh-Hans.json | 7 ++- .../Localization/Domain/zh-Hant.json | 7 ++- .../FeatureManagement/FeaturesController.cs | 16 ++++++- .../FeatureManagementModal.cshtml.cs | 18 +++++++- .../Localization/Resources/en.json | 7 +-- .../Localization/Resources/tr.json | 3 +- .../Localization/Resources/zh-Hans.json | 3 +- .../Localization/Resources/zh-Hant.json | 3 +- .../TenantManagement/Tenants/Index.cshtml | 9 +++- .../Pages/TenantManagement/Tenants/Index.js | 10 +++- 16 files changed, 173 insertions(+), 22 deletions(-) create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeatureManagementPermissions.cs create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeaturePermissionDefinitionProvider.cs diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeatureManagementPermissions.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeatureManagementPermissions.cs new file mode 100644 index 0000000000..86772c0a7b --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeatureManagementPermissions.cs @@ -0,0 +1,16 @@ +using Volo.Abp.Reflection; + +namespace Volo.Abp.FeatureManagement +{ + public class FeatureManagementPermissions + { + public const string GroupName = "FeatureManagement"; + + public const string ManageHostFeatures = GroupName + ".ManageHostFeatures"; + + public static string[] GetAll() + { + return ReflectionHelper.GetPublicConstantsRecursively(typeof(FeatureManagementPermissions)); + } + } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeaturePermissionDefinitionProvider.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeaturePermissionDefinitionProvider.cs new file mode 100644 index 0000000000..27e159ee8a --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeaturePermissionDefinitionProvider.cs @@ -0,0 +1,28 @@ +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.FeatureManagement.Localization; +using Volo.Abp.Localization; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.FeatureManagement +{ + public class FeaturePermissionDefinitionProvider : PermissionDefinitionProvider + { + public override void Define(IPermissionDefinitionContext context) + { + var featureManagementGroup = context.AddGroup( + FeatureManagementPermissions.GroupName, + L("Permission:FeatureManagement"), + multiTenancySide: MultiTenancySides.Host); + + featureManagementGroup.AddPermission( + FeatureManagementPermissions.ManageHostFeatures, + L("Permission:FeatureManagement.ManageHostFeatures"), + multiTenancySide: MultiTenancySides.Host); + } + + private static LocalizableString L(string name) + { + return LocalizableString.Create(name); + } + } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/IFeatureAppService.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/IFeatureAppService.cs index 2c337f1b2a..a2a87b195b 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/IFeatureAppService.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/IFeatureAppService.cs @@ -6,8 +6,12 @@ namespace Volo.Abp.FeatureManagement { public interface IFeatureAppService : IApplicationService { - Task GetAsync([NotNull] string providerName, [NotNull] string providerKey); + Task 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); + + Task GetHostAsync(); + + Task UpdateHostAsync(UpdateFeaturesDto input); } } diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs index f0c310650d..6969739baa 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.AspNetCore.Authorization; @@ -65,6 +66,51 @@ namespace Volo.Abp.FeatureManagement } } + [Authorize(FeatureManagementPermissions.ManageHostFeatures)] + public async Task GetHostAsync() + { + var featureDefinitions = FeatureDefinitionManager.GetAll().Where(x => x.IsAvailableToHost); + var features = new List(); + + foreach (var featureDefinition in featureDefinitions) + { + var feature = await FeatureManager.GetOrNullWithProviderAsync(featureDefinition.Name, HostFeatureValueProvider.ProviderName, null); + features.Add(new FeatureDto + { + 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(features, HostFeatureValueProvider.ProviderName, null); + + return new FeatureListDto { Features = features }; + } + + [Authorize(FeatureManagementPermissions.ManageHostFeatures)] + public async Task UpdateHostAsync(UpdateFeaturesDto input) + { + foreach (var feature in input.Features) + { + var featureDefinition = FeatureDefinitionManager.GetOrNull(feature.Name); + if (featureDefinition == null || !featureDefinition.IsAvailableToHost) + { + throw new UserFriendlyException(L["FeatureNotAvailable"]); + } + + await FeatureManager.SetAsync(feature.Name, feature.Value, HostFeatureValueProvider.ProviderName, null); + } + } + protected virtual void SetFeatureDepth(List features, string providerName, string providerKey, FeatureDto parentFeature = null, int depth = 0) { diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/en.json b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/en.json index e5a2406aaf..6e17e8fc48 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/en.json +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/en.json @@ -2,6 +2,9 @@ "culture": "en", "texts": { "Features": "Features", - "NoFeatureFoundMessage": "There isn't any available feature." + "NoFeatureFoundMessage": "There isn't any available feature.", + "FeatureNotAvailable": "Feature not available.", + "Permission:FeatureManagement": "Feature management", + "Permission:FeatureManagement.ManageHostFeatures": "Manage Host features" } -} \ No newline at end of file +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/tr.json b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/tr.json index b566f654ca..de5ad20624 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/tr.json +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/tr.json @@ -2,6 +2,9 @@ "culture": "tr", "texts": { "Features": "Özellikler", - "NoFeatureFoundMessage": "Hiç özellik yok." + "NoFeatureFoundMessage": "Hiç özellik yok.", + "FeatureNotAvailable": "özelliği mevcut değil.", + "Permission:FeatureManagement": "Özellik yönetimi", + "Permission:FeatureManagement.ManageHostFeatures": "Yönetilen Ana Bilgisayar özellikleri" } -} \ No newline at end of file +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/zh-Hans.json b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/zh-Hans.json index 69b8ca89b1..2ecdb3997b 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/zh-Hans.json +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/zh-Hans.json @@ -2,6 +2,9 @@ "culture": "zh-Hans", "texts": { "Features": "功能", - "NoFeatureFoundMessage": "没有可用的功能." + "NoFeatureFoundMessage": "没有可用的功能.", + "FeatureNotAvailable": "功能不可用.", + "Permission:FeatureManagement": "特性管理", + "Permission:FeatureManagement.ManageHostFeatures": "管理Host特性" } -} \ No newline at end of file +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/zh-Hant.json b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/zh-Hant.json index bc4dbf268f..6a8ccd099b 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/zh-Hant.json +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/zh-Hant.json @@ -2,6 +2,9 @@ "culture": "zh-Hant", "texts": { "Features": "功能", - "NoFeatureFoundMessage": "沒有可用的功能." + "NoFeatureFoundMessage": "沒有可用的功能.", + "FeatureNotAvailable": "功能不可用.", + "Permission:FeatureManagement": "功能管理", + "Permission:FeatureManagement.ManageHostFeatures": "管理Host功能" } -} \ No newline at end of file +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/FeaturesController.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/FeaturesController.cs index 0ae78ed03f..a5c9c1c819 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/FeaturesController.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/FeaturesController.cs @@ -27,5 +27,19 @@ namespace Volo.Abp.FeatureManagement { return FeatureAppService.UpdateAsync(providerName, providerKey, input); } + + [HttpGet] + [Route("host")] + public virtual Task GetHostAsync() + { + return FeatureAppService.GetHostAsync(); + } + + [HttpPut] + [Route("host")] + public virtual Task UpdateHostAsync(UpdateFeaturesDto input) + { + return FeatureAppService.UpdateHostAsync(input); + } } -} \ No newline at end of file +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml.cs index e46d686e69..0fd5d756f7 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml.cs @@ -37,7 +37,14 @@ namespace Volo.Abp.FeatureManagement.Web.Pages.FeatureManagement public virtual async Task OnGetAsync() { - FeatureListDto = await FeatureAppService.GetAsync(ProviderName, ProviderKey); + if (ProviderName == HostFeatureValueProvider.ProviderName) + { + FeatureListDto = await FeatureAppService.GetHostAsync(); + } + else + { + FeatureListDto = await FeatureAppService.GetAsync(ProviderName, ProviderKey); + } } public virtual async Task OnPostAsync() @@ -51,7 +58,14 @@ namespace Volo.Abp.FeatureManagement.Web.Pages.FeatureManagement }).ToList() }; - await FeatureAppService.UpdateAsync(ProviderName, ProviderKey, features); + if (ProviderName == HostFeatureValueProvider.ProviderName) + { + await FeatureAppService.UpdateHostAsync(features); + } + else + { + await FeatureAppService.UpdateAsync(ProviderName, ProviderKey, features); + } return NoContent(); } diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en.json index d6d1d7c155..ba4109c392 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en.json @@ -10,13 +10,14 @@ "ConnectionStrings": "Connection Strings", "DisplayName:DefaultConnectionString": "Default Connection String", "DisplayName:UseSharedDatabase": "Use the Shared Database", + "ManageHostFeatures": "Manage Host features", "Permission:TenantManagement": "Tenant management", "Permission:Create": "Create", "Permission:Edit": "Edit", "Permission:Delete": "Delete", "Permission:ManageConnectionStrings": "Manage connection strings", "Permission:ManageFeatures": "Manage features", - "DisplayName:AdminEmailAddress": "Admin Email Address", - "DisplayName:AdminPassword": "Admin Password" + "DisplayName:AdminEmailAddress": "Admin Email Address", + "DisplayName:AdminPassword": "Admin Password" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/tr.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/tr.json index 72b570f3f9..7315cccc3f 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/tr.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/tr.json @@ -10,6 +10,7 @@ "ConnectionStrings": "Bağlantı cümlesi", "DisplayName:DefaultConnectionString": "Varsayılan bağlantı cümlesi", "DisplayName:UseSharedDatabase": "Paylaşılan veritabanını kullan", + "ManageHostFeatures": "Toplantı Sahibi özelliklerini yönetin", "Permission:TenantManagement": "Müşteri yönetimi", "Permission:Create": "Oluşturma", "Permission:Edit": "Düzenleme", @@ -19,4 +20,4 @@ "DisplayName:AdminEmailAddress": "Admin Eposta Adresi", "DisplayName:AdminPassword": "Admin Şifresi" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/zh-Hans.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/zh-Hans.json index b60c68e2e0..f85f6cf638 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/zh-Hans.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/zh-Hans.json @@ -10,6 +10,7 @@ "ConnectionStrings": "连接字符串", "DisplayName:DefaultConnectionString": "默认连接字符串", "DisplayName:UseSharedDatabase": "使用共享数据库", + "ManageHostFeatures": "管理Host特性", "Permission:TenantManagement": "租户管理", "Permission:Create": "创建", "Permission:Edit": "编辑", @@ -17,4 +18,4 @@ "Permission:ManageConnectionStrings": "管理连接字符串", "Permission:ManageFeatures": "管理功能" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/zh-Hant.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/zh-Hant.json index ea8be8ae0e..6950a1ca00 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/zh-Hant.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/zh-Hant.json @@ -10,13 +10,14 @@ "ConnectionStrings": "資料庫連線字串", "DisplayName:DefaultConnectionString": "預設資料庫連線字串", "DisplayName:UseSharedDatabase": "使用共用資料庫", + "ManageHostFeatures": "管理Host功能", "Permission:TenantManagement": "租戶管理", "Permission:Create": "新增", "Permission:Edit": "編輯", "Permission:Delete": "刪除", "Permission:ManageConnectionStrings": "管理資料庫連線字串", "Permission:ManageFeatures": "管理功能", - "DisplayName:AdminEmailAddress": "管理者信箱", + "DisplayName:AdminEmailAddress": "管理者信箱", "DisplayName:AdminPassword": "管理者密碼" } } diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.cshtml b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.cshtml index eabc75c786..bb972b02d8 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.cshtml +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.cshtml @@ -2,6 +2,7 @@ @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Mvc.Localization @using Volo.Abp.AspNetCore.Mvc.UI.Layout +@using Volo.Abp.FeatureManagement @using Volo.Abp.TenantManagement @using Volo.Abp.TenantManagement.Localization @using Volo.Abp.TenantManagement.Web.Navigation @@ -29,9 +30,13 @@ @L["Tenants"] + @if (await Authorization.IsGrantedAsync(FeatureManagementPermissions.ManageHostFeatures)) + { + + } @if (await Authorization.IsGrantedAsync(TenantManagementPermissions.Tenants.Create)) { - + } @@ -39,4 +44,4 @@ - \ No newline at end of file + diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.js b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.js index 4b19d0bbb7..828f163908 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.js +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.js @@ -100,7 +100,7 @@ }, 0 //adds as the first contributor ); - + $(function () { var _$wrapper = $('#TenantsWrapper'); @@ -128,5 +128,13 @@ e.preventDefault(); _createModal.open(); }); + + _$wrapper.find('button[name=ManageHostFeatures]').click(function (e) { + e.preventDefault(); + _featuresModal.open({ + providerName: 'H', + providerKey: 'H' + }); + }); }); })(); From df780227f5f0486ae0c56d5ae26c326922fd8631 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Sun, 6 Sep 2020 21:05:22 +0300 Subject: [PATCH 03/86] fix: use an intersection type for strict null checks --- npm/ng-packs/packages/core/src/lib/models/config.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/models/config.ts b/npm/ng-packs/packages/core/src/lib/models/config.ts index be640a4dcf..cf4eb007c9 100644 --- a/npm/ng-packs/packages/core/src/lib/models/config.ts +++ b/npm/ng-packs/packages/core/src/lib/models/config.ts @@ -22,11 +22,12 @@ export namespace Config { logoUrl?: string; } - export interface ApiConfig { + export type ApiConfig = { [key: string]: string; - rootNamespace?: string; url: string; - } + } & Partial<{ + rootNamespace: string; + }>; export interface Apis { [key: string]: ApiConfig; From 2811de7a708fc6ce5461726a43f2a58824782632 Mon Sep 17 00:00:00 2001 From: maliming <6908465+maliming@users.noreply.github.com> Date: Mon, 7 Sep 2020 16:11:12 +0800 Subject: [PATCH 04/86] Add more property to user claims. --- .../CurrentUserDto.cs | 10 ++++ .../AbpApplicationConfigurationAppService.cs | 5 ++ .../Volo/Abp/Security/Claims/AbpClaimTypes.cs | 56 ++++++++++++------- .../Volo/Abp/Users/CurrentUser.cs | 6 +- .../Volo/Abp/Users/ICurrentUser.cs | 8 ++- .../Abp/Identity/AbpIdentityDomainModule.cs | 15 ++++- .../Identity/AbpUserClaimsPrincipalFactory.cs | 38 ++++++++++--- .../Abp/Identity/ClaimsIdentityExtensions.cs | 21 +++++++ 8 files changed, 126 insertions(+), 33 deletions(-) create mode 100644 modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/ClaimsIdentityExtensions.cs diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs index ce08bebe87..5d1f6d8414 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs @@ -13,8 +13,18 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations public string UserName { get; set; } + public string Name { get; set; } + + public string SurName { get; set; } + public string Email { get; set; } + public bool EmailVerified { get; set; } + + public string PhoneNumber { get; set; } + + public bool PhoneNumberVerified { get; set; } + public string[] Roles { get; set; } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs index 54815d6ff5..89bf3e7cfc 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs @@ -117,7 +117,12 @@ namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations Id = _currentUser.Id, TenantId = _currentUser.TenantId, UserName = _currentUser.UserName, + SurName = _currentUser.SurName, + Name = _currentUser.Name, Email = _currentUser.Email, + EmailVerified = _currentUser.EmailVerified, + PhoneNumber = _currentUser.PhoneNumber, + PhoneNumberVerified = _currentUser.PhoneNumberVerified, Roles = _currentUser.Roles }; } diff --git a/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/AbpClaimTypes.cs b/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/AbpClaimTypes.cs index d0b883e4f3..a797b360ee 100644 --- a/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/AbpClaimTypes.cs +++ b/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/AbpClaimTypes.cs @@ -8,55 +8,71 @@ namespace Volo.Abp.Security.Claims /// public static class AbpClaimTypes { + public const string ClaimTypeNamespace = "abp_"; + /// - /// Default: + /// Default: abp_username /// - public static string UserName { get; set; } = ClaimTypes.Name; + public static string UserName { get; set; } = ClaimTypeNamespace + "username"; /// - /// Default: + /// Default: abp_name /// - public static string UserId { get; set; } = ClaimTypes.NameIdentifier; + public static string Name { get; set; } = ClaimTypeNamespace + "name"; /// - /// Default: + /// Default: abp_surname /// - public static string Role { get; set; } = ClaimTypes.Role; + public static string SurName { get; set; } = ClaimTypeNamespace + "surname"; /// - /// Default: + /// Default: abp_user_id /// - public static string Email { get; set; } = ClaimTypes.Email; + public static string UserId { get; set; } = ClaimTypeNamespace + "user_id"; /// - /// Default: "email_verified". + /// Default: abp_role /// - public static string EmailVerified { get; set; } = "email_verified"; + public static string Role { get; set; } = ClaimTypeNamespace + "role"; /// - /// Default: "phone_number". + /// Default: abp_security_stamp /// - public static string PhoneNumber { get; set; } = "phone_number"; + public static string SecurityStamp { get; set; } = ClaimTypeNamespace + "security_stamp"; /// - /// Default: "phone_number_verified". + /// Default: abp_email /// - public static string PhoneNumberVerified { get; set; } = "phone_number_verified"; + public static string Email { get; set; } = ClaimTypeNamespace + "email"; /// - /// Default: "tenantid". + /// Default: abp_email_verified /// - public static string TenantId { get; set; } = "tenantid"; + public static string EmailVerified { get; set; } = ClaimTypeNamespace + "email_verified"; + /// + /// Default: abp_phone_number + /// + public static string PhoneNumber { get; set; } = ClaimTypeNamespace + "phone_number"; + + /// + /// Default: abp_phone_number_verified + /// + public static string PhoneNumberVerified { get; set; } = ClaimTypeNamespace + "phone_number_verified"; + + /// + /// Default: abp_tenant_id + /// + public static string TenantId { get; set; } = ClaimTypeNamespace + "tenant_id"; /// - /// Default: "editionid". + /// Default: abp_edition_id /// - public static string EditionId { get; set; } = "editionid"; + public static string EditionId { get; set; } = ClaimTypeNamespace + "edition_id"; /// - /// Default: "client_id". + /// Default: abp_client_id /// - public static string ClientId { get; set; } = "client_id"; + public static string ClientId { get; set; } = ClaimTypeNamespace + "client_id"; } } diff --git a/framework/src/Volo.Abp.Security/Volo/Abp/Users/CurrentUser.cs b/framework/src/Volo.Abp.Security/Volo/Abp/Users/CurrentUser.cs index 487274860f..e769b9ec11 100644 --- a/framework/src/Volo.Abp.Security/Volo/Abp/Users/CurrentUser.cs +++ b/framework/src/Volo.Abp.Security/Volo/Abp/Users/CurrentUser.cs @@ -17,6 +17,10 @@ namespace Volo.Abp.Users public virtual string UserName => this.FindClaimValue(AbpClaimTypes.UserName); + public virtual string Name => this.FindClaimValue(AbpClaimTypes.Name); + + public virtual string SurName => this.FindClaimValue(AbpClaimTypes.SurName); + public virtual string PhoneNumber => this.FindClaimValue(AbpClaimTypes.PhoneNumber); public virtual bool PhoneNumberVerified => string.Equals(this.FindClaimValue(AbpClaimTypes.PhoneNumberVerified), "true", StringComparison.InvariantCultureIgnoreCase); @@ -56,4 +60,4 @@ namespace Volo.Abp.Users return FindClaims(AbpClaimTypes.Role).Any(c => c.Value == roleName); } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Security/Volo/Abp/Users/ICurrentUser.cs b/framework/src/Volo.Abp.Security/Volo/Abp/Users/ICurrentUser.cs index 18613406d8..528f176c94 100644 --- a/framework/src/Volo.Abp.Security/Volo/Abp/Users/ICurrentUser.cs +++ b/framework/src/Volo.Abp.Security/Volo/Abp/Users/ICurrentUser.cs @@ -14,9 +14,15 @@ namespace Volo.Abp.Users [CanBeNull] string UserName { get; } + [CanBeNull] + string Name { get; } + + [CanBeNull] + string SurName { get; } + [CanBeNull] string PhoneNumber { get; } - + bool PhoneNumberVerified { get; } [CanBeNull] diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityDomainModule.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityDomainModule.cs index d21e945f60..685f66a9ea 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityDomainModule.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityDomainModule.cs @@ -8,6 +8,7 @@ using Volo.Abp.Domain.Entities.Events.Distributed; using Volo.Abp.Modularity; using Volo.Abp.ObjectExtending; using Volo.Abp.ObjectExtending.Modularity; +using Volo.Abp.Security.Claims; using Volo.Abp.Users; namespace Volo.Abp.Identity @@ -36,7 +37,7 @@ namespace Volo.Abp.Identity options.EtoMappings.Add(typeof(AbpIdentityDomainModule)); options.EtoMappings.Add(typeof(AbpIdentityDomainModule)); }); - + var identityBuilder = context.Services.AddAbpIdentity(options => { options.User.RequireUniqueEmail = true; @@ -45,6 +46,14 @@ namespace Volo.Abp.Identity context.Services.AddObjectAccessor(identityBuilder); context.Services.ExecutePreConfiguredActions(identityBuilder); + Configure(options => + { + options.ClaimsIdentity.UserIdClaimType = AbpClaimTypes.UserId; + options.ClaimsIdentity.UserNameClaimType = AbpClaimTypes.UserName; + options.ClaimsIdentity.RoleClaimType = AbpClaimTypes.Role; + options.ClaimsIdentity.SecurityStampClaimType = AbpClaimTypes.SecurityStamp; + }); + AddAbpIdentityOptionsFactory(context.Services); } @@ -67,7 +76,7 @@ namespace Volo.Abp.Identity IdentityModuleExtensionConsts.EntityNames.ClaimType, typeof(IdentityClaimType) ); - + ModuleExtensionConfigurationHelper.ApplyEntityConfigurationToEntity( IdentityModuleExtensionConsts.ModuleName, IdentityModuleExtensionConsts.EntityNames.OrganizationUnit, @@ -81,4 +90,4 @@ namespace Volo.Abp.Identity services.Replace(ServiceDescriptor.Scoped, OptionsManager>()); } } -} \ No newline at end of file +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpUserClaimsPrincipalFactory.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpUserClaimsPrincipalFactory.cs index 8db2b95942..c9e266bf23 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpUserClaimsPrincipalFactory.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpUserClaimsPrincipalFactory.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; @@ -6,6 +7,7 @@ using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; using Volo.Abp.Security.Claims; using Volo.Abp.Uow; +using Volo.Abp.Users; namespace Volo.Abp.Identity { @@ -13,11 +15,11 @@ namespace Volo.Abp.Identity { public AbpUserClaimsPrincipalFactory( UserManager userManager, - RoleManager roleManager, - IOptions options) + RoleManager roleManager, + IOptions options) : base( - userManager, - roleManager, + userManager, + roleManager, options) { } @@ -26,14 +28,34 @@ namespace Volo.Abp.Identity public override async Task CreateAsync(IdentityUser user) { var principal = await base.CreateAsync(user); + var identity = principal.Identities.First(); if (user.TenantId.HasValue) { - principal.Identities - .First() - .AddClaim(new Claim(AbpClaimTypes.TenantId, user.TenantId.ToString())); + identity.AddIfNotContains(new Claim(AbpClaimTypes.TenantId, user.TenantId.ToString())); } + if (!user.Name.IsNullOrWhiteSpace()) + { + identity.AddIfNotContains(new Claim(AbpClaimTypes.Name, user.Name)); + } + if (!user.Surname.IsNullOrWhiteSpace()) + { + identity.AddIfNotContains(new Claim(AbpClaimTypes.SurName, user.Surname)); + } + + if (!user.PhoneNumber.IsNullOrWhiteSpace()) + { + identity.AddIfNotContains(new Claim(AbpClaimTypes.PhoneNumber, user.PhoneNumber)); + } + identity.AddIfNotContains(new Claim(AbpClaimTypes.PhoneNumberVerified, user.PhoneNumberConfirmed.ToString())); + + if (!user.Email.IsNullOrWhiteSpace()) + { + identity.AddIfNotContains(new Claim(AbpClaimTypes.Email, user.Email)); + } + identity.AddIfNotContains(new Claim(AbpClaimTypes.EmailVerified, user.EmailConfirmed.ToString())); + return principal; } } diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/ClaimsIdentityExtensions.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/ClaimsIdentityExtensions.cs new file mode 100644 index 0000000000..156f092f9e --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/ClaimsIdentityExtensions.cs @@ -0,0 +1,21 @@ +using System; +using System.Linq; +using System.Security.Claims; + +namespace Volo.Abp.Identity +{ + public static class ClaimsIdentityExtensions + { + public static ClaimsIdentity AddIfNotContains(this ClaimsIdentity claimsIdentity, Claim claim) + { + if (!claimsIdentity.Claims.Any(existClaim => + existClaim != null && + string.Equals(existClaim.Type, claim.Type, StringComparison.OrdinalIgnoreCase))) + { + claimsIdentity.AddClaim(claim); + } + + return claimsIdentity; + } + } +} From 2c6cd37af2f7e7a70565e4f91f971fd281074e70 Mon Sep 17 00:00:00 2001 From: maliming <6908465+maliming@users.noreply.github.com> Date: Mon, 7 Sep 2020 16:54:00 +0800 Subject: [PATCH 05/86] Fix unit test. --- .../Volo/Abp/Authorization/AuthorizationTestBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/AuthorizationTestBase.cs b/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/AuthorizationTestBase.cs index 5cf9976e46..89f45600f4 100644 --- a/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/AuthorizationTestBase.cs +++ b/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/AuthorizationTestBase.cs @@ -23,7 +23,7 @@ namespace Volo.Abp.Authorization new Claim(AbpClaimTypes.Role, "MyRole") }; - var identity = new ClaimsIdentity(claims); + var identity = new ClaimsIdentity(claims,null, AbpClaimTypes.UserName, AbpClaimTypes.Role); var claimsPrincipal = new ClaimsPrincipal(identity); var principalAccessor = Substitute.For(); principalAccessor.Principal.Returns(ci => claimsPrincipal); From 80a1b0bfdb35faca35f8d7ef5161d7c6b211eca9 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Mon, 7 Sep 2020 17:22:02 +0800 Subject: [PATCH 06/86] Add unadded users/roles method to OrganizationUnitRepository --- .../Identity/IOrganizationUnitRepository.cs | 16 ++++++ .../Volo/Abp/Identity/OrganizationUnit.cs | 10 ++-- .../EfCoreOrganizationUnitRepository.cs | 52 +++++++++++++++++-- .../MongoOrganizationUnitRepository.cs | 37 +++++++++++++ .../OrganizationUnitRepository_Tests.cs | 21 ++++++++ 5 files changed, 128 insertions(+), 8 deletions(-) diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs index 6650f62c82..81474425db 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs @@ -55,6 +55,14 @@ namespace Volo.Abp.Identity CancellationToken cancellationToken = default ); + Task> GetUnaddedRolesAsync( + OrganizationUnit organizationUnit, + string filter = null, + string sorting = null, + bool includeDetails = false, + CancellationToken cancellationToken = default + ); + Task> GetMembersAsync( OrganizationUnit organizationUnit, string sorting = null, @@ -71,6 +79,14 @@ namespace Volo.Abp.Identity CancellationToken cancellationToken = default ); + Task> GetUnaddedUsersAsync( + OrganizationUnit organizationUnit, + string filter = null, + string sorting = null, + bool includeDetails = false, + CancellationToken cancellationToken = default + ); + Task RemoveAllRolesAsync( OrganizationUnit organizationUnit, CancellationToken cancellationToken = default diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/OrganizationUnit.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/OrganizationUnit.cs index 3d1bbb0531..981a0f8fae 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/OrganizationUnit.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/OrganizationUnit.cs @@ -23,16 +23,16 @@ namespace Volo.Abp.Identity /// /// 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. /// public virtual string Code { get; internal set; } /// - /// Display name of this role. + /// Display name of this OrganizationUnit. /// - public virtual string DisplayName { get; set; } - + public virtual string DisplayName { get; set; } + /// /// Roles of this OU. /// @@ -77,7 +77,7 @@ namespace Volo.Abp.Identity } /// - /// 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". /// /// Parent code. Can be null or empty if parent is a root. diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs index 7bdb7d6837..d50745ab1d 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs @@ -12,7 +12,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore { public class EfCoreOrganizationUnitRepository : EfCoreRepository, - IOrganizationUnitRepository + IOrganizationUnitRepository { public EfCoreOrganizationUnitRepository( IDbContextProvider dbContextProvider) @@ -56,6 +56,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore .PageBy(skipCount, maxResultCount) .ToListAsync(GetCancellationToken(cancellationToken)); } + public virtual async Task> GetListAsync( IEnumerable ids, bool includeDetails = false, @@ -111,6 +112,23 @@ namespace Volo.Abp.Identity.EntityFrameworkCore return await query.CountAsync(GetCancellationToken(cancellationToken)); } + public virtual async Task> GetUnaddedRolesAsync( + OrganizationUnit organizationUnit, + string filter = null, + string sorting = 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)) + .ToListAsync(cancellationToken); + } + public virtual async Task> GetMembersAsync( OrganizationUnit organizationUnit, string sorting = null, @@ -118,8 +136,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 +155,35 @@ namespace Volo.Abp.Identity.EntityFrameworkCore return await query.CountAsync(GetCancellationToken(cancellationToken)); } + public virtual async Task> GetUnaddedUsersAsync( + OrganizationUnit organizationUnit, + string filter = null, + string sorting = null, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + var userIdsInOrganizationUnit = DbContext.Set() + .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)) + .ToListAsync(cancellationToken); + } + public override IQueryable WithDetails() { return GetQueryable().IncludeDetails(); diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs index 8c163d4126..2823c99a6c 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs @@ -107,6 +107,22 @@ namespace Volo.Abp.Identity.MongoDB .CountAsync(cancellationToken); } + public async Task> GetUnaddedRolesAsync( + OrganizationUnit organizationUnit, + string filter = null, + string sorting = 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>() + .ToListAsync(cancellationToken); + } + public virtual async Task> GetMembersAsync( OrganizationUnit organizationUnit, string sorting = null, @@ -135,6 +151,27 @@ namespace Volo.Abp.Identity.MongoDB return await query.CountAsync(GetCancellationToken(cancellationToken)); } + public async Task> GetUnaddedUsersAsync( + OrganizationUnit organizationUnit, + string filter = null, + string sorting = null, + bool includeDetails = false, + CancellationToken cancellationToken = default) + { + return await DbContext.Users.AsQueryable() + .Where(u => !u.OrganizationUnits.Any(uou => uou.OrganizationUnitId == organizationUnit.Id)) + .WhereIf>( + !filter.IsNullOrWhiteSpace(), + u => + u.UserName.Contains(filter) || + u.Email.Contains(filter) || + (u.PhoneNumber != null && u.PhoneNumber.Contains(filter)) + ) + .OrderBy(sorting ?? nameof(IdentityUser.UserName)) + .As>() + .ToListAsync(GetCancellationToken(cancellationToken)); + } + public virtual Task RemoveAllRolesAsync(OrganizationUnit organizationUnit, CancellationToken cancellationToken = default) { organizationUnit.Roles.Clear(); diff --git a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs index bb8300d449..97544cd1e9 100644 --- a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs @@ -284,5 +284,26 @@ 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")); + } } } From 6845b0a58804ba1a61c73504985fbb01446282c6 Mon Sep 17 00:00:00 2001 From: maliming <6908465+maliming@users.noreply.github.com> Date: Mon, 7 Sep 2020 20:18:24 +0800 Subject: [PATCH 07/86] Update modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/tr.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Halil İbrahim Kalkan --- .../Volo/Abp/FeatureManagement/Localization/Domain/tr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/tr.json b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/tr.json index de5ad20624..a7dab6dac3 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/tr.json +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/tr.json @@ -3,7 +3,7 @@ "texts": { "Features": "Özellikler", "NoFeatureFoundMessage": "Hiç özellik yok.", - "FeatureNotAvailable": "özelliği mevcut değil.", + "FeatureNotAvailable": "Özellik bulunamadı.", "Permission:FeatureManagement": "Özellik yönetimi", "Permission:FeatureManagement.ManageHostFeatures": "Yönetilen Ana Bilgisayar özellikleri" } From e2bba2941eb8125276c5024100ec0260c3dc2a8c Mon Sep 17 00:00:00 2001 From: maliming <6908465+maliming@users.noreply.github.com> Date: Mon, 7 Sep 2020 20:18:41 +0800 Subject: [PATCH 08/86] Update modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/tr.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Halil İbrahim Kalkan --- .../Volo/Abp/FeatureManagement/Localization/Domain/tr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/tr.json b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/tr.json index a7dab6dac3..285b4143cf 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/tr.json +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/tr.json @@ -5,6 +5,6 @@ "NoFeatureFoundMessage": "Hiç özellik yok.", "FeatureNotAvailable": "Özellik bulunamadı.", "Permission:FeatureManagement": "Özellik yönetimi", - "Permission:FeatureManagement.ManageHostFeatures": "Yönetilen Ana Bilgisayar özellikleri" + "Permission:FeatureManagement.ManageHostFeatures": "Host özelliklerini düzenle" } } From dfd2baee7f8ad0fef68eff134014447084555c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Mon, 7 Sep 2020 17:25:44 +0300 Subject: [PATCH 09/86] Stared to document the feature system --- docs/en/Features.md | 46 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/docs/en/Features.md b/docs/en/Features.md index 4b31ca16e9..d5262c6cdb 100644 --- a/docs/en/Features.md +++ b/docs/en/Features.md @@ -1,3 +1,47 @@ # Features -TODO \ No newline at end of file +ABP Feature system is used to **enable**, **disable** or **change the behavior** of the application features **on runtime**. + +Feature system was originally designed to control the tenant features in a **multi-tenant** application. However, it is **extensible** and capable of determining features by any condition. + +> The feature system is implemented with the [Volo.Abp.Features](https://www.nuget.org/packages/Volo.Abp.Features) NuGet package. Most of the times you don't need to manually [install it](https://abp.io/package-detail/Volo.Abp.Features) since it comes pre-installed with the [application startup template](Startup-Templates/Application.md). + +## Checking for the Features + +Before starting to explain how to define features, let's see how to check a feature in your application code. + +### RequiresFeature Attribute + +`[RequiresFeature]` attribute (defined in the `Volo.Abp.Features` namespace) is used to declaratively check if a feature is enabled or not. + +**Example: Check if the current user/tenant has "PDF Reporting" feature enabled** + +```csharp +public class ReportingAppService : ApplicationService, IReportingAppService +{ + public async Task GetCsvReportAsync() + { + throw new System.NotImplementedException(); + } + + [RequiresFeature("MyApp.PdfReporting")] + public async Task GetPdfReportAsync() + { + throw new System.NotImplementedException(); + } +} +``` + +* `RequiresFeature(...)` simply gets a feature name to check if it is enabled or not. If not enabled, an authorization [exception](Exception-Handling.md) is thrown and a proper response is returned to the client side. +* `[RequiresFeature]` can be used for a **method** or a **class**. When you use it for a class, all the +* `RequiresFeature` may get multiple feature names, like `[RequiresFeature("Feature1", "Feature2")]`. In this case ABP checks if current user/tenant has any of the features enabled. Use `[RequiresFeature("Feature1", "Feature2", RequiresAll = true)]` to force to allow only if all of the features are enabled. +* Multiple usage of `[RequiresFeature]` attribute is enabled, so it checks all of them. + +#### About the Interception + +ABP Framework uses the interception system to make the `[RequiresFeature]` attribute working. So, it can work with any class that is injected from the [dependency injection](Dependency-Injection.md). + +However, there are **some rules should be followed** in order to make it working; + +* If you are **not injecting** the service over an interface (like `IMyService`), then the methods of the service must be `virtual` (otherwise, [dynamic proxy / interception](Dynamic-Proxying-Interceptors.md) system can not work). +* Only `async` methods (methods returning a `Task` or `Task`) are intercepted. From 735a0c2db68c054ad456afe4f6231317191bb5d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Mon, 7 Sep 2020 18:11:10 +0300 Subject: [PATCH 10/86] Update Features.md --- docs/en/Features.md | 115 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 103 insertions(+), 12 deletions(-) diff --git a/docs/en/Features.md b/docs/en/Features.md index d5262c6cdb..2c859d643f 100644 --- a/docs/en/Features.md +++ b/docs/en/Features.md @@ -2,6 +2,8 @@ ABP Feature system is used to **enable**, **disable** or **change the behavior** of the application features **on runtime**. +The runtime value for a feature can simply be a `boolean`, like enabled (`true`) or disabled (`false`). However, you can store **any kind** of runtime value for feature. + Feature system was originally designed to control the tenant features in a **multi-tenant** application. However, it is **extensible** and capable of determining features by any condition. > The feature system is implemented with the [Volo.Abp.Features](https://www.nuget.org/packages/Volo.Abp.Features) NuGet package. Most of the times you don't need to manually [install it](https://abp.io/package-detail/Volo.Abp.Features) since it comes pre-installed with the [application startup template](Startup-Templates/Application.md). @@ -12,36 +14,125 @@ Before starting to explain how to define features, let's see how to check a feat ### RequiresFeature Attribute -`[RequiresFeature]` attribute (defined in the `Volo.Abp.Features` namespace) is used to declaratively check if a feature is enabled or not. +`[RequiresFeature]` attribute (defined in the `Volo.Abp.Features` namespace) is used to declaratively check if a feature is enabled or not. It is a useful shortcut for the `boolean` features. -**Example: Check if the current user/tenant has "PDF Reporting" feature enabled** +**Example: Check if the "PDF Reporting" feature enabled** ```csharp public class ReportingAppService : ApplicationService, IReportingAppService { - public async Task GetCsvReportAsync() - { - throw new System.NotImplementedException(); - } - [RequiresFeature("MyApp.PdfReporting")] public async Task GetPdfReportAsync() { - throw new System.NotImplementedException(); + //TODO... } } ``` * `RequiresFeature(...)` simply gets a feature name to check if it is enabled or not. If not enabled, an authorization [exception](Exception-Handling.md) is thrown and a proper response is returned to the client side. -* `[RequiresFeature]` can be used for a **method** or a **class**. When you use it for a class, all the -* `RequiresFeature` may get multiple feature names, like `[RequiresFeature("Feature1", "Feature2")]`. In this case ABP checks if current user/tenant has any of the features enabled. Use `[RequiresFeature("Feature1", "Feature2", RequiresAll = true)]` to force to allow only if all of the features are enabled. -* Multiple usage of `[RequiresFeature]` attribute is enabled, so it checks all of them. +* `[RequiresFeature]` can be used for a **method** or a **class**. When you use it for a class, all the methods of that class require the given feature. +* `RequiresFeature` may get multiple feature names, like `[RequiresFeature("Feature1", "Feature2")]`. In this case ABP checks if any of the features enabled. Use `RequiresAll` option, like `[RequiresFeature("Feature1", "Feature2", RequiresAll = true)]` to force to check all of the features to be enabled. +* Multiple usage of `[RequiresFeature]` attribute is supported for a method or class. ABP check checks all of them in that case. + +> Feature name can be any arbitrary string. It should be unique for a feature. #### About the Interception -ABP Framework uses the interception system to make the `[RequiresFeature]` attribute working. So, it can work with any class that is injected from the [dependency injection](Dependency-Injection.md). +ABP Framework uses the interception system to make the `[RequiresFeature]` attribute working. So, it can work with any class (application services, controllers...) that is injected from the [dependency injection](Dependency-Injection.md). However, there are **some rules should be followed** in order to make it working; * If you are **not injecting** the service over an interface (like `IMyService`), then the methods of the service must be `virtual` (otherwise, [dynamic proxy / interception](Dynamic-Proxying-Interceptors.md) system can not work). * Only `async` methods (methods returning a `Task` or `Task`) are intercepted. + +> There is an exception for the controllers and razor pages. They don't require the following rules since ABP Framework uses action/page filters to implement the feature checking in this case. + +### IFeatureChecker Service + +`IFeatureChecker` allows to check a feature in your application code. + +#### IsEnabledAsync + +Returns `true` if the given feature is enabled. So, you can conditionally execute your business flow. + +**Example: Check if the "PDF Reporting" feature enabled** + +```csharp +public class ReportingAppService : ApplicationService, IReportingAppService +{ + private readonly IFeatureChecker _featureChecker; + + public ReportingAppService(IFeatureChecker featureChecker) + { + _featureChecker = featureChecker; + } + + public async Task GetPdfReportAsync() + { + if (await _featureChecker.IsEnabledAsync("MyApp.PdfReporting")) + { + //TODO... + } + else + { + //TODO... + } + } +} +``` + +`IsEnabledAsync` has overloads to check multiple features in one method call. + +#### GetOrNullAsync + +Gets the current value for a feature. This method returns a `string`, so you store any kind of value inside it, by converting to or from `string`. + +**Example: Check the maximum product count allowed** + +```csharp +public class ProductController : AbpController +{ + private readonly IFeatureChecker _featureChecker; + + public ProductController(IFeatureChecker featureChecker) + { + _featureChecker = featureChecker; + } + + public async Task Create(CreateProductModel model) + { + var currentProductCount = await GetCurrentProductCountFromDatabase(); + + //GET THE FEATURE VALUE + var maxProductCountLimit = + await _featureChecker.GetOrNullAsync("MyApp.MaxProductCount"); + + if (currentProductCount >= Convert.ToInt32(maxProductCountLimit)) + { + throw new BusinessException( + "MyApp:ReachToMaxProductCountLimit", + $"You can not create more than {maxProductCountLimit} products!" + ); + } + + //TODO: Create the product in the database... + } + + private async Task GetCurrentProductCountFromDatabase() + { + throw new System.NotImplementedException(); + } +} +``` + +In this way, you can create numeric limits in your SaaS application depending on the current user or tenant. + +Instead of manually converting the value to `int`, you can use the generic overload of the `GetAsync` method: + +```csharp +var maxProductCountLimit = await _featureChecker.GetAsync("MyApp.MaxProductCount"); +``` + +#### Extension Methods + +TODO \ No newline at end of file From 460686a22d003e0475a62402d3f56b5e015e819f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Mon, 7 Sep 2020 20:46:43 +0300 Subject: [PATCH 11/86] Added empty sections to the features document --- docs/en/Features.md | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/docs/en/Features.md b/docs/en/Features.md index 2c859d643f..82d1790b9e 100644 --- a/docs/en/Features.md +++ b/docs/en/Features.md @@ -2,19 +2,19 @@ ABP Feature system is used to **enable**, **disable** or **change the behavior** of the application features **on runtime**. -The runtime value for a feature can simply be a `boolean`, like enabled (`true`) or disabled (`false`). However, you can store **any kind** of runtime value for feature. +The runtime value for a feature is generally a `boolean` value, like `true` (enabled) or `false` (disabled). However, you can get/set **any kind** of value for feature. -Feature system was originally designed to control the tenant features in a **multi-tenant** application. However, it is **extensible** and capable of determining features by any condition. +Feature system was originally designed to control the tenant features in a **multi-tenant** application. However, it is **extensible** and capable of determining the features by any condition. > The feature system is implemented with the [Volo.Abp.Features](https://www.nuget.org/packages/Volo.Abp.Features) NuGet package. Most of the times you don't need to manually [install it](https://abp.io/package-detail/Volo.Abp.Features) since it comes pre-installed with the [application startup template](Startup-Templates/Application.md). ## Checking for the Features -Before starting to explain how to define features, let's see how to check a feature in your application code. +Before explaining to define features, let's see how to check a feature value in your application code. ### RequiresFeature Attribute -`[RequiresFeature]` attribute (defined in the `Volo.Abp.Features` namespace) is used to declaratively check if a feature is enabled or not. It is a useful shortcut for the `boolean` features. +`[RequiresFeature]` attribute (defined in the `Volo.Abp.Features` namespace) is used to declaratively check if a feature is `true` (enabled) or not. It is a useful shortcut for the `boolean` features. **Example: Check if the "PDF Reporting" feature enabled** @@ -42,10 +42,10 @@ ABP Framework uses the interception system to make the `[RequiresFeature]` attri However, there are **some rules should be followed** in order to make it working; -* If you are **not injecting** the service over an interface (like `IMyService`), then the methods of the service must be `virtual` (otherwise, [dynamic proxy / interception](Dynamic-Proxying-Interceptors.md) system can not work). +* If you are **not injecting** the service over an interface (like `IMyService`), then the methods of the service must be `virtual`. Otherwise, [dynamic proxy / interception](Dynamic-Proxying-Interceptors.md) system can not work. * Only `async` methods (methods returning a `Task` or `Task`) are intercepted. -> There is an exception for the controllers and razor pages. They don't require the following rules since ABP Framework uses action/page filters to implement the feature checking in this case. +> There is an exception for the **controller and razor page methods**. They **don't require** the following the rules above, since ABP Framework uses the action/page filters to implement the feature checking in this case. ### IFeatureChecker Service @@ -125,7 +125,7 @@ public class ProductController : AbpController } ``` -In this way, you can create numeric limits in your SaaS application depending on the current user or tenant. +This example uses a numeric value as a feature limit product counts for a user/tenant in a SaaS application. Instead of manually converting the value to `int`, you can use the generic overload of the `GetAsync` method: @@ -135,4 +135,27 @@ var maxProductCountLimit = await _featureChecker.GetAsync("MyApp.MaxProduct #### Extension Methods +There are some useful extension methods for the `IFeatureChecker` interface; + +* `Task GetAsync(string name, T defaultValue = default)`: Used to get a value of a feature with the given type `T`. Allows to specify a `defaultValue` that is returned when the feature value is `null`. +* `CheckEnabledAsync(string name)`: Checks if given feature is enabled. Throws an `AbpAuthorizationException` if the feature was not `true` (enabled). + +## Defining the Features + +TODO + +## Feature Management + +TODO + +## Advanced Topics + +TODO + +### Feature Value Providers + +TODO + +### Feature Store + TODO \ No newline at end of file From 01e612ee1082fb8b0c398f208b520716c7f2dbe2 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Mon, 7 Sep 2020 20:52:17 +0300 Subject: [PATCH 12/86] fix: avoid horizontal gap in datatable on resize --- .../ngx-datatable-default.directive.ts | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts b/npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts index ec6710474f..64a64af4eb 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts @@ -1,12 +1,19 @@ -import { Directive, HostBinding, Input } from '@angular/core'; -import { ColumnMode, DatatableComponent } from '@swimlane/ngx-datatable'; +import { DOCUMENT } from '@angular/common'; +import { AfterViewInit, Directive, HostBinding, Inject, Input, OnDestroy } from '@angular/core'; +import { ColumnMode, DatatableComponent, ScrollerComponent } from '@swimlane/ngx-datatable'; +import { fromEvent, Subscription } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; @Directive({ // tslint:disable-next-line selector: 'ngx-datatable[default]', exportAs: 'ngxDatatableDefault', }) -export class NgxDatatableDefaultDirective { +export class NgxDatatableDefaultDirective implements AfterViewInit, OnDestroy { + private subscription = new Subscription(); + + private resizeDiff = 0; + @Input() class = 'material bordered'; @HostBinding('class') @@ -14,7 +21,7 @@ export class NgxDatatableDefaultDirective { return `ngx-datatable ${this.class}`; } - constructor(private table: DatatableComponent) { + constructor(private table: DatatableComponent, @Inject(DOCUMENT) private document: Document) { this.table.columnMode = ColumnMode.force; this.table.footerHeight = 50; this.table.headerHeight = 50; @@ -22,4 +29,41 @@ export class NgxDatatableDefaultDirective { this.table.scrollbarH = true; this.table.virtualization = false; } + + private fixHorizontalGap(scroller: ScrollerComponent) { + const { body, documentElement } = this.document; + + if (documentElement.scrollHeight !== documentElement.clientHeight) { + if (this.resizeDiff === 0) { + this.resizeDiff = window.innerWidth - body.offsetWidth; + scroller.scrollWidth -= this.resizeDiff; + } + } else { + scroller.scrollWidth += this.resizeDiff; + this.resizeDiff = 0; + } + } + + private fixStyleOnWindowResize() { + // avoided @HostListener('window:resize') in favor of performance + const subscription = fromEvent(window, 'resize') + .pipe(debounceTime(500)) + .subscribe(() => { + const { scroller } = this.table.bodyComponent; + + if (!scroller) return; + + this.fixHorizontalGap(scroller); + }); + + this.subscription.add(subscription); + } + + ngAfterViewInit() { + this.fixStyleOnWindowResize(); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } } From 01cd1d56920bfca89bb7b193b2a934c9d542394c Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Mon, 7 Sep 2020 21:21:12 +0300 Subject: [PATCH 13/86] fix: avoid unnecessary scrolls on datatable --- .../packages/theme-shared/src/lib/constants/styles.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/npm/ng-packs/packages/theme-shared/src/lib/constants/styles.ts b/npm/ng-packs/packages/theme-shared/src/lib/constants/styles.ts index a3698f2164..a54aecf581 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/constants/styles.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/constants/styles.ts @@ -32,6 +32,11 @@ export default ` min-width: 215px; } +.datatable-scroll { + margin-bottom: 5px !important; + width: unset !important; +} + .ui-table-scrollable-body::-webkit-scrollbar { height: 5px !important; width: 5px !important; From 60a54bc9f126cad6ce7f3a15200cf0e0c9f95c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Mon, 7 Sep 2020 21:37:55 +0300 Subject: [PATCH 14/86] Added section: Defining the Features. --- docs/en/Features.md | 125 ++++++++++++++++++++++++++++- docs/en/images/features-action.png | Bin 0 -> 16742 bytes docs/en/images/features-modal.png | Bin 0 -> 35412 bytes 3 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 docs/en/images/features-action.png create mode 100644 docs/en/images/features-modal.png diff --git a/docs/en/Features.md b/docs/en/Features.md index 82d1790b9e..e125f16f08 100644 --- a/docs/en/Features.md +++ b/docs/en/Features.md @@ -4,7 +4,7 @@ ABP Feature system is used to **enable**, **disable** or **change the behavior** The runtime value for a feature is generally a `boolean` value, like `true` (enabled) or `false` (disabled). However, you can get/set **any kind** of value for feature. -Feature system was originally designed to control the tenant features in a **multi-tenant** application. However, it is **extensible** and capable of determining the features by any condition. +Feature system was originally designed to control the tenant features in a **[multi-tenant](Multi-Tenancy.md)** application. However, it is **extensible** and capable of determining the features by any condition. > The feature system is implemented with the [Volo.Abp.Features](https://www.nuget.org/packages/Volo.Abp.Features) NuGet package. Most of the times you don't need to manually [install it](https://abp.io/package-detail/Volo.Abp.Features) since it comes pre-installed with the [application startup template](Startup-Templates/Application.md). @@ -142,6 +142,129 @@ There are some useful extension methods for the `IFeatureChecker` interface; ## Defining the Features +A feature should be defined to be able to check it. + +### FeatureDefinitionProvider + +Create a class inheriting the `FeatureDefinitionProvider` to define permissions. + +**Example: Defining permissions** + +```csharp +using Volo.Abp.Features; + +namespace FeaturesDemo +{ + public class MyFeatureDefinitionProvider : FeatureDefinitionProvider + { + public override void Define(IFeatureDefinitionContext context) + { + var myGroup = context.AddGroup("MyApp"); + + myGroup.AddFeature("MyApp.PdfReporting", defaultValue: "false"); + myGroup.AddFeature("MyApp.MaxProductCount", defaultValue: "10"); + } + } +} +``` + +> ABP automatically discovers this class and registers the features. No additional configuration required. + +* 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. + +### Other Feature Properties + +While these minimal definitions are enough to make the feature system working, you can specify the **optional properties** for the features; + +* `DisplayName`: A localizable string that will be used to show the feature name on the user interface. +* `Description`: A longer localizable text to describe the feature. +* `ValueType`: Type of the feature value. Can be a class implementing the `IStringValueType`. Built-in types: + * `ToggleStringValueType`: Used to define `true`/`false`, `on`/`off`, `enabled`/`disabled` style features. A checkbox is shown on the UI. + * `FreeTextStringValueType`: Used to define free text values. A textbox is shown on the UI. + * `SelectionStringValueType`: Used to force the value to be selected from a list. A dropdown list is shown on the UI. +* `IsVisibleToClients` (default: `true`): Set false to hide the value of this feature from clients (browsers). Sharing the value with the clients helps them to conditionally show/hide/change the UI parts based on the feature value. +* `Properties`: A dictionary to set/get arbitrary key-value pairs related to this feature. This can be a point for customization. + +So, based on these descriptions, it would be better to define these features as shown below: + +```csharp +using FeaturesDemo.Localization; +using Volo.Abp.Features; +using Volo.Abp.Localization; +using Volo.Abp.Validation.StringValues; + +namespace FeaturesDemo +{ + public class MyFeatureDefinitionProvider : FeatureDefinitionProvider + { + public override void Define(IFeatureDefinitionContext context) + { + var myGroup = context.AddGroup("MyApp"); + + myGroup.AddFeature( + "MyApp.PdfReporting", + defaultValue: "false", + displayName: LocalizableString + .Create("PdfReporting"), + valueType: new ToggleStringValueType() + ); + + myGroup.AddFeature( + "MyApp.MaxProductCount", + defaultValue: "10", + displayName: LocalizableString + .Create("MaxProductCount"), + valueType: new FreeTextStringValueType( + new NumericValueValidator(0, 1000000)) + ); + } + } +} + +``` + +* `FeaturesDemoResource` is the project name in this example code. See the [localization document](Localization.md) for details about the localization system. +* First feature is set to `ToggleStringValueType`, while the second one is set to `FreeTextStringValueType` with a numeric validator that allows to the values from `0` to `1,000,000`. + +Remember to define the localization the keys in your localization file: + +````json +"PdfReporting": "PDF Reporting", +"MaxProductCount": "Maximum number of products" +```` + +See the [localization document](Localization.md) for details about the localization system. + +### Feature Management Modal + +The [application startup template](Startup-Templates/Application.md) comes with the [tenant management](Modules/Tenant-Management.md) and the [feature management](Modules/Feature-Management.md) modules pre-installed. + +Whenever you define a new feature, it will be available on the **feature management modal**. To open this modal, navigate to the **tenant management page** and select the `Features` action for a tenant (create a new tenant if there is no tenant yet): + +![features-action](images/features-action.png) + +This action opens a modal to manage the feature values for the selected tenant: + +![features-modal](images/features-modal.png) + +So, you can enable, disable and set values for a tenant. These values will be used whenever a user of this tenant uses the application. + +See the *Feature Management* section below to learn more about managing the features. + +### Child Features + +A feature may have child features. This is especially useful if you want to create a feature that is selectable only if another feature was enabled. + +TODO + +### Changing Features Definitions of a Depended Module + +TODO + +### Check a Permission in the Client Side + TODO ## Feature Management diff --git a/docs/en/images/features-action.png b/docs/en/images/features-action.png new file mode 100644 index 0000000000000000000000000000000000000000..8283a95be946232016b65a63f47c211424c6ba54 GIT binary patch literal 16742 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�UfiA=i< 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 literal 0 HcmV?d00001 diff --git a/docs/en/images/features-modal.png b/docs/en/images/features-modal.png new file mode 100644 index 0000000000000000000000000000000000000000..74ee8aabc33189a420f3205ddc0c90215e5767c9 GIT binary patch literal 35412 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 Q7XuJ@y85}Sb4q9e0P>=`F8}}l literal 0 HcmV?d00001 From 8e2cd0e5b9f9a98b9ec60f536e515a9c9378c841 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Mon, 7 Sep 2020 21:48:23 +0300 Subject: [PATCH 15/86] fix: avoid ng-packagr error by mocking Document type --- .../ngx-datatable-default.directive.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts b/npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts index 64a64af4eb..e8b9ffac85 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts @@ -21,7 +21,7 @@ export class NgxDatatableDefaultDirective implements AfterViewInit, OnDestroy { return `ngx-datatable ${this.class}`; } - constructor(private table: DatatableComponent, @Inject(DOCUMENT) private document: Document) { + constructor(private table: DatatableComponent, @Inject(DOCUMENT) private document: MockDocument) { this.table.columnMode = ColumnMode.force; this.table.footerHeight = 50; this.table.headerHeight = 50; @@ -67,3 +67,18 @@ export class NgxDatatableDefaultDirective implements AfterViewInit, OnDestroy { this.subscription.unsubscribe(); } } + +// fix: https://github.com/angular/angular/issues/20351 +interface MockDocument { + body: MockBody; + documentElement: MockDocumentElement; +} + +interface MockBody { + offsetWidth: number; +} + +interface MockDocumentElement { + clientHeight: number; + scrollHeight: number; +} From 502ac9eea5d1dc769826a828fba72fd8e2ca6070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Mon, 7 Sep 2020 21:49:04 +0300 Subject: [PATCH 16/86] added example: Defining child features --- docs/en/Features.md | 47 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/docs/en/Features.md b/docs/en/Features.md index e125f16f08..963f15a9b7 100644 --- a/docs/en/Features.md +++ b/docs/en/Features.md @@ -222,7 +222,6 @@ namespace FeaturesDemo } } } - ``` * `FeaturesDemoResource` is the project name in this example code. See the [localization document](Localization.md) for details about the localization system. @@ -257,7 +256,51 @@ See the *Feature Management* section below to learn more about managing the feat A feature may have child features. This is especially useful if you want to create a feature that is selectable only if another feature was enabled. -TODO +**Example: Defining child features** + +```csharp +using FeaturesDemo.Localization; +using Volo.Abp.Features; +using Volo.Abp.Localization; +using Volo.Abp.Validation.StringValues; + +namespace FeaturesDemo +{ + public class MyFeatureDefinitionProvider : FeatureDefinitionProvider + { + public override void Define(IFeatureDefinitionContext context) + { + var myGroup = context.AddGroup("MyApp"); + + var reportingFeature = myGroup.AddFeature( + "MyApp.Reporting", + defaultValue: "false", + displayName: LocalizableString + .Create("Reporting"), + valueType: new ToggleStringValueType() + ); + + reportingFeature.CreateChild( + "MyApp.PdfReporting", + defaultValue: "false", + displayName: LocalizableString + .Create("PdfReporting"), + valueType: new ToggleStringValueType() + ); + + reportingFeature.CreateChild( + "MyApp.ExcelReporting", + defaultValue: "false", + displayName: LocalizableString + .Create("ExcelReporting"), + valueType: new ToggleStringValueType() + ); + } + } +} +``` + +The example above defines a *Reporting* feature with two children: *PDF Reporting* and *Excel Reporting*. ### Changing Features Definitions of a Depended Module From 36f8bb65a98aa26cfe48b4735534b9629da1ea83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Mon, 7 Sep 2020 21:58:30 +0300 Subject: [PATCH 17/86] Add link to the angular UI doc for the permissions and revise some sections. --- docs/en/Authorization.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/en/Authorization.md b/docs/en/Authorization.md index 4b7502f437..eef4237039 100644 --- a/docs/en/Authorization.md +++ b/docs/en/Authorization.md @@ -80,6 +80,8 @@ namespace Acme.BookStore.Permissions > ABP automatically discovers this class. No additional configuration required! +> You typically define this class inside the `Application.Contracts` project of your [application](Startup-Templates/Application.md). The startup template already comes with an empty class named *YourProjectNamePermissionDefinitionProvider* that you can start with. + In the `Define` method, you first need to add a **permission group** or get an existing group then add **permissions** to this group. When you define a permission, it becomes usable in the ASP.NET Core authorization system as a **policy** name. It also becomes visible in the UI. See permissions dialog for a role: @@ -276,14 +278,24 @@ public async Task CreateAsync(CreateAuthorDto input) > Tip: Prefer to use the `Authorize` attribute wherever possible, since it is declarative & simple. Use `IAuthorizationService` if you need to conditionally check a permission and run a business code based on the permission check. -### Check a Permission in JavaScript +## Check a Permission in JavaScript + +You may need to check a policy/permission on the client side. + +### MVC UI -You may need to check a policy/permission on the client side. For ASP.NET Core MVC / Razor Pages applications, you can use the `abp.auth` API. Example: +For ASP.NET Core MVC / Razor Pages applications, you can use the `abp.auth` API. + +**Example: Check if a given permission has been granted for the current user** ```js abp.auth.isGranted('MyPermissionName'); ``` +### Angular UI + +See the [permission management document](UI/Angular/Permission-Management.md) for the Angular UI. + ## Permission Management Permission management is normally done by an admin user using the permission management modal: @@ -374,7 +386,7 @@ Configure(options => ### Permission Store -`IPermissionStore` is the only interface that needs to be implemented to read the value of permissions from a persistence source, generally a database system. Permission management module implements it. See the [permission management module documentation](Modules/Permission-Management.md) for more information +`IPermissionStore` is the only interface that needs to be implemented to read the value of permissions from a persistence source, generally a database system. The Permission Management module implements it and pre-installed in the application startup template. See the [permission management module documentation](Modules/Permission-Management.md) for more information ### AlwaysAllowAuthorizationService From 8e45c9a240b9930eede375fc0470de51ee5b4673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Mon, 7 Sep 2020 21:58:42 +0300 Subject: [PATCH 18/86] Add Feature Store section. --- docs/en/Features.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/Features.md b/docs/en/Features.md index 963f15a9b7..35ba5a0de0 100644 --- a/docs/en/Features.md +++ b/docs/en/Features.md @@ -306,7 +306,7 @@ The example above defines a *Reporting* feature with two children: *PDF Reportin TODO -### Check a Permission in the Client Side +## Check a Permission in the Client Side TODO @@ -324,4 +324,4 @@ TODO ### Feature Store -TODO \ No newline at end of file +`IFeatureStore` is the only interface that needs to be implemented to read the value of features from a persistence source, generally a database system. The Feature Management module implements it and pre-installed in the application startup template. See the [feature management module documentation](https://docs.abp.io/en/abp/latest/Modules/Feature-Management) for more information \ No newline at end of file From 40978470c6b5bc8f032d320aa59535d237c646b8 Mon Sep 17 00:00:00 2001 From: maliming <6908465+maliming@users.noreply.github.com> Date: Tue, 8 Sep 2020 11:17:34 +0800 Subject: [PATCH 19/86] Refactor --- .../FeatureManagement/IFeatureAppService.cs | 8 +-- .../FeatureManagement/FeatureAppService.cs | 52 ++++--------------- .../AbpFeatureManagementDomainModule.cs | 1 + .../FeatureManagement/FeaturesController.cs | 14 ----- .../FeatureManagementModal.cshtml.cs | 21 +------- .../Pages/TenantManagement/Tenants/Index.js | 3 +- 6 files changed, 15 insertions(+), 84 deletions(-) diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/IFeatureAppService.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/IFeatureAppService.cs index a2a87b195b..ce9609976e 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/IFeatureAppService.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/IFeatureAppService.cs @@ -6,12 +6,8 @@ namespace Volo.Abp.FeatureManagement { public interface IFeatureAppService : IApplicationService { - Task GetAsync([NotNull] string providerName, [NotNull] string providerKey); + Task GetAsync([NotNull] string providerName, string providerKey); - Task UpdateAsync([NotNull] string providerName, [NotNull] string providerKey, UpdateFeaturesDto input); - - Task GetHostAsync(); - - Task UpdateHostAsync(UpdateFeaturesDto input); + Task UpdateAsync([NotNull] string providerName, string providerKey, UpdateFeaturesDto input); } } diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs index 6969739baa..7bb305054e 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs @@ -25,13 +25,18 @@ namespace Volo.Abp.FeatureManagement Options = options.Value; } - public virtual async Task GetAsync([NotNull] string providerName, [NotNull] string providerKey) + public virtual async Task GetAsync([NotNull] string providerName, string providerKey) { await CheckProviderPolicy(providerName); var featureDefinitions = FeatureDefinitionManager.GetAll(); var features = new List(); + if (providerName == HostFeatureValueProvider.ProviderName) + { + featureDefinitions = featureDefinitions.Where(x => x.IsAvailableToHost).ToList(); + } + foreach (var featureDefinition in featureDefinitions) { var feature = await FeatureManager.GetOrNullWithProviderAsync(featureDefinition.Name, providerName, providerKey); @@ -56,58 +61,19 @@ namespace Volo.Abp.FeatureManagement return new FeatureListDto { Features = features }; } - public virtual async Task UpdateAsync([NotNull] string providerName, [NotNull] string providerKey, UpdateFeaturesDto input) + public virtual async Task UpdateAsync([NotNull] string providerName, string providerKey, UpdateFeaturesDto input) { await CheckProviderPolicy(providerName); - foreach (var feature in input.Features) - { - await FeatureManager.SetAsync(feature.Name, feature.Value, providerName, providerKey); - } - } - - [Authorize(FeatureManagementPermissions.ManageHostFeatures)] - public async Task GetHostAsync() - { - var featureDefinitions = FeatureDefinitionManager.GetAll().Where(x => x.IsAvailableToHost); - var features = new List(); - - foreach (var featureDefinition in featureDefinitions) - { - var feature = await FeatureManager.GetOrNullWithProviderAsync(featureDefinition.Name, HostFeatureValueProvider.ProviderName, null); - features.Add(new FeatureDto - { - 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(features, HostFeatureValueProvider.ProviderName, null); - - return new FeatureListDto { Features = features }; - } - - [Authorize(FeatureManagementPermissions.ManageHostFeatures)] - public async Task UpdateHostAsync(UpdateFeaturesDto input) - { foreach (var feature in input.Features) { var featureDefinition = FeatureDefinitionManager.GetOrNull(feature.Name); - if (featureDefinition == null || !featureDefinition.IsAvailableToHost) + if (featureDefinition == null || (providerName == HostFeatureValueProvider.ProviderName && !featureDefinition.IsAvailableToHost)) { throw new UserFriendlyException(L["FeatureNotAvailable"]); } - await FeatureManager.SetAsync(feature.Name, feature.Value, HostFeatureValueProvider.ProviderName, null); + await FeatureManager.SetAsync(feature.Name, feature.Value, providerName, providerKey); } } diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs index 17e0b9fc04..b9992309dd 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs @@ -19,6 +19,7 @@ namespace Volo.Abp.FeatureManagement { options.Providers.Add(); options.Providers.Add(); + options.ProviderPolicies[HostFeatureValueProvider.ProviderName] = "FeatureManagement.ManageHostFeatures"; options.Providers.Add(); //TODO: Should be moved to the Tenant Management module diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/FeaturesController.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/FeaturesController.cs index a5c9c1c819..1c956e1fcf 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/FeaturesController.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/FeaturesController.cs @@ -27,19 +27,5 @@ namespace Volo.Abp.FeatureManagement { return FeatureAppService.UpdateAsync(providerName, providerKey, input); } - - [HttpGet] - [Route("host")] - public virtual Task GetHostAsync() - { - return FeatureAppService.GetHostAsync(); - } - - [HttpPut] - [Route("host")] - public virtual Task UpdateHostAsync(UpdateFeaturesDto input) - { - return FeatureAppService.UpdateHostAsync(input); - } } } diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml.cs index 0fd5d756f7..ecc59d11f9 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml.cs @@ -16,7 +16,6 @@ namespace Volo.Abp.FeatureManagement.Web.Pages.FeatureManagement [BindProperty(SupportsGet = true)] public string ProviderName { get; set; } - [Required] [HiddenInput] [BindProperty(SupportsGet = true)] public string ProviderKey { get; set; } @@ -37,14 +36,7 @@ namespace Volo.Abp.FeatureManagement.Web.Pages.FeatureManagement public virtual async Task OnGetAsync() { - if (ProviderName == HostFeatureValueProvider.ProviderName) - { - FeatureListDto = await FeatureAppService.GetHostAsync(); - } - else - { - FeatureListDto = await FeatureAppService.GetAsync(ProviderName, ProviderKey); - } + FeatureListDto = await FeatureAppService.GetAsync(ProviderName, ProviderKey); } public virtual async Task OnPostAsync() @@ -58,14 +50,7 @@ namespace Volo.Abp.FeatureManagement.Web.Pages.FeatureManagement }).ToList() }; - if (ProviderName == HostFeatureValueProvider.ProviderName) - { - await FeatureAppService.UpdateHostAsync(features); - } - else - { - await FeatureAppService.UpdateAsync(ProviderName, ProviderKey, features); - } + await FeatureAppService.UpdateAsync(ProviderName, ProviderKey, features); return NoContent(); } @@ -88,8 +73,6 @@ namespace Volo.Abp.FeatureManagement.Web.Pages.FeatureManagement public string Value { get; set; } - public string ProviderName { get; set; } - public bool BoolValue { get; set; } public string Type { get; set; } diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.js b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.js index 828f163908..ecb2309f36 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.js +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.js @@ -132,8 +132,7 @@ _$wrapper.find('button[name=ManageHostFeatures]').click(function (e) { e.preventDefault(); _featuresModal.open({ - providerName: 'H', - providerKey: 'H' + providerName: 'H' }); }); }); From 0bae2564c127e359247e5ec87ba510eaf6f311c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 8 Sep 2020 10:27:01 +0300 Subject: [PATCH 20/86] Create Angular UI features page --- docs/en/UI/Angular/Features.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docs/en/UI/Angular/Features.md diff --git a/docs/en/UI/Angular/Features.md b/docs/en/UI/Angular/Features.md new file mode 100644 index 0000000000..f749030a26 --- /dev/null +++ b/docs/en/UI/Angular/Features.md @@ -0,0 +1,3 @@ +# Angular UI: Features + +> This document explains how to get feature values in an Angular application. See the [Features document](../../Features.md) to learn the feature system. \ No newline at end of file From 00c5114b1e3ec8d52b4bdc0243bfdf34c6f6f14e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 8 Sep 2020 10:29:04 +0300 Subject: [PATCH 21/86] Added sections to the features document --- docs/en/Features.md | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/docs/en/Features.md b/docs/en/Features.md index 35ba5a0de0..1ab26a113a 100644 --- a/docs/en/Features.md +++ b/docs/en/Features.md @@ -304,11 +304,38 @@ The example above defines a *Reporting* feature with two children: *PDF Reportin ### Changing Features Definitions of a Depended Module -TODO +A class deriving from the `FeatureDefinitionProvider` (just like the example above) can also get the existing permission definitions (defined by the depended [modules](Module-Development-Basics.md)) and change their definitions. -## Check a Permission in the Client Side +**Example: Manipulate an existing feature definition** -TODO +```csharp +var someGroup = context.GetGroupOrNull("SomeModule"); +var feature = someGroup.Features.FirstOrDefault(f => f.Name == "SomeFeature"); +if (feature != null) +{ + feature.Description = ... + feature.CreateChild(...); +} +``` + +## Check a Feature in the Client Side + +A feature value is available at the client side too, unless you set `IsVisibleToClients` to `false` on the feature definition. The feature values are exposed from the [Application Configuration API](API/Application-Configuration.md) and usable via some services on the UI. + +### ASP.NET Core MVC / Razor Pages UI + +Use `abp.features` API to get the feature values. + +**Example: Get feature values in the JavaScript code** + +````js +var isEnabled = abp.features.values["MyApp.ExcelReporting"] === "true"; +var count = abp.features.values["MyApp.MaxProductCount"]; +```` + +### Angular UI + +See the [features](Features.md) document for the Angular UI. ## Feature Management From fcdcc3e6708feaaf8532a655106f65fe4940fafd Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 8 Sep 2020 10:39:55 +0300 Subject: [PATCH 22/86] feat: create replaceable-components.service --- .../packages/core/src/lib/services/index.ts | 1 + .../replaceable-components.service.ts | 65 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/services/replaceable-components.service.ts diff --git a/npm/ng-packs/packages/core/src/lib/services/index.ts b/npm/ng-packs/packages/core/src/lib/services/index.ts index c084fd4808..d3d13de20b 100644 --- a/npm/ng-packs/packages/core/src/lib/services/index.ts +++ b/npm/ng-packs/packages/core/src/lib/services/index.ts @@ -9,6 +9,7 @@ export * from './localization.service'; export * from './multi-tenancy.service'; export * from './profile-state.service'; export * from './profile.service'; +export * from './replaceable-components.service'; export * from './rest.service'; export * from './routes.service'; export * from './session-state.service'; diff --git a/npm/ng-packs/packages/core/src/lib/services/replaceable-components.service.ts b/npm/ng-packs/packages/core/src/lib/services/replaceable-components.service.ts new file mode 100644 index 0000000000..418e608c49 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/services/replaceable-components.service.ts @@ -0,0 +1,65 @@ +import { Injectable, NgZone } from '@angular/core'; +import { Router } from '@angular/router'; +import { ReplaceableComponents } from '../models/replaceable-components'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { noop } from '../utils/common-utils'; +import { map, filter } from 'rxjs/operators'; + +@Injectable({ providedIn: 'root' }) +export class ReplaceableComponentsService { + private components$ = new BehaviorSubject([]); + + get replaceableComponents$(): Observable { + return this.components$.asObservable(); + } + + get replaceableComponents(): ReplaceableComponents.ReplaceableComponent[] { + return this.components$.value; + } + + constructor(private ngZone: NgZone, private router: Router) {} + + // TODO: Create a shared service for route reload and more + private reloadRoute() { + const { shouldReuseRoute } = this.router.routeReuseStrategy; + const setRouteReuse = (reuse: typeof shouldReuseRoute) => { + this.router.routeReuseStrategy.shouldReuseRoute = reuse; + }; + + setRouteReuse(() => false); + this.router.navigated = false; + + this.ngZone.run(async () => { + await this.router.navigateByUrl(this.router.url).catch(noop); + setRouteReuse(shouldReuseRoute); + }); + } + + add(replaceableComponent: ReplaceableComponents.ReplaceableComponent, reload?: boolean): void { + let replaceableComponents = this.components$.value; + + const index = replaceableComponents.findIndex( + component => component.key === replaceableComponent.key, + ); + + if (index > -1) { + replaceableComponents[index] = replaceableComponent; + } else { + replaceableComponents = [...replaceableComponents, replaceableComponent]; + } + + this.components$.next(replaceableComponents); + + if (reload) this.reloadRoute(); + } + + get(replaceableComponentKey: string): ReplaceableComponents.ReplaceableComponent { + return this.replaceableComponents.find(component => component.key === replaceableComponentKey); + } + + get$(replaceableComponentKey: string): Observable { + return this.replaceableComponents$.pipe( + map(components => components.find(component => component.key === replaceableComponentKey)), + ); + } +} From a2f6235225001958db5151dbf4f2217c2c0d8d18 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 8 Sep 2020 10:40:27 +0300 Subject: [PATCH 23/86] chore: deprecate replaceable-components.state --- .../actions/replaceable-components.actions.ts | 3 +- .../states/replaceable-components.state.ts | 30 ++++++------------- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/actions/replaceable-components.actions.ts b/npm/ng-packs/packages/core/src/lib/actions/replaceable-components.actions.ts index 8df6a384ad..a800ab858a 100644 --- a/npm/ng-packs/packages/core/src/lib/actions/replaceable-components.actions.ts +++ b/npm/ng-packs/packages/core/src/lib/actions/replaceable-components.actions.ts @@ -1,7 +1,8 @@ import { ReplaceableComponents } from '../models/replaceable-components'; +// tslint:disable: max-line-length /** - * @see usage: https://github.com/abpframework/abp/pull/2522#issue-358333183 + * @deprecated To be deleted in v4.0. Use ReplaceableComponentsService instead. See the doc (https://docs.abp.io/en/abp/latest/UI/Angular/Component-Replacement) */ export class AddReplaceableComponent { static readonly type = '[ReplaceableComponents] Add'; diff --git a/npm/ng-packs/packages/core/src/lib/states/replaceable-components.state.ts b/npm/ng-packs/packages/core/src/lib/states/replaceable-components.state.ts index 82096c5472..e5caa9f95b 100644 --- a/npm/ng-packs/packages/core/src/lib/states/replaceable-components.state.ts +++ b/npm/ng-packs/packages/core/src/lib/states/replaceable-components.state.ts @@ -1,11 +1,14 @@ -import { Injectable, NgZone } from '@angular/core'; -import { Router } from '@angular/router'; +import { Injectable } from '@angular/core'; import { Action, createSelector, Selector, State, StateContext } from '@ngxs/store'; import snq from 'snq'; import { AddReplaceableComponent } from '../actions/replaceable-components.actions'; import { ReplaceableComponents } from '../models/replaceable-components'; -import { noop } from '../utils/common-utils'; +import { ReplaceableComponentsService } from '../services/replaceable-components.service'; +// tslint:disable: max-line-length +/** + * @deprecated To be deleted in v4.0. Use ReplaceableComponentsService instead. See the doc (https://docs.abp.io/en/abp/latest/UI/Angular/Component-Replacement) + */ @State({ name: 'ReplaceableComponentsState', defaults: { replaceableComponents: [] } as ReplaceableComponents.State, @@ -30,23 +33,7 @@ export class ReplaceableComponentsState { return selector; } - constructor(private ngZone: NgZone, private router: Router) {} - - // TODO: Create a shared service for route reload and more - private reloadRoute() { - const { shouldReuseRoute } = this.router.routeReuseStrategy; - const setRouteReuse = (reuse: typeof shouldReuseRoute) => { - this.router.routeReuseStrategy.shouldReuseRoute = reuse; - }; - - setRouteReuse(() => false); - this.router.navigated = false; - - this.ngZone.run(async () => { - await this.router.navigateByUrl(this.router.url).catch(noop); - setRouteReuse(shouldReuseRoute); - }); - } + constructor(private service: ReplaceableComponentsService) {} @Action(AddReplaceableComponent) replaceableComponentsAction( @@ -69,6 +56,7 @@ export class ReplaceableComponentsState { replaceableComponents, }); - if (reload) this.reloadRoute(); + console.log(this.service); + this.service.add(payload, reload); } } From 7e7d86a745343c2e9c377a1c1e3752a45776c6ed Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 8 Sep 2020 10:42:07 +0300 Subject: [PATCH 24/86] feat: implement the new service to project --- .../components/dynamic-layout.component.ts | 9 ++- .../replaceable-route-container.component.ts | 9 ++- .../replaceable-template.directive.ts | 9 ++- .../tests/dynamic-layout.component.spec.ts | 63 +++++++++---------- .../src/lib/providers/styles.provider.ts | 39 ++++++------ 5 files changed, 61 insertions(+), 68 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts b/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts index c47b3c02fd..3d6870f577 100644 --- a/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts +++ b/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts @@ -1,13 +1,12 @@ import { Component, Injector, Optional, SkipSelf, Type } from '@angular/core'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; -import { Store } from '@ngxs/store'; import { eLayoutType } from '../enums/common'; import { ABP } from '../models'; import { ReplaceableComponents } from '../models/replaceable-components'; import { LocalizationService } from '../services/localization.service'; +import { ReplaceableComponentsService } from '../services/replaceable-components.service'; import { RoutesService } from '../services/routes.service'; import { SubscriptionService } from '../services/subscription.service'; -import { ReplaceableComponentsState } from '../states/replaceable-components.state'; import { findRoute, getRoutePath } from '../utils/route-utils'; import { TreeNode } from '../utils/tree-utils'; @@ -37,7 +36,7 @@ export class DynamicLayoutComponent { constructor( injector: Injector, private localizationService: LocalizationService, - private store: Store, + private replaceableComponents: ReplaceableComponentsService, private subscription: SubscriptionService, @Optional() @SkipSelf() dynamicLayoutComponent: DynamicLayoutComponent, ) { @@ -67,7 +66,7 @@ export class DynamicLayoutComponent { if (!expectedLayout) expectedLayout = eLayoutType.empty; const key = this.layouts.get(expectedLayout); - this.layout = this.getComponent(key).component; + this.layout = this.getComponent(key)?.component; } }); @@ -82,6 +81,6 @@ export class DynamicLayoutComponent { } private getComponent(key: string): ReplaceableComponents.ReplaceableComponent { - return this.store.selectSnapshot(ReplaceableComponentsState.getComponent(key)); + return this.replaceableComponents.get(key); } } diff --git a/npm/ng-packs/packages/core/src/lib/components/replaceable-route-container.component.ts b/npm/ng-packs/packages/core/src/lib/components/replaceable-route-container.component.ts index 04c596d398..6c0dabd511 100644 --- a/npm/ng-packs/packages/core/src/lib/components/replaceable-route-container.component.ts +++ b/npm/ng-packs/packages/core/src/lib/components/replaceable-route-container.component.ts @@ -1,10 +1,9 @@ import { Component, OnInit, Type } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { Store } from '@ngxs/store'; import { distinctUntilChanged } from 'rxjs/operators'; import { ReplaceableComponents } from '../models/replaceable-components'; +import { ReplaceableComponentsService } from '../services/replaceable-components.service'; import { SubscriptionService } from '../services/subscription.service'; -import { ReplaceableComponentsState } from '../states/replaceable-components.state'; @Component({ selector: 'abp-replaceable-route-container', @@ -22,7 +21,7 @@ export class ReplaceableRouteContainerComponent implements OnInit { constructor( private route: ActivatedRoute, - private store: Store, + private replaceableComponents: ReplaceableComponentsService, private subscription: SubscriptionService, ) {} @@ -31,8 +30,8 @@ export class ReplaceableRouteContainerComponent implements OnInit { this.componentKey = (this.route.snapshot.data .replaceableComponent as ReplaceableComponents.RouteData).key; - const component$ = this.store - .select(ReplaceableComponentsState.getComponent(this.componentKey)) + const component$ = this.replaceableComponents + .get$(this.componentKey) .pipe(distinctUntilChanged()); this.subscription.addOne( diff --git a/npm/ng-packs/packages/core/src/lib/directives/replaceable-template.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/replaceable-template.directive.ts index fd9051c64e..489d1afabc 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/replaceable-template.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/replaceable-template.directive.ts @@ -10,15 +10,14 @@ import { Type, ViewContainerRef, } from '@angular/core'; -import { Store } from '@ngxs/store'; import compare from 'just-compare'; import { Subscription } from 'rxjs'; import { filter } from 'rxjs/operators'; import snq from 'snq'; import { ABP } from '../models/common'; import { ReplaceableComponents } from '../models/replaceable-components'; +import { ReplaceableComponentsService } from '../services/replaceable-components.service'; import { SubscriptionService } from '../services/subscription.service'; -import { ReplaceableComponentsState } from '../states/replaceable-components.state'; @Directive({ selector: '[abpReplaceableTemplate]', providers: [SubscriptionService] }) export class ReplaceableTemplateDirective implements OnInit, OnChanges { @@ -45,7 +44,7 @@ export class ReplaceableTemplateDirective implements OnInit, OnChanges { private templateRef: TemplateRef, private cfRes: ComponentFactoryResolver, private vcRef: ViewContainerRef, - private store: Store, + private replaceableComponents: ReplaceableComponentsService, private subscription: SubscriptionService, ) { this.context = { @@ -58,8 +57,8 @@ export class ReplaceableTemplateDirective implements OnInit, OnChanges { } ngOnInit() { - const component$ = this.store - .select(ReplaceableComponentsState.getComponent(this.data.componentKey)) + const component$ = this.replaceableComponents + .get$(this.data.componentKey) .pipe( filter( (res = {} as ReplaceableComponents.ReplaceableComponent) => diff --git a/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts index 4d5f53a9ba..43ad6f87b1 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts @@ -7,7 +7,11 @@ import { NEVER } from 'rxjs'; import { DynamicLayoutComponent, RouterOutletComponent } from '../components'; import { eLayoutType } from '../enums/common'; import { ABP } from '../models'; -import { ApplicationConfigurationService, RoutesService } from '../services'; +import { + ApplicationConfigurationService, + RoutesService, + ReplaceableComponentsService, +} from '../services'; import { ReplaceableComponentsState } from '../states'; @Component({ @@ -78,33 +82,7 @@ const routes: ABP.Route[] = [ }, ]; -const storeData = { - ReplaceableComponentsState: { - replaceableComponents: [ - { - key: 'Theme.ApplicationLayoutComponent', - component: DummyApplicationLayoutComponent, - }, - { - key: 'Theme.AccountLayoutComponent', - component: DummyAccountLayoutComponent, - }, - { - key: 'Theme.EmptyLayoutComponent', - component: DummyEmptyLayoutComponent, - }, - ], - }, -}; - describe('DynamicLayoutComponent', () => { - const mockActions: Actions = NEVER; - const mockStore = ({ - selectSnapshot() { - return true; - }, - } as unknown) as Store; - const createComponent = createRoutingFactory({ component: RouterOutletComponent, stubsEnabled: false, @@ -113,10 +91,16 @@ describe('DynamicLayoutComponent', () => { providers: [ { provide: RoutesService, - useFactory: () => new RoutesService(mockActions, mockStore), + useFactory: () => + new RoutesService(NEVER, ({ + selectSnapshot() { + return true; + }, + } as unknown) as Store), }, + ReplaceableComponentsService, ], - imports: [RouterModule, DummyLayoutModule, NgxsModule.forRoot([ReplaceableComponentsState])], + imports: [RouterModule, DummyLayoutModule, NgxsModule.forRoot()], routes: [ { path: '', component: RouterOutletComponent }, { @@ -163,15 +147,26 @@ describe('DynamicLayoutComponent', () => { }); let spectator: SpectatorRouting; - let store: Store; + let replaceableComponents: ReplaceableComponentsService; beforeEach(async () => { spectator = createComponent(); - store = spectator.inject(Store); + replaceableComponents = spectator.inject(ReplaceableComponentsService); const routesService = spectator.inject(RoutesService); routesService.add(routes); - store.reset(storeData); + replaceableComponents.add({ + key: 'Theme.ApplicationLayoutComponent', + component: DummyApplicationLayoutComponent, + }); + replaceableComponents.add({ + key: 'Theme.AccountLayoutComponent', + component: DummyAccountLayoutComponent, + }); + replaceableComponents.add({ + key: 'Theme.EmptyLayoutComponent', + component: DummyEmptyLayoutComponent, + }); }); it('should handle application layout from parent abp route and display it', async () => { @@ -204,8 +199,8 @@ describe('DynamicLayoutComponent', () => { }); it('should not display any layout when layouts are empty', async () => { - store.reset({ ...storeData, ReplaceableComponentsState: {} }); - + const spy = jest.spyOn(replaceableComponents, 'get'); + spy.mockReturnValue(null); spectator.detectChanges(); spectator.router.navigateByUrl('/withoutLayout'); diff --git a/npm/ng-packs/packages/theme-basic/src/lib/providers/styles.provider.ts b/npm/ng-packs/packages/theme-basic/src/lib/providers/styles.provider.ts index ab6ea9cd1e..b823503912 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/providers/styles.provider.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/providers/styles.provider.ts @@ -1,4 +1,4 @@ -import { AddReplaceableComponent, CONTENT_STRATEGY, DomInsertionService } from '@abp/ng.core'; +import { ReplaceableComponentsService, CONTENT_STRATEGY, DomInsertionService } from '@abp/ng.core'; import { APP_INITIALIZER } from '@angular/core'; import { Store } from '@ngxs/store'; import { AccountLayoutComponent } from '../components/account-layout/account-layout.component'; @@ -11,32 +11,33 @@ export const BASIC_THEME_STYLES_PROVIDERS = [ { provide: APP_INITIALIZER, useFactory: configureStyles, - deps: [DomInsertionService, Store], + deps: [DomInsertionService, ReplaceableComponentsService], multi: true, }, ]; -export function configureStyles(domInsertion: DomInsertionService, store: Store) { +export function configureStyles( + domInsertion: DomInsertionService, + replaceableComponents: ReplaceableComponentsService, +) { return () => { domInsertion.insertContent(CONTENT_STRATEGY.AppendStyleToHead(styles)); - initLayouts(store); + initLayouts(replaceableComponents); }; } -function initLayouts(store: Store) { - store.dispatch([ - new AddReplaceableComponent({ - key: eThemeBasicComponents.ApplicationLayout, - component: ApplicationLayoutComponent, - }), - new AddReplaceableComponent({ - key: eThemeBasicComponents.AccountLayout, - component: AccountLayoutComponent, - }), - new AddReplaceableComponent({ - key: eThemeBasicComponents.EmptyLayout, - component: EmptyLayoutComponent, - }), - ]); +function initLayouts(replaceableComponents: ReplaceableComponentsService) { + replaceableComponents.add({ + key: eThemeBasicComponents.ApplicationLayout, + component: ApplicationLayoutComponent, + }); + replaceableComponents.add({ + key: eThemeBasicComponents.AccountLayout, + component: AccountLayoutComponent, + }); + replaceableComponents.add({ + key: eThemeBasicComponents.EmptyLayout, + component: EmptyLayoutComponent, + }); } From 731539ef84a655c47e92891417aa1a72dafc236a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 8 Sep 2020 10:43:42 +0300 Subject: [PATCH 25/86] Added Feature Management section. --- docs/en/Features.md | 35 +++++++++++++++++++++++- docs/en/Modules/Feature-Management.md | 2 ++ docs/en/Modules/Permission-Management.md | 2 ++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/docs/en/Features.md b/docs/en/Features.md index 1ab26a113a..05712eb803 100644 --- a/docs/en/Features.md +++ b/docs/en/Features.md @@ -170,6 +170,8 @@ 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. @@ -339,7 +341,38 @@ 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: + +![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; + } + + 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 diff --git a/docs/en/Modules/Feature-Management.md b/docs/en/Modules/Feature-Management.md index 5c41277dd8..968b742092 100644 --- a/docs/en/Modules/Feature-Management.md +++ b/docs/en/Modules/Feature-Management.md @@ -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 \ No newline at end of file diff --git a/docs/en/Modules/Permission-Management.md b/docs/en/Modules/Permission-Management.md index faa03eecc9..ad48b60171 100644 --- a/docs/en/Modules/Permission-Management.md +++ b/docs/en/Modules/Permission-Management.md @@ -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 \ No newline at end of file From 5c173a692e6d927d82688cca0c446f054d0fcf63 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Tue, 8 Sep 2020 15:54:12 +0800 Subject: [PATCH 26/86] Group features on the feature management modal --- .../Abp/Features/FeatureDefinitionManager.cs | 9 +- .../Abp/Features/IFeatureDefinitionManager.cs | 4 +- .../Abp/FeatureManagement/FeatureGroupDto.cs | 18 ++++ .../Abp/FeatureManagement/FeatureListDto.cs | 9 -- .../GetFeatureListResultDto.cs | 9 ++ .../FeatureManagement/IFeatureAppService.cs | 2 +- .../FeatureManagement/FeatureAppService.cs | 55 +++++++---- .../FeatureManagement/FeaturesController.cs | 2 +- .../FeatureManagementModal.cshtml | 98 +++++++++++-------- .../FeatureManagementModal.cshtml.cs | 19 +++- .../FeatureAppService_Tests.cs | 4 +- .../StringValueJsonConverter_Tests.cs | 82 +++++++++------- 12 files changed, 190 insertions(+), 121 deletions(-) create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeatureGroupDto.cs delete mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeatureListDto.cs create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/GetFeatureListResultDto.cs diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionManager.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionManager.cs index f8e8d9e9fe..31498e0756 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionManager.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionManager.cs @@ -62,6 +62,11 @@ namespace Volo.Abp.Features return FeatureDefinitions.GetOrDefault(name); } + public IReadOnlyList GetGroups() + { + return FeatureGroupDefinitions.Values.ToImmutableList(); + } + protected virtual Dictionary CreateFeatureDefinitions() { var features = new Dictionary(); @@ -78,7 +83,7 @@ namespace Volo.Abp.Features } protected virtual void AddFeatureToDictionaryRecursively( - Dictionary features, + Dictionary features, FeatureDefinition feature) { if (features.ContainsKey(feature.Name)) @@ -114,4 +119,4 @@ namespace Volo.Abp.Features return context.Groups; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionManager.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionManager.cs index c0172d7bc3..ca0a8cbcc6 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionManager.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionManager.cs @@ -11,5 +11,7 @@ namespace Volo.Abp.Features IReadOnlyList GetAll(); FeatureDefinition GetOrNull(string name); + + IReadOnlyList GetGroups(); } -} \ No newline at end of file +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeatureGroupDto.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeatureGroupDto.cs new file mode 100644 index 0000000000..dea10fd298 --- /dev/null +++ b/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 Features { get; set; } + + public string GetNormalizedGroupName() + { + return Name.Replace(".", "_"); + } + } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeatureListDto.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeatureListDto.cs deleted file mode 100644 index ee7bad8d5c..0000000000 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeatureListDto.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace Volo.Abp.FeatureManagement -{ - public class FeatureListDto - { - public List Features { get; set; } - } -} \ No newline at end of file diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/GetFeatureListResultDto.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/GetFeatureListResultDto.cs new file mode 100644 index 0000000000..86329598cc --- /dev/null +++ b/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 Groups { get; set; } + } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/IFeatureAppService.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/IFeatureAppService.cs index 2c337f1b2a..00c36b7f55 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/IFeatureAppService.cs +++ b/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 { - Task GetAsync([NotNull] string providerName, [NotNull] string providerKey); + Task GetAsync([NotNull] string providerName, [NotNull] string providerKey); Task UpdateAsync([NotNull] string providerName, [NotNull] string providerKey, UpdateFeaturesDto input); } diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs index f0c310650d..dbe649e51f 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs +++ b/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 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 GetAsync([NotNull] string providerName, [NotNull] string providerKey) + public virtual async Task GetAsync([NotNull] string providerName, [NotNull] string providerKey) { await CheckProviderPolicy(providerName); - var featureDefinitions = FeatureDefinitionManager.GetAll(); - var features = new List(); + var result = new GetFeatureListResultDto + { + Groups = new List() + }; - 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() + }; + + 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) diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/FeaturesController.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/FeaturesController.cs index 0ae78ed03f..cc00fb370b 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/FeaturesController.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/FeaturesController.cs @@ -17,7 +17,7 @@ namespace Volo.Abp.FeatureManagement } [HttpGet] - public virtual Task GetAsync(string providerName, string providerKey) + public virtual Task GetAsync(string providerName, string providerKey) { return FeatureAppService.GetAsync(providerName, providerKey); } diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml b/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml index f5adbf79d4..601926b193 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml @@ -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 L @{ @@ -13,53 +12,67 @@
- @if (Model.FeatureListDto?.Features != null && Model.FeatureListDto.Features.Any()) + @if (Model.FeatureListResultDto != null && Model.FeatureListResultDto.Groups.Any()) { - - - @for (var i = 0; i < Model.FeatureListDto.Features.Count; i++) - { - var feature = Model.FeatureListDto.Features[i]; - var disabled = Model.IsDisabled(feature.Provider.Name); -
+ + + + @for (var i = 0; i < Model.FeatureListResultDto.Groups.Count; i++) + { + +

@Model.FeatureListResultDto.Groups[i].DisplayName

+
+
+
+ @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); +
- @feature.DisplayName @(disabled ? $"({feature.Provider.Name})" : "") + @feature.DisplayName @(disabled ? $"({feature.Provider.Name})" : "") - - - @if (feature.ValueType is FreeTextStringValueType) - { - - - } - @if (feature.ValueType is SelectionStringValueType) - { - - + + @if (feature.ValueType is FreeTextStringValueType) + { + + + } + @if (feature.ValueType is SelectionStringValueType) + { + + + } + @if (feature.ValueType is ToggleStringValueType) + { + + + } +
} - } - - } - @if (feature.ValueType is ToggleStringValueType) - { - - - } -
- } +
+
+ + } + +
- + } else { @@ -69,4 +82,3 @@ }
- diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml.cs index e46d686e69..1464a1fc9b 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml.cs +++ b/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; } [BindProperty] - public List Features { get; set; } + public List 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 OnGetAsync() { - FeatureListDto = await FeatureAppService.GetAsync(ProviderName, ProviderKey); + ValidateModel(); + + FeatureListResultDto = await FeatureAppService.GetAsync(ProviderName, ProviderKey); + + return Page(); } public virtual async Task 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 Features { get; set; } + } + public class FeatureViewModel { public string Name { get; set; } diff --git a/modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/FeatureAppService_Tests.cs b/modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/FeatureAppService_Tests.cs index b257f873cd..2f904a46d2 100644 --- a/modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/FeatureAppService_Tests.cs +++ b/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()); 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(); diff --git a/modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/StringValueJsonConverter_Tests.cs b/modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/StringValueJsonConverter_Tests.cs index 528e0de5e8..1b7553b773 100644 --- a/modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/StringValueJsonConverter_Tests.cs +++ b/modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/StringValueJsonConverter_Tests.cs @@ -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 + Groups = new List { - new FeatureDto + new FeatureGroupDto { - ValueType = new FreeTextStringValueType + Name = "MyGroup", + DisplayName = "MyGroup", + Features = new List { - 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(serialized); + var featureListDto2 = _jsonSerializer.Deserialize(serialized); - featureListDto2.Features[0].ValueType.ShouldBeOfType(); - featureListDto2.Features[0].ValueType.Validator.ShouldBeOfType(); + featureListDto2.Groups[0].Features[0].ValueType.ShouldBeOfType(); + featureListDto2.Groups[0].Features[0].ValueType.Validator.ShouldBeOfType(); - featureListDto2.Features[1].ValueType.ShouldBeOfType(); - featureListDto2.Features[1].ValueType.Validator.ShouldBeOfType(); - featureListDto2.Features[1].ValueType.As().ItemSource.Items.ShouldBeOfType(); - featureListDto2.Features[1].ValueType.As().ItemSource.Items.ShouldContain(x => + featureListDto2.Groups[0].Features[1].ValueType.ShouldBeOfType(); + featureListDto2.Groups[0].Features[1].ValueType.Validator.ShouldBeOfType(); + featureListDto2.Groups[0].Features[1].ValueType.As().ItemSource.Items.ShouldBeOfType(); + featureListDto2.Groups[0].Features[1].ValueType.As().ItemSource.Items.ShouldContain(x => x.Value == "TestValue" && x.DisplayText.ResourceName == "TestResourceName" && x.DisplayText.Name == "TestName"); - featureListDto2.Features[2].ValueType.ShouldBeOfType(); - featureListDto2.Features[2].ValueType.Validator.ShouldBeOfType(); - featureListDto2.Features[2].ValueType.Validator.As().MaxValue.ShouldBe(1000); - featureListDto2.Features[2].ValueType.Validator.As().MinValue.ShouldBe(10); + featureListDto2.Groups[0].Features[2].ValueType.ShouldBeOfType(); + featureListDto2.Groups[0].Features[2].ValueType.Validator.ShouldBeOfType(); + featureListDto2.Groups[0].Features[2].ValueType.Validator.As().MaxValue.ShouldBe(1000); + featureListDto2.Groups[0].Features[2].ValueType.Validator.As().MinValue.ShouldBe(10); } } } From 2104c86eab18ce32a0f74ecd6d3356f4d21e9c3b Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 8 Sep 2020 11:07:37 +0300 Subject: [PATCH 27/86] chore: remove console.log --- .../packages/core/src/lib/states/replaceable-components.state.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/npm/ng-packs/packages/core/src/lib/states/replaceable-components.state.ts b/npm/ng-packs/packages/core/src/lib/states/replaceable-components.state.ts index e5caa9f95b..61a5fef5f6 100644 --- a/npm/ng-packs/packages/core/src/lib/states/replaceable-components.state.ts +++ b/npm/ng-packs/packages/core/src/lib/states/replaceable-components.state.ts @@ -56,7 +56,6 @@ export class ReplaceableComponentsState { replaceableComponents, }); - console.log(this.service); this.service.add(payload, reload); } } From 8fdbab9576574b37b3a112e965c91fe0fe64da09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 8 Sep 2020 11:09:28 +0300 Subject: [PATCH 28/86] Completed the Features document. --- docs/en/Features.md | 67 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/docs/en/Features.md b/docs/en/Features.md index 05712eb803..1ec35f730a 100644 --- a/docs/en/Features.md +++ b/docs/en/Features.md @@ -176,6 +176,8 @@ namespace FeaturesDemo * 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; @@ -376,11 +378,70 @@ public class MyService : ITransientDependency ## Advanced Topics -TODO - ### 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 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(options => +{ + options.ValueProviders.Add(); +}); +``` + +Use this code inside the `ConfigureServices` of your [module](Module-Development-Basics.md) class. ### Feature Store From 311287cce9b522f326e16e6138b378b020884251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 8 Sep 2020 11:14:48 +0300 Subject: [PATCH 29/86] Added Features to the nav menu --- docs/en/Multi-Tenancy.md | 3 +++ docs/en/docs-nav.json | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/docs/en/Multi-Tenancy.md b/docs/en/Multi-Tenancy.md index 901a5c5cff..5138c9de04 100644 --- a/docs/en/Multi-Tenancy.md +++ b/docs/en/Multi-Tenancy.md @@ -369,3 +369,6 @@ options.AddDomainTenantResolver("{0}.mydomain.com"); options.AddDomainTenantResolver("{0}.com"); ```` +## See Also + +* [Features](Features.md) \ No newline at end of file diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index dc872a2279..654bc720e2 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -170,6 +170,10 @@ "text": "Settings", "path": "Settings.md" }, + { + "text": "Features", + "path": "Features.md" + }, { "text": "Data Filtering", "path": "Data-Filtering.md" From 89df9fdde0a3f5a33ae6c4c01001ad4b4469c42f Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 8 Sep 2020 12:05:54 +0300 Subject: [PATCH 30/86] test: fix testing errors --- ...laceable-route-container.component.spec.ts | 18 +++++++-------- .../replaceable-template.directive.spec.ts | 22 +++++++++++-------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/tests/replaceable-route-container.component.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/replaceable-route-container.component.spec.ts index f5874bca46..16973c17c8 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/replaceable-route-container.component.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/replaceable-route-container.component.spec.ts @@ -5,6 +5,7 @@ import { Store } from '@ngxs/store'; import { of, Subject, BehaviorSubject } from 'rxjs'; import { ReplaceableRouteContainerComponent } from '../components/replaceable-route-container.component'; import { ReplaceableComponentsState } from '../states'; +import { ReplaceableComponentsService } from '../services/replaceable-components.service'; @Component({ selector: 'abp-external-component', @@ -30,16 +31,15 @@ const activatedRouteMock = { }; describe('ReplaceableRouteContainerComponent', () => { - const selectResponse = new BehaviorSubject(undefined); - const mockSelect = jest.fn(() => selectResponse); - let spectator: SpectatorHost; + const get$Res = new BehaviorSubject(undefined); + const replaceableComponents = spectator.inject(ReplaceableComponentsService); + const spy = jest.spyOn(replaceableComponents, 'get$'); + spy.mockReturnValue(get$Res as any); + const createHost = createHostFactory({ component: ReplaceableRouteContainerComponent, - providers: [ - { provide: ActivatedRoute, useValue: activatedRouteMock }, - { provide: Store, useValue: { select: mockSelect } }, - ], + providers: [{ provide: ActivatedRoute, useValue: activatedRouteMock }], declarations: [ExternalComponent, DefaultComponent], entryComponents: [DefaultComponent, ExternalComponent], }); @@ -55,11 +55,11 @@ describe('ReplaceableRouteContainerComponent', () => { }); it("should display the external component if it's available in store.", () => { - selectResponse.next({ component: ExternalComponent }); + get$Res.next({ component: ExternalComponent }); spectator.detectChanges(); expect(spectator.query('p')).toHaveText('external'); - selectResponse.next({ component: null }); + get$Res.next({ component: null }); spectator.detectChanges(); expect(spectator.query('p')).toHaveText('default'); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/replaceable-template.directive.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/replaceable-template.directive.spec.ts index 613508f56b..2923dd473b 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/replaceable-template.directive.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/replaceable-template.directive.spec.ts @@ -4,6 +4,8 @@ import { Store } from '@ngxs/store'; import { Subject } from 'rxjs'; import { ReplaceableTemplateDirective } from '../directives'; import { ReplaceableComponents } from '../models'; +import { Router } from '@angular/router'; +import { ReplaceableComponentsService } from '../services/replaceable-components.service'; @Component({ selector: 'abp-default-component', @@ -48,15 +50,17 @@ class ExternalComponent { } describe('ReplaceableTemplateDirective', () => { - const selectResponse = new Subject(); - const mockSelect = jest.fn(() => selectResponse); - let spectator: SpectatorDirective; + const get$Res = new Subject(); + + const replaceableComponents = spectator.inject(ReplaceableComponentsService); + const spy = jest.spyOn(replaceableComponents, 'get$'); + spy.mockReturnValue(get$Res as any); const createDirective = createDirectiveFactory({ directive: ReplaceableTemplateDirective, - providers: [{ provide: Store, useValue: { select: mockSelect } }], declarations: [DefaultComponent, ExternalComponent], entryComponents: [ExternalComponent], + mocks: [Router], }); describe('without external component', () => { @@ -72,7 +76,7 @@ describe('ReplaceableTemplateDirective', () => { `, { hostProps: { oneWay: { label: 'Test' }, twoWay: false, twoWayChange, someOutput } }, ); - selectResponse.next(undefined); + get$Res.next(undefined); const component = spectator.query(DefaultComponent); spectator.directive.context.initTemplate(component); spectator.detectChanges(); @@ -114,7 +118,7 @@ describe('ReplaceableTemplateDirective', () => { `, { hostProps: { oneWay: { label: 'Test' }, twoWay: false, twoWayChange, someOutput } }, ); - selectResponse.next({ component: ExternalComponent, key: 'TestModule.TestComponent' }); + get$Res.next({ component: ExternalComponent, key: 'TestModule.TestComponent' }); }); afterEach(() => twoWayChange.mockClear()); @@ -150,7 +154,7 @@ describe('ReplaceableTemplateDirective', () => { const externalComponent = spectator.query(ExternalComponent); spectator.setHostInput({ oneWay: 'test' }); externalComponent.data.inputs.twoWay = true; - selectResponse.next({ component: null, key: 'TestModule.TestComponent' }); + get$Res.next({ component: null, key: 'TestModule.TestComponent' }); spectator.detectChanges(); const component = spectator.query(DefaultComponent); spectator.directive.context.initTemplate(component); @@ -161,14 +165,14 @@ describe('ReplaceableTemplateDirective', () => { }); it('should reset default component subscriptions', () => { - selectResponse.next({ component: null, key: 'TestModule.TestComponent' }); + get$Res.next({ component: null, key: 'TestModule.TestComponent' }); const component = spectator.query(DefaultComponent); spectator.directive.context.initTemplate(component); spectator.detectChanges(); const unsubscribe = jest.fn(() => {}); spectator.directive.defaultComponentSubscriptions.twoWayChange.unsubscribe = unsubscribe; - selectResponse.next({ component: ExternalComponent, key: 'TestModule.TestComponent' }); + get$Res.next({ component: ExternalComponent, key: 'TestModule.TestComponent' }); expect(unsubscribe).toHaveBeenCalled(); }); }); From f5b6ef2388250eb4c742254ff9b0b0331299702d Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Tue, 8 Sep 2020 18:06:58 +0800 Subject: [PATCH 31/86] add paged --- .../Volo/Abp/Identity/IOrganizationUnitRepository.cs | 8 ++++++-- .../EfCoreOrganizationUnitRepository.cs | 10 ++++++++-- .../MongoDB/MongoOrganizationUnitRepository.cs | 10 ++++++++-- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs index 81474425db..4d5bf7d060 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs @@ -57,8 +57,10 @@ namespace Volo.Abp.Identity Task> GetUnaddedRolesAsync( OrganizationUnit organizationUnit, - string filter = null, string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + string filter = null, bool includeDetails = false, CancellationToken cancellationToken = default ); @@ -81,8 +83,10 @@ namespace Volo.Abp.Identity Task> GetUnaddedUsersAsync( OrganizationUnit organizationUnit, - string filter = null, string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + string filter = null, bool includeDetails = false, CancellationToken cancellationToken = default ); diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs index d50745ab1d..ebfa9b8ebb 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs @@ -114,8 +114,10 @@ namespace Volo.Abp.Identity.EntityFrameworkCore public virtual async Task> GetUnaddedRolesAsync( OrganizationUnit organizationUnit, - string filter = null, string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + string filter = null, bool includeDetails = false, CancellationToken cancellationToken = default) { @@ -126,6 +128,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore .IncludeDetails(includeDetails) .WhereIf(!filter.IsNullOrWhiteSpace(), r => r.Name.Contains(filter)) .OrderBy(sorting ?? nameof(IdentityRole.Name)) + .PageBy(skipCount, maxResultCount) .ToListAsync(cancellationToken); } @@ -157,8 +160,10 @@ namespace Volo.Abp.Identity.EntityFrameworkCore public virtual async Task> GetUnaddedUsersAsync( OrganizationUnit organizationUnit, - string filter = null, string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + string filter = null, bool includeDetails = false, CancellationToken cancellationToken = default) { @@ -181,6 +186,7 @@ namespace Volo.Abp.Identity.EntityFrameworkCore return await query .IncludeDetails(includeDetails) .OrderBy(sorting ?? nameof(IdentityUser.Name)) + .PageBy(skipCount, maxResultCount) .ToListAsync(cancellationToken); } diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs index 2823c99a6c..2b28a2e3a1 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs @@ -109,8 +109,10 @@ namespace Volo.Abp.Identity.MongoDB public async Task> GetUnaddedRolesAsync( OrganizationUnit organizationUnit, - string filter = null, string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + string filter = null, bool includeDetails = false, CancellationToken cancellationToken = default) { @@ -120,6 +122,7 @@ namespace Volo.Abp.Identity.MongoDB .WhereIf(!filter.IsNullOrWhiteSpace(), r => r.Name.Contains(filter)) .OrderBy(sorting ?? nameof(IdentityRole.Name)) .As>() + .PageBy>(skipCount, maxResultCount) .ToListAsync(cancellationToken); } @@ -153,8 +156,10 @@ namespace Volo.Abp.Identity.MongoDB public async Task> GetUnaddedUsersAsync( OrganizationUnit organizationUnit, - string filter = null, string sorting = null, + int maxResultCount = int.MaxValue, + int skipCount = 0, + string filter = null, bool includeDetails = false, CancellationToken cancellationToken = default) { @@ -169,6 +174,7 @@ namespace Volo.Abp.Identity.MongoDB ) .OrderBy(sorting ?? nameof(IdentityUser.UserName)) .As>() + .PageBy>(skipCount, maxResultCount) .ToListAsync(GetCancellationToken(cancellationToken)); } From 59a6c630be8f9de8cbadbe68f8c328fa55de37d8 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Tue, 8 Sep 2020 18:33:59 +0800 Subject: [PATCH 32/86] Add get count method --- .../Identity/IOrganizationUnitRepository.cs | 12 +++++++ .../EfCoreOrganizationUnitRepository.cs | 31 +++++++++++++++++++ .../MongoOrganizationUnitRepository.cs | 29 +++++++++++++++++ .../OrganizationUnitRepository_Tests.cs | 17 ++++++++++ 4 files changed, 89 insertions(+) diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs index 4d5bf7d060..bf39874e00 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs @@ -65,6 +65,12 @@ namespace Volo.Abp.Identity CancellationToken cancellationToken = default ); + Task GetUnaddedRolesCountAsync( + OrganizationUnit organizationUnit, + string filter = null, + CancellationToken cancellationToken = default + ); + Task> GetMembersAsync( OrganizationUnit organizationUnit, string sorting = null, @@ -91,6 +97,12 @@ namespace Volo.Abp.Identity CancellationToken cancellationToken = default ); + Task GetUnaddedUsersCountAsync( + OrganizationUnit organizationUnit, + string filter = null, + CancellationToken cancellationToken = default + ); + Task RemoveAllRolesAsync( OrganizationUnit organizationUnit, CancellationToken cancellationToken = default diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs index ebfa9b8ebb..59fbd9518e 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs @@ -132,6 +132,19 @@ namespace Volo.Abp.Identity.EntityFrameworkCore .ToListAsync(cancellationToken); } + public virtual async Task 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> GetMembersAsync( OrganizationUnit organizationUnit, string sorting = null, @@ -190,6 +203,24 @@ namespace Volo.Abp.Identity.EntityFrameworkCore .ToListAsync(cancellationToken); } + public virtual async Task GetUnaddedUsersCountAsync( + OrganizationUnit organizationUnit, + string filter = null, + CancellationToken cancellationToken = default) + { + var userIdsInOrganizationUnit = DbContext.Set() + .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 WithDetails() { return GetQueryable().IncludeDetails(); diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs index 2b28a2e3a1..10ae4cd038 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs @@ -126,6 +126,19 @@ namespace Volo.Abp.Identity.MongoDB .ToListAsync(cancellationToken); } + public async Task 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>() + .CountAsync(cancellationToken); + } + public virtual async Task> GetMembersAsync( OrganizationUnit organizationUnit, string sorting = null, @@ -178,6 +191,22 @@ namespace Volo.Abp.Identity.MongoDB .ToListAsync(GetCancellationToken(cancellationToken)); } + public async Task 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>( + !filter.IsNullOrWhiteSpace(), + u => + u.UserName.Contains(filter) || + u.Email.Contains(filter) || + (u.PhoneNumber != null && u.PhoneNumber.Contains(filter)) + ) + .As>() + .CountAsync(GetCancellationToken(cancellationToken)); + } + public virtual Task RemoveAllRolesAsync(OrganizationUnit organizationUnit, CancellationToken cancellationToken = default) { organizationUnit.Roles.Clear(); diff --git a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs index 97544cd1e9..a671ba3284 100644 --- a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs @@ -305,5 +305,22 @@ namespace Volo.Abp.Identity 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); + } } } From 431d02f177153c79ae6330a45c8c27de3d95a1fa Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 8 Sep 2020 14:26:45 +0300 Subject: [PATCH 33/86] chore: add deprecation message logger to replaceable-components.state --- .../lib/states/replaceable-components.state.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/npm/ng-packs/packages/core/src/lib/states/replaceable-components.state.ts b/npm/ng-packs/packages/core/src/lib/states/replaceable-components.state.ts index 61a5fef5f6..8b861f144e 100644 --- a/npm/ng-packs/packages/core/src/lib/states/replaceable-components.state.ts +++ b/npm/ng-packs/packages/core/src/lib/states/replaceable-components.state.ts @@ -1,10 +1,19 @@ -import { Injectable } from '@angular/core'; +import { Injectable, isDevMode } from '@angular/core'; import { Action, createSelector, Selector, State, StateContext } from '@ngxs/store'; import snq from 'snq'; import { AddReplaceableComponent } from '../actions/replaceable-components.actions'; import { ReplaceableComponents } from '../models/replaceable-components'; import { ReplaceableComponentsService } from '../services/replaceable-components.service'; +function logDeprecationMsg() { + if (isDevMode()) { + console.warn(` + ReplacableComponentsState has been deprecated. Use ReplaceableComponentsService instead. + See the doc https://docs.abp.io/en/abp/latest/UI/Angular/Component-Replacement + `); + } +} + // tslint:disable: max-line-length /** * @deprecated To be deleted in v4.0. Use ReplaceableComponentsService instead. See the doc (https://docs.abp.io/en/abp/latest/UI/Angular/Component-Replacement) @@ -19,6 +28,7 @@ export class ReplaceableComponentsState { static getAll({ replaceableComponents, }: ReplaceableComponents.State): ReplaceableComponents.ReplaceableComponent[] { + logDeprecationMsg(); return replaceableComponents || []; } @@ -26,6 +36,7 @@ export class ReplaceableComponentsState { const selector = createSelector( [ReplaceableComponentsState], (state: ReplaceableComponents.State): ReplaceableComponents.ReplaceableComponent => { + logDeprecationMsg(); return snq(() => state.replaceableComponents.find(component => component.key === key)); }, ); @@ -40,6 +51,8 @@ export class ReplaceableComponentsState { { getState, patchState }: StateContext, { payload, reload }: AddReplaceableComponent, ) { + logDeprecationMsg(); + let { replaceableComponents } = getState(); const index = snq( From d43dc102c76e0f34d2ce2341e9774309ff004c01 Mon Sep 17 00:00:00 2001 From: Yunus Emre Kalkan Date: Tue, 8 Sep 2020 15:30:40 +0300 Subject: [PATCH 34/86] Update Default.cshtml --- .../Pages/CmsKit/Shared/Components/Commenting/Default.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml index ab7fc9805b..f988dd29e7 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml @@ -175,7 +175,7 @@ } - @if (comment.Replies.Count >= 5) + @if (comment.Replies.Count >= 3) {
@if (CurrentUser.IsAuthenticated) From 9b44c00c1afd16b1499fa3d196858416bbc5987f Mon Sep 17 00:00:00 2001 From: maliming <6908465+maliming@users.noreply.github.com> Date: Tue, 8 Sep 2020 21:27:58 +0800 Subject: [PATCH 35/86] Use standard name as abp claim type. --- .../Volo/Abp/Security/Claims/AbpClaimTypes.cs | 54 +++++++++---------- .../Authorization/AuthorizationTestBase.cs | 2 +- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/AbpClaimTypes.cs b/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/AbpClaimTypes.cs index a797b360ee..e829ecddf3 100644 --- a/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/AbpClaimTypes.cs +++ b/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/AbpClaimTypes.cs @@ -8,71 +8,69 @@ namespace Volo.Abp.Security.Claims /// public static class AbpClaimTypes { - public const string ClaimTypeNamespace = "abp_"; - /// - /// Default: abp_username + /// Default: /// - public static string UserName { get; set; } = ClaimTypeNamespace + "username"; + public static string UserName { get; set; } = ClaimTypes.Name; /// - /// Default: abp_name + /// Default: /// - public static string Name { get; set; } = ClaimTypeNamespace + "name"; + public static string Name { get; set; } = ClaimTypes.GivenName; /// - /// Default: abp_surname + /// Default: /// - public static string SurName { get; set; } = ClaimTypeNamespace + "surname"; + public static string SurName { get; set; } = ClaimTypes.Surname; /// - /// Default: abp_user_id + /// Default: security_stamp /// - public static string UserId { get; set; } = ClaimTypeNamespace + "user_id"; + public static string SecurityStamp { get; set; } = "security_stamp"; /// - /// Default: abp_role + /// Default: /// - public static string Role { get; set; } = ClaimTypeNamespace + "role"; + public static string UserId { get; set; } = ClaimTypes.NameIdentifier; /// - /// Default: abp_security_stamp + /// Default: /// - public static string SecurityStamp { get; set; } = ClaimTypeNamespace + "security_stamp"; + public static string Role { get; set; } = ClaimTypes.Role; /// - /// Default: abp_email + /// Default: /// - public static string Email { get; set; } = ClaimTypeNamespace + "email"; + public static string Email { get; set; } = ClaimTypes.Email; /// - /// Default: abp_email_verified + /// Default: "email_verified". /// - public static string EmailVerified { get; set; } = ClaimTypeNamespace + "email_verified"; + public static string EmailVerified { get; set; } = "email_verified"; /// - /// Default: abp_phone_number + /// Default: "phone_number". /// - public static string PhoneNumber { get; set; } = ClaimTypeNamespace + "phone_number"; + public static string PhoneNumber { get; set; } = "phone_number"; /// - /// Default: abp_phone_number_verified + /// Default: "phone_number_verified". /// - public static string PhoneNumberVerified { get; set; } = ClaimTypeNamespace + "phone_number_verified"; + public static string PhoneNumberVerified { get; set; } = "phone_number_verified"; /// - /// Default: abp_tenant_id + /// Default: "tenantid". /// - public static string TenantId { get; set; } = ClaimTypeNamespace + "tenant_id"; + public static string TenantId { get; set; } = "tenant_id"; /// - /// Default: abp_edition_id + /// Default: "editionid". /// - public static string EditionId { get; set; } = ClaimTypeNamespace + "edition_id"; + public static string EditionId { get; set; } = "edition_id"; /// - /// Default: abp_client_id + /// Default: "client_id". /// - public static string ClientId { get; set; } = ClaimTypeNamespace + "client_id"; + public static string ClientId { get; set; } = "client_id"; } } diff --git a/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/AuthorizationTestBase.cs b/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/AuthorizationTestBase.cs index 89f45600f4..5cf9976e46 100644 --- a/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/AuthorizationTestBase.cs +++ b/framework/test/Volo.Abp.Authorization.Tests/Volo/Abp/Authorization/AuthorizationTestBase.cs @@ -23,7 +23,7 @@ namespace Volo.Abp.Authorization new Claim(AbpClaimTypes.Role, "MyRole") }; - var identity = new ClaimsIdentity(claims,null, AbpClaimTypes.UserName, AbpClaimTypes.Role); + var identity = new ClaimsIdentity(claims); var claimsPrincipal = new ClaimsPrincipal(identity); var principalAccessor = Substitute.For(); principalAccessor.Principal.Returns(ci => claimsPrincipal); From 69f6caca9171604549da09235883784d2a23f563 Mon Sep 17 00:00:00 2001 From: maliming <6908465+maliming@users.noreply.github.com> Date: Tue, 8 Sep 2020 21:29:32 +0800 Subject: [PATCH 36/86] Update AbpClaimTypes.cs --- .../Volo/Abp/Security/Claims/AbpClaimTypes.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/AbpClaimTypes.cs b/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/AbpClaimTypes.cs index e829ecddf3..6d6672ec2b 100644 --- a/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/AbpClaimTypes.cs +++ b/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/AbpClaimTypes.cs @@ -59,12 +59,12 @@ namespace Volo.Abp.Security.Claims public static string PhoneNumberVerified { get; set; } = "phone_number_verified"; /// - /// Default: "tenantid". + /// Default: "tenant_id". /// public static string TenantId { get; set; } = "tenant_id"; /// - /// Default: "editionid". + /// Default: "edition_id". /// public static string EditionId { get; set; } = "edition_id"; From 835e5466c0e62377d948ed9deece0be00bc46e36 Mon Sep 17 00:00:00 2001 From: maliming <6908465+maliming@users.noreply.github.com> Date: Tue, 8 Sep 2020 22:20:24 +0800 Subject: [PATCH 37/86] Refactor --- .../Volo/Abp/Features/AbpFeaturesModule.cs | 1 - .../Abp/Features/HostFeatureValueProvider.cs | 30 ---------- .../FeatureManagement/FeatureAppService.cs | 33 +++++++---- .../Localization/Domain/en.json | 1 - .../Localization/Domain/tr.json | 1 - .../Localization/Domain/zh-Hans.json | 1 - .../Localization/Domain/zh-Hant.json | 1 - .../AbpFeatureManagementDomainModule.cs | 3 - .../HostFeatureManagementProvider.cs | 59 ------------------- .../Pages/TenantManagement/Tenants/Index.js | 2 +- 10 files changed, 21 insertions(+), 111 deletions(-) delete mode 100644 framework/src/Volo.Abp.Features/Volo/Abp/Features/HostFeatureValueProvider.cs delete mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/HostFeatureManagementProvider.cs diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/AbpFeaturesModule.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/AbpFeaturesModule.cs index 7db32231e0..f30017feed 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/AbpFeaturesModule.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/AbpFeaturesModule.cs @@ -26,7 +26,6 @@ namespace Volo.Abp.Features context.Services.Configure(options => { options.ValueProviders.Add(); - options.ValueProviders.Add(); options.ValueProviders.Add(); options.ValueProviders.Add(); }); diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/HostFeatureValueProvider.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/HostFeatureValueProvider.cs deleted file mode 100644 index d895813e6f..0000000000 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/HostFeatureValueProvider.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Threading.Tasks; -using Volo.Abp.MultiTenancy; - -namespace Volo.Abp.Features -{ - public class HostFeatureValueProvider : FeatureValueProvider - { - public const string ProviderName = "H"; - - public override string Name => ProviderName; - - protected ICurrentTenant CurrentTenant { get; } - - public HostFeatureValueProvider(IFeatureStore featureStore, ICurrentTenant currentTenant) - : base(featureStore) - { - CurrentTenant = currentTenant; - } - - public override async Task GetOrNullAsync(FeatureDefinition feature) - { - if (CurrentTenant.Id.HasValue || !feature.IsAvailableToHost) - { - return null; - } - - return await FeatureStore.GetOrNullAsync(feature.Name, Name, null); - } - } -} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs index 5c19df8751..6f0fe4815e 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs @@ -28,7 +28,7 @@ namespace Volo.Abp.FeatureManagement public virtual async Task GetAsync([NotNull] string providerName, string providerKey) { - await CheckProviderPolicy(providerName); + await CheckProviderPolicy(providerName, providerKey); var result = new GetFeatureListResultDto { @@ -46,7 +46,10 @@ namespace Volo.Abp.FeatureManagement foreach (var featureDefinition in group.GetFeaturesWithChildren()) { - if (providerName == HostFeatureValueProvider.ProviderName && !featureDefinition.IsAvailableToHost) + if (providerName == TenantFeatureValueProvider.ProviderName && + CurrentTenant.Id == null && + providerKey == null && + !featureDefinition.IsAvailableToHost) { continue; } @@ -78,16 +81,10 @@ namespace Volo.Abp.FeatureManagement public virtual async Task UpdateAsync([NotNull] string providerName, string providerKey, UpdateFeaturesDto input) { - await CheckProviderPolicy(providerName); + await CheckProviderPolicy(providerName, providerKey); foreach (var feature in input.Features) { - var featureDefinition = FeatureDefinitionManager.GetOrNull(feature.Name); - if (featureDefinition == null || (providerName == HostFeatureValueProvider.ProviderName && !featureDefinition.IsAvailableToHost)) - { - throw new UserFriendlyException(L["FeatureNotAvailable"]); - } - await FeatureManager.SetAsync(feature.Name, feature.Value, providerName, providerKey); } } @@ -105,12 +102,22 @@ namespace Volo.Abp.FeatureManagement } } - protected virtual async Task CheckProviderPolicy(string providerName) + protected virtual async Task CheckProviderPolicy(string providerName, string providerKey) { - var policyName = Options.ProviderPolicies.GetOrDefault(providerName); - if (policyName.IsNullOrEmpty()) + string policyName; + if (providerName == TenantFeatureValueProvider.ProviderName) { - throw new AbpException($"No policy defined to get/set permissions for the provider '{policyName}'. Use {nameof(FeatureManagementOptions)} to map the policy."); + policyName = CurrentTenant.Id == null && providerKey == null ? + "FeatureManagement.ManageHostFeatures" : + "AbpTenantManagement.Tenants.ManageFeatures"; + } + else + { + policyName = Options.ProviderPolicies.GetOrDefault(providerName); + if (policyName.IsNullOrEmpty()) + { + throw new AbpException($"No policy defined to get/set permissions for the provider '{policyName}'. Use {nameof(FeatureManagementOptions)} to map the policy."); + } } await AuthorizationService.CheckAsync(policyName); diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/en.json b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/en.json index 6e17e8fc48..26f5dc736b 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/en.json +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/en.json @@ -3,7 +3,6 @@ "texts": { "Features": "Features", "NoFeatureFoundMessage": "There isn't any available feature.", - "FeatureNotAvailable": "Feature not available.", "Permission:FeatureManagement": "Feature management", "Permission:FeatureManagement.ManageHostFeatures": "Manage Host features" } diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/tr.json b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/tr.json index 285b4143cf..8233674933 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/tr.json +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/tr.json @@ -3,7 +3,6 @@ "texts": { "Features": "Özellikler", "NoFeatureFoundMessage": "Hiç özellik yok.", - "FeatureNotAvailable": "Özellik bulunamadı.", "Permission:FeatureManagement": "Özellik yönetimi", "Permission:FeatureManagement.ManageHostFeatures": "Host özelliklerini düzenle" } diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/zh-Hans.json b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/zh-Hans.json index 2ecdb3997b..9737a9a59c 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/zh-Hans.json +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/zh-Hans.json @@ -3,7 +3,6 @@ "texts": { "Features": "功能", "NoFeatureFoundMessage": "没有可用的功能.", - "FeatureNotAvailable": "功能不可用.", "Permission:FeatureManagement": "特性管理", "Permission:FeatureManagement.ManageHostFeatures": "管理Host特性" } diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/zh-Hant.json b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/zh-Hant.json index 6a8ccd099b..7d68fd5bc2 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/zh-Hant.json +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/zh-Hant.json @@ -3,7 +3,6 @@ "texts": { "Features": "功能", "NoFeatureFoundMessage": "沒有可用的功能.", - "FeatureNotAvailable": "功能不可用.", "Permission:FeatureManagement": "功能管理", "Permission:FeatureManagement.ManageHostFeatures": "管理Host功能" } diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs index b9992309dd..34a0e198c7 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs @@ -18,13 +18,10 @@ namespace Volo.Abp.FeatureManagement Configure(options => { options.Providers.Add(); - options.Providers.Add(); - options.ProviderPolicies[HostFeatureValueProvider.ProviderName] = "FeatureManagement.ManageHostFeatures"; options.Providers.Add(); //TODO: Should be moved to the Tenant Management module options.Providers.Add(); - options.ProviderPolicies[TenantFeatureValueProvider.ProviderName] = "AbpTenantManagement.Tenants.ManageFeatures"; }); Configure(options => diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/HostFeatureManagementProvider.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/HostFeatureManagementProvider.cs deleted file mode 100644 index d40d2dfdec..0000000000 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/HostFeatureManagementProvider.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Features; -using Volo.Abp.MultiTenancy; - -namespace Volo.Abp.FeatureManagement -{ - public class HostFeatureManagementProvider : FeatureManagementProvider, ITransientDependency - { - public override string Name => HostFeatureValueProvider.ProviderName; - - protected ICurrentTenant CurrentTenant { get; } - - public HostFeatureManagementProvider( - IFeatureManagementStore store, - ICurrentTenant currentTenant) - : base(store) - { - CurrentTenant = currentTenant; - } - - public override async Task GetOrNullAsync(FeatureDefinition feature, string providerKey) - { - if (IsHostSide(feature)) - { - return await Store.GetOrNullAsync(feature.Name, Name, NormalizeProviderKey(providerKey)); - } - - return null; - } - - public override async Task SetAsync(FeatureDefinition feature, string value, string providerKey) - { - if (IsHostSide(feature)) - { - await Store.SetAsync(feature.Name, value, Name, NormalizeProviderKey(providerKey)); - } - } - - public override async Task ClearAsync(FeatureDefinition feature, string providerKey) - { - if (IsHostSide(feature)) - { - await Store.DeleteAsync(feature.Name, Name, NormalizeProviderKey(providerKey)); - } - } - - protected override string NormalizeProviderKey(string providerKey) - { - return null; - } - - //TODO: Should throw an ex when there is not in the host side? - protected virtual bool IsHostSide(FeatureDefinition feature) - { - return feature.IsAvailableToHost && CurrentTenant.Id == null; - } - } -} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.js b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.js index ecb2309f36..69bbb0b955 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.js +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.js @@ -132,7 +132,7 @@ _$wrapper.find('button[name=ManageHostFeatures]').click(function (e) { e.preventDefault(); _featuresModal.open({ - providerName: 'H' + providerName: 'T' }); }); }); From cd91017415ab506d2cd0dafed9f047b4caa2ce38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ahmet=20=C3=87otur?= Date: Tue, 8 Sep 2020 17:31:32 +0300 Subject: [PATCH 38/86] Update AbpComponentDemoSectionTagHelper.cs --- .../Shared/TagHelpers/AbpComponentDemoSectionTagHelper.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/TagHelpers/AbpComponentDemoSectionTagHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/TagHelpers/AbpComponentDemoSectionTagHelper.cs index 722460992d..1f1f71b066 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/TagHelpers/AbpComponentDemoSectionTagHelper.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/TagHelpers/AbpComponentDemoSectionTagHelper.cs @@ -47,7 +47,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.S output.PreContent.AppendHtml($"

{Title}

"); output.PreContent.AppendHtml("
"); output.PreContent.AppendHtml("