");
- output.PostContent.AppendHtml("
");
+ output.PostContent.AppendHtml("");
output.PostContent.AppendHtml("");
output.PostContent.Append(content.GetContent());
output.PostContent.AppendHtml("");
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.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.Security/Volo/Abp/Security/Claims/AbpClaimTypes.cs b/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/AbpClaimTypes.cs
index d0b883e4f3..271d5d3d94 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
@@ -13,6 +13,16 @@ namespace Volo.Abp.Security.Claims
///
public static string UserName { get; set; } = ClaimTypes.Name;
+ ///
+ /// Default:
+ ///
+ public static string Name { get; set; } = ClaimTypes.GivenName;
+
+ ///
+ /// Default:
+ ///
+ public static string SurName { get; set; } = ClaimTypes.Surname;
+
///
/// Default:
///
@@ -48,7 +58,6 @@ namespace Volo.Abp.Security.Claims
///
public static string TenantId { get; set; } = "tenantid";
-
///
/// Default: "editionid".
///
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/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/DataFiltering/HardDelete_Tests.cs b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/DataFiltering/HardDelete_Tests.cs
index 66c6e58b5c..699e6de5a3 100644
--- a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/DataFiltering/HardDelete_Tests.cs
+++ b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/DataFiltering/HardDelete_Tests.cs
@@ -2,9 +2,11 @@
using System.Collections.Generic;
using System.Text;
using Volo.Abp.TestApp.Testing;
+using Xunit;
namespace Volo.Abp.MongoDB.DataFiltering
{
+ [Collection(MongoTestCollection.Name)]
public class HardDelete_Tests : HardDelete_Tests
{
}
diff --git a/global.json b/global.json
new file mode 100644
index 0000000000..6156170057
--- /dev/null
+++ b/global.json
@@ -0,0 +1,6 @@
+{
+ "sdk": {
+ "version": "3.1.102",
+ "rollForward": "latestFeature"
+ }
+}
\ No newline at end of file
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 00c36b7f55..9ddddc7401 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,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 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 dbe649e51f..bfb6e65ea8 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;
@@ -25,9 +26,9 @@ 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);
+ await CheckProviderPolicy(providerName, providerKey);
var result = new GetFeatureListResultDto
{
@@ -45,6 +46,14 @@ namespace Volo.Abp.FeatureManagement
foreach (var featureDefinition in group.GetFeaturesWithChildren())
{
+ if (providerName == TenantFeatureValueProvider.ProviderName &&
+ CurrentTenant.Id == null &&
+ providerKey == null &&
+ !featureDefinition.IsAvailableToHost)
+ {
+ continue;
+ }
+
var feature = await FeatureManager.GetOrNullWithProviderAsync(featureDefinition.Name, providerName, providerKey);
groupDto.Features.Add(new FeatureDto
{
@@ -70,9 +79,9 @@ namespace Volo.Abp.FeatureManagement
return result;
}
- 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);
+ await CheckProviderPolicy(providerName, providerKey);
foreach (var feature in input.Features)
{
@@ -93,12 +102,20 @@ 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 && CurrentTenant.Id == null && providerKey == null )
+ {
+ policyName = "FeatureManagement.ManageHostFeatures";
+ }
+ else
{
- throw new AbpException($"No policy defined to get/set permissions for the provider '{policyName}'. Use {nameof(FeatureManagementOptions)} to map the policy.");
+ 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 e5a2406aaf..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
@@ -2,6 +2,8 @@
"culture": "en",
"texts": {
"Features": "Features",
- "NoFeatureFoundMessage": "There isn't any available feature."
+ "NoFeatureFoundMessage": "There isn't any available feature.",
+ "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..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
@@ -2,6 +2,8 @@
"culture": "tr",
"texts": {
"Features": "Özellikler",
- "NoFeatureFoundMessage": "Hiç özellik yok."
+ "NoFeatureFoundMessage": "Hiç özellik yok.",
+ "Permission:FeatureManagement": "Özellik yönetimi",
+ "Permission:FeatureManagement.ManageHostFeatures": "Host özelliklerini düzenle"
}
-}
\ 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..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
@@ -2,6 +2,8 @@
"culture": "zh-Hans",
"texts": {
"Features": "功能",
- "NoFeatureFoundMessage": "没有可用的功能."
+ "NoFeatureFoundMessage": "没有可用的功能.",
+ "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..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
@@ -2,6 +2,8 @@
"culture": "zh-Hant",
"texts": {
"Features": "功能",
- "NoFeatureFoundMessage": "沒有可用的功能."
+ "NoFeatureFoundMessage": "沒有可用的功能.",
+ "Permission:FeatureManagement": "功能管理",
+ "Permission:FeatureManagement.ManageHostFeatures": "管理Host功能"
}
-}
\ No newline at end of file
+}
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..38a2fa3c56 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
{
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 cc00fb370b..c7dff4d8a3 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
@@ -28,4 +28,4 @@ namespace Volo.Abp.FeatureManagement
return FeatureAppService.UpdateAsync(providerName, providerKey, 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 1464a1fc9b..367268019c 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; }
@@ -83,8 +82,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/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..6feddd125b 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,13 @@ 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;
+ });
+
AddAbpIdentityOptionsFactory(context.Services);
}
@@ -67,7 +75,7 @@ namespace Volo.Abp.Identity
IdentityModuleExtensionConsts.EntityNames.ClaimType,
typeof(IdentityClaimType)
);
-
+
ModuleExtensionConfigurationHelper.ApplyEntityConfigurationToEntity(
IdentityModuleExtensionConsts.ModuleName,
IdentityModuleExtensionConsts.EntityNames.OrganizationUnit,
@@ -81,4 +89,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;
+ }
+ }
+}
diff --git a/modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/OrganizationUnitRepository_Tests.cs b/modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/OrganizationUnitRepository_Tests.cs
index d60611b2b6..dc134f9974 100644
--- a/modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/OrganizationUnitRepository_Tests.cs
+++ b/modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/OrganizationUnitRepository_Tests.cs
@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
+using Xunit;
namespace Volo.Abp.Identity.MongoDB
{
+ [Collection(MongoTestCollection.Name)]
public class OrganizationUnitRepository_Tests : OrganizationUnitRepository_Tests
{
}
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..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
@@ -100,7 +100,7 @@
},
0 //adds as the first contributor
);
-
+
$(function () {
var _$wrapper = $('#TenantsWrapper');
@@ -128,5 +128,12 @@
e.preventDefault();
_createModal.open();
});
+
+ _$wrapper.find('button[name=ManageHostFeatures]').click(function (e) {
+ e.preventDefault();
+ _featuresModal.open({
+ providerName: 'T'
+ });
+ });
});
})();
diff --git a/npm/ng-packs/package.json b/npm/ng-packs/package.json
index 32ff245f95..7d749a8d9c 100644
--- a/npm/ng-packs/package.json
+++ b/npm/ng-packs/package.json
@@ -55,11 +55,11 @@
"@ng-bootstrap/ng-bootstrap": "^7.0.0",
"@ngneat/spectator": "^5.13.0",
"@ngx-validate/core": "^0.0.11",
- "@ngxs/devtools-plugin": "^3.6.2",
- "@ngxs/logger-plugin": "^3.6.2",
- "@ngxs/router-plugin": "^3.6.2",
- "@ngxs/storage-plugin": "^3.6.2",
- "@ngxs/store": "^3.6.2",
+ "@ngxs/devtools-plugin": "^3.7.0",
+ "@ngxs/logger-plugin": "^3.7.0",
+ "@ngxs/router-plugin": "^3.7.0",
+ "@ngxs/storage-plugin": "^3.7.0",
+ "@ngxs/store": "^3.7.0",
"@schematics/angular": "~10.0.5",
"@swimlane/ngx-datatable": "^17.1.0",
"@types/jest": "^25.2.3",
diff --git a/npm/ng-packs/packages/core/package.json b/npm/ng-packs/packages/core/package.json
index 06eeb447a5..c3531c750b 100644
--- a/npm/ng-packs/packages/core/package.json
+++ b/npm/ng-packs/packages/core/package.json
@@ -9,9 +9,9 @@
"dependencies": {
"@abp/utils": "^3.1.0",
"@angular/localize": "~10.0.10",
- "@ngxs/router-plugin": "^3.6.2",
- "@ngxs/storage-plugin": "^3.6.2",
- "@ngxs/store": "^3.6.2",
+ "@ngxs/router-plugin": "^3.7.0",
+ "@ngxs/storage-plugin": "^3.7.0",
+ "@ngxs/store": "^3.7.0",
"angular-oauth2-oidc": "^10.0.0",
"just-clone": "^3.1.0",
"just-compare": "^1.3.0",
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/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/models/utility.ts b/npm/ng-packs/packages/core/src/lib/models/utility.ts
index e84ad15340..228cab0de3 100644
--- a/npm/ng-packs/packages/core/src/lib/models/utility.ts
+++ b/npm/ng-packs/packages/core/src/lib/models/utility.ts
@@ -1,4 +1,13 @@
import { TemplateRef, Type } from '@angular/core';
+export type DeepPartial = {
+ [P in keyof T]?: T[P] extends Serializable ? DeepPartial : T[P];
+};
+
+type Serializable = Record<
+ string | number | symbol,
+ string | number | boolean | Record
+>;
+
export type InferredInstanceOf = T extends Type ? U : never;
export type InferredContextOf = T extends TemplateRef ? U : never;
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..674db83aea
--- /dev/null
+++ b/npm/ng-packs/packages/core/src/lib/services/replaceable-components.service.ts
@@ -0,0 +1,57 @@
+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';
+import { InternalStore } from '../utils/internal-store-utils';
+import { reloadRoute } from '../utils/route-utils';
+
+@Injectable({ providedIn: 'root' })
+export class ReplaceableComponentsService {
+ private store: InternalStore;
+
+ get replaceableComponents$(): Observable {
+ return this.store.sliceState(state => state);
+ }
+
+ get replaceableComponents(): ReplaceableComponents.ReplaceableComponent[] {
+ return this.store.state;
+ }
+
+ get onUpdate$(): Observable {
+ return this.store.sliceUpdate(state => state);
+ }
+
+ constructor(private ngZone: NgZone, private router: Router) {
+ this.store = new InternalStore([]);
+ }
+
+ add(replaceableComponent: ReplaceableComponents.ReplaceableComponent, reload?: boolean): void {
+ const replaceableComponents = [...this.store.state];
+
+ const index = replaceableComponents.findIndex(
+ component => component.key === replaceableComponent.key,
+ );
+
+ if (index > -1) {
+ replaceableComponents[index] = replaceableComponent;
+ } else {
+ replaceableComponents.push(replaceableComponent);
+ }
+
+ this.store.patch(replaceableComponents);
+
+ if (reload) reloadRoute(this.router, this.ngZone);
+ }
+
+ 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)),
+ );
+ }
+}
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..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,11 +1,23 @@
-import { Injectable, NgZone } from '@angular/core';
-import { Router } from '@angular/router';
+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 { noop } from '../utils/common-utils';
+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)
+ */
@State({
name: 'ReplaceableComponentsState',
defaults: { replaceableComponents: [] } as ReplaceableComponents.State,
@@ -16,6 +28,7 @@ export class ReplaceableComponentsState {
static getAll({
replaceableComponents,
}: ReplaceableComponents.State): ReplaceableComponents.ReplaceableComponent[] {
+ logDeprecationMsg();
return replaceableComponents || [];
}
@@ -23,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));
},
);
@@ -30,29 +44,15 @@ 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(
{ getState, patchState }: StateContext,
{ payload, reload }: AddReplaceableComponent,
) {
+ logDeprecationMsg();
+
let { replaceableComponents } = getState();
const index = snq(
@@ -69,6 +69,6 @@ export class ReplaceableComponentsState {
replaceableComponents,
});
- if (reload) this.reloadRoute();
+ this.service.add(payload, reload);
}
}
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/core/src/lib/tests/internal-store.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/internal-store.spec.ts
new file mode 100644
index 0000000000..e0eed44eb6
--- /dev/null
+++ b/npm/ng-packs/packages/core/src/lib/tests/internal-store.spec.ts
@@ -0,0 +1,108 @@
+import clone from 'just-clone';
+import { take } from 'rxjs/operators';
+import { DeepPartial } from '../models';
+import { InternalStore } from '../utils';
+
+const mockInitialState = {
+ foo: {
+ bar: {
+ baz: [() => {}],
+ qux: null as Promise,
+ },
+ n: 0,
+ },
+ x: '',
+ a: false,
+};
+
+type MockState = typeof mockInitialState;
+
+const patch1: DeepPartial = { foo: { bar: { baz: [() => {}] } } };
+const expected1: MockState = clone(mockInitialState);
+expected1.foo.bar.baz = patch1.foo.bar.baz;
+
+const patch2: DeepPartial = { foo: { bar: { qux: Promise.resolve() } } };
+const expected2: MockState = clone(mockInitialState);
+expected2.foo.bar.qux = patch2.foo.bar.qux;
+
+const patch3: DeepPartial = { foo: { n: 1 } };
+const expected3: MockState = clone(mockInitialState);
+expected3.foo.n = patch3.foo.n;
+
+const patch4: DeepPartial = { x: 'X' };
+const expected4: MockState = clone(mockInitialState);
+expected4.x = patch4.x;
+
+const patch5: DeepPartial = { a: true };
+const expected5: MockState = clone(mockInitialState);
+expected5.a = patch5.a;
+
+describe('Internal Store', () => {
+ describe('sliceState', () => {
+ test.each`
+ selector | expected
+ ${(state: MockState) => state.a} | ${mockInitialState.a}
+ ${(state: MockState) => state.x} | ${mockInitialState.x}
+ ${(state: MockState) => state.foo.n} | ${mockInitialState.foo.n}
+ ${(state: MockState) => state.foo.bar} | ${mockInitialState.foo.bar}
+ ${(state: MockState) => state.foo.bar.baz} | ${mockInitialState.foo.bar.baz}
+ ${(state: MockState) => state.foo.bar.qux} | ${mockInitialState.foo.bar.qux}
+ `(
+ 'should return observable $expected when selector is $selector',
+ async ({ selector, expected }) => {
+ const store = new InternalStore(mockInitialState);
+
+ const value = await store
+ .sliceState(selector)
+ .pipe(take(1))
+ .toPromise();
+
+ expect(value).toEqual(expected);
+ },
+ );
+ });
+
+ describe('patchState', () => {
+ test.each`
+ patch | expected
+ ${patch1} | ${expected1}
+ ${patch2} | ${expected2}
+ ${patch3} | ${expected3}
+ ${patch4} | ${expected4}
+ ${patch5} | ${expected5}
+ `('should set state as $expected when patch is $patch', ({ patch, expected }) => {
+ const store = new InternalStore(mockInitialState);
+
+ store.patch(patch);
+
+ expect(store.state).toEqual(expected);
+ });
+ });
+
+ describe('sliceUpdate', () => {
+ it('should return slice of update$ based on selector', done => {
+ const store = new InternalStore(mockInitialState);
+
+ const onQux$ = store.sliceUpdate(state => state.foo.bar.qux);
+
+ onQux$.pipe(take(1)).subscribe(value => {
+ expect(value).toEqual(patch2.foo.bar.qux);
+ done();
+ });
+
+ store.patch(patch1);
+ store.patch(patch2);
+ });
+ });
+
+ describe('reset', () => {
+ it('should reset state to initialState', () => {
+ const store = new InternalStore(mockInitialState);
+
+ store.patch(patch1);
+ store.reset();
+
+ expect(store.state).toEqual(mockInitialState);
+ });
+ });
+});
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..9b8b62ef46 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
@@ -1,10 +1,9 @@
-import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
import { Component } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
-import { Store } from '@ngxs/store';
-import { of, Subject, BehaviorSubject } from 'rxjs';
+import { ActivatedRoute, Router } from '@angular/router';
+import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
+import { 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,18 +29,18 @@ const activatedRouteMock = {
};
describe('ReplaceableRouteContainerComponent', () => {
- const selectResponse = new BehaviorSubject(undefined);
- const mockSelect = jest.fn(() => selectResponse);
-
let spectator: SpectatorHost;
+ const get$Res = new BehaviorSubject(undefined);
+
const createHost = createHostFactory({
component: ReplaceableRouteContainerComponent,
providers: [
{ provide: ActivatedRoute, useValue: activatedRouteMock },
- { provide: Store, useValue: { select: mockSelect } },
+ { provide: ReplaceableComponentsService, useValue: { get$: () => get$Res } },
],
declarations: [ExternalComponent, DefaultComponent],
entryComponents: [DefaultComponent, ExternalComponent],
+ mocks: [Router],
});
beforeEach(() => {
@@ -55,11 +54,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..2c1432e525 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
@@ -1,9 +1,11 @@
import { Component, EventEmitter, Inject, Input, OnInit, Optional, Output } from '@angular/core';
import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest';
import { Store } from '@ngxs/store';
-import { Subject } from 'rxjs';
+import { BehaviorSubject } 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,15 @@ class ExternalComponent {
}
describe('ReplaceableTemplateDirective', () => {
- const selectResponse = new Subject();
- const mockSelect = jest.fn(() => selectResponse);
-
let spectator: SpectatorDirective;
+ const get$Res = new BehaviorSubject(undefined);
+
const createDirective = createDirectiveFactory({
directive: ReplaceableTemplateDirective,
- providers: [{ provide: Store, useValue: { select: mockSelect } }],
declarations: [DefaultComponent, ExternalComponent],
entryComponents: [ExternalComponent],
+ mocks: [Router],
+ providers: [{ provide: ReplaceableComponentsService, useValue: { get$: () => get$Res } }],
});
describe('without external component', () => {
@@ -72,7 +74,7 @@ describe('ReplaceableTemplateDirective', () => {
`,
{ hostProps: { oneWay: { label: 'Test' }, twoWay: false, twoWayChange, someOutput } },
);
- selectResponse.next(undefined);
+
const component = spectator.query(DefaultComponent);
spectator.directive.context.initTemplate(component);
spectator.detectChanges();
@@ -114,7 +116,8 @@ 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 +153,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 +164,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();
});
});
diff --git a/npm/ng-packs/packages/core/src/lib/utils/file-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/file-utils.ts
new file mode 100644
index 0000000000..b5241ec096
--- /dev/null
+++ b/npm/ng-packs/packages/core/src/lib/utils/file-utils.ts
@@ -0,0 +1,20 @@
+export function downloadBlob(blob: Blob, filename: string) {
+ const blobUrl = URL.createObjectURL(blob);
+
+ const link = document.createElement('a');
+
+ link.href = blobUrl;
+ link.download = filename;
+
+ document.body.appendChild(link);
+
+ link.dispatchEvent(
+ new MouseEvent('click', {
+ bubbles: true,
+ cancelable: true,
+ view: window,
+ }),
+ );
+
+ document.body.removeChild(link);
+}
diff --git a/npm/ng-packs/packages/core/src/lib/utils/index.ts b/npm/ng-packs/packages/core/src/lib/utils/index.ts
index 383261fc07..2d7ef8eab4 100644
--- a/npm/ng-packs/packages/core/src/lib/utils/index.ts
+++ b/npm/ng-packs/packages/core/src/lib/utils/index.ts
@@ -3,9 +3,11 @@ export * from './common-utils';
export * from './date-utils';
export * from './environment-utils';
export * from './factory-utils';
+export * from './file-utils';
export * from './form-utils';
export * from './generator-utils';
export * from './initial-utils';
+export * from './internal-store-utils';
export * from './lazy-load-utils';
export * from './localization-utils';
export * from './multi-tenancy-utils';
diff --git a/npm/ng-packs/packages/core/src/lib/utils/internal-store-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/internal-store-utils.ts
new file mode 100644
index 0000000000..7a481121ee
--- /dev/null
+++ b/npm/ng-packs/packages/core/src/lib/utils/internal-store-utils.ts
@@ -0,0 +1,36 @@
+import compare from 'just-compare';
+import { BehaviorSubject, Subject } from 'rxjs';
+import { distinctUntilChanged, filter, map } from 'rxjs/operators';
+import { DeepPartial } from '../models';
+import { deepMerge } from './object-utils';
+
+export class InternalStore {
+ private state$ = new BehaviorSubject(this.initialState);
+
+ private update$ = new Subject>();
+
+ get state() {
+ return this.state$.value;
+ }
+
+ sliceState = (
+ selector: (state: State) => Slice,
+ compareFn: (s1: Slice, s2: Slice) => boolean = compare,
+ ) => this.state$.pipe(map(selector), distinctUntilChanged(compareFn));
+
+ sliceUpdate = (
+ selector: (state: DeepPartial) => Slice,
+ filterFn = (x: Slice) => x !== undefined,
+ ) => this.update$.pipe(map(selector), filter(filterFn));
+
+ constructor(private initialState: State) {}
+
+ patch(state: DeepPartial) {
+ this.state$.next(deepMerge(this.state, state));
+ this.update$.next(state);
+ }
+
+ reset() {
+ this.patch(this.initialState);
+ }
+}
diff --git a/npm/ng-packs/packages/core/src/lib/utils/route-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/route-utils.ts
index a749090695..388e76cbf8 100644
--- a/npm/ng-packs/packages/core/src/lib/utils/route-utils.ts
+++ b/npm/ng-packs/packages/core/src/lib/utils/route-utils.ts
@@ -2,6 +2,8 @@ import { PRIMARY_OUTLET, Router, UrlSegmentGroup } from '@angular/router';
import { ABP } from '../models/common';
import { RoutesService } from '../services/routes.service';
import { TreeNode } from './tree-utils';
+import { noop } from './common-utils';
+import { NgZone } from '@angular/core';
export function findRoute(routes: RoutesService, path: string): TreeNode {
const node = routes.find(route => route.path === path);
@@ -23,3 +25,18 @@ export function getRoutePath(router: Router, url = router.url) {
return '/' + (primaryGroup || emptyGroup).segments.map(({ path }) => path).join('/');
}
+
+export function reloadRoute(router: Router, ngZone: NgZone) {
+ const { shouldReuseRoute } = router.routeReuseStrategy;
+ const setRouteReuse = (reuse: typeof shouldReuseRoute) => {
+ router.routeReuseStrategy.shouldReuseRoute = reuse;
+ };
+
+ setRouteReuse(() => false);
+ router.navigated = false;
+
+ ngZone.run(async () => {
+ await router.navigateByUrl(router.url).catch(noop);
+ setRouteReuse(shouldReuseRoute);
+ });
+}
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,
+ });
}
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;
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..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
@@ -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: MockDocument) {
this.table.columnMode = ColumnMode.force;
this.table.footerHeight = 50;
this.table.headerHeight = 50;
@@ -22,4 +29,56 @@ 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();
+ }
+}
+
+// fix: https://github.com/angular/angular/issues/20351
+interface MockDocument {
+ body: MockBody;
+ documentElement: MockDocumentElement;
+}
+
+interface MockBody {
+ offsetWidth: number;
+}
+
+interface MockDocumentElement {
+ clientHeight: number;
+ scrollHeight: number;
}
diff --git a/npm/ng-packs/yarn.lock b/npm/ng-packs/yarn.lock
index 512eb7d30b..1af94b7c24 100644
--- a/npm/ng-packs/yarn.lock
+++ b/npm/ng-packs/yarn.lock
@@ -2338,17 +2338,17 @@
dependencies:
tslib "^1.9.0"
-"@ngxs/devtools-plugin@^3.6.2":
- version "3.6.2"
- resolved "https://registry.yarnpkg.com/@ngxs/devtools-plugin/-/devtools-plugin-3.6.2.tgz#aa0a4835f90fb905951d7712dc3ce508cbc15a2c"
- integrity sha512-0UUZlpXgEtrHoWNeVQXEvUyC6pW8nTACpqJgecuBjYJMa5imCCUSXdrpear8ztJuWwpLqMUSGk5cICNhKqK59g==
+"@ngxs/devtools-plugin@^3.7.0":
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/@ngxs/devtools-plugin/-/devtools-plugin-3.7.0.tgz#5b6b3e63411da527fcee1e8280714e1b95a838c7"
+ integrity sha512-jjq91AbnnhzSm4QRUd7M0Y+HnUYnsSTVwUy8c1BsH8rGQ9c77xIveQaF2UBngPaDKQzTyzdVO2rV7twy3W2/fg==
dependencies:
tslib "^1.9.0"
-"@ngxs/logger-plugin@^3.6.2":
- version "3.6.2"
- resolved "https://registry.yarnpkg.com/@ngxs/logger-plugin/-/logger-plugin-3.6.2.tgz#1a8132a0b1eb95cad79736120fd50a331fe7347b"
- integrity sha512-qF4ci9Ua7jL7r5EhZBEb0JB0HuGsTgFShK3OVTYSXU9+XfSb+5vrDLtmTshmBqQy6dWCd+2xq4LzBgc8IKMRuQ==
+"@ngxs/logger-plugin@^3.7.0":
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/@ngxs/logger-plugin/-/logger-plugin-3.7.0.tgz#a27d6bb27360fc2677773c57868740b8cd33cc41"
+ integrity sha512-yr3NXXJEqJnxSuKiHaTbNmoprKKcrIS1PUIlOm9nvKsOmyrGskxu+MYCzUifemXDIyXSP2u0OBNPrVUumcXyhg==
dependencies:
tslib "^1.9.0"
@@ -2359,6 +2359,13 @@
dependencies:
tslib "^1.9.0"
+"@ngxs/router-plugin@^3.7.0":
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/@ngxs/router-plugin/-/router-plugin-3.7.0.tgz#9d0595d8ec12e6143eb9f2f726fcc7cdec8cd635"
+ integrity sha512-nmArryNIBLWqIKLWelOQhgxEC3evLPDNDkE+PEYvwoASC4NP5rHIqJv0borJAWBCwo10t5wPeT1417vBQex5aQ==
+ dependencies:
+ tslib "^1.9.0"
+
"@ngxs/storage-plugin@^3.6.2":
version "3.6.2"
resolved "https://registry.yarnpkg.com/@ngxs/storage-plugin/-/storage-plugin-3.6.2.tgz#6fe2168891382c635406df02308f67b585efc60a"
@@ -2366,6 +2373,13 @@
dependencies:
tslib "^1.9.0"
+"@ngxs/storage-plugin@^3.7.0":
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/@ngxs/storage-plugin/-/storage-plugin-3.7.0.tgz#3a0bc75f5dbba17a18e37b69cfde5d470cfceb45"
+ integrity sha512-j5rGYfhi1S+sAky956DIs+6AYP9FMBWL2Uz+omKBS/i42mjhyD26UfApORjwCOyW5PCb4Tq3B14ZPxyAaSi/OA==
+ dependencies:
+ tslib "^1.9.0"
+
"@ngxs/store@^3.6.2":
version "3.6.2"
resolved "https://registry.yarnpkg.com/@ngxs/store/-/store-3.6.2.tgz#cfba63dc1e5bd422e89e54b3332cd69818510624"
@@ -2373,6 +2387,13 @@
dependencies:
tslib "^1.9.0"
+"@ngxs/store@^3.7.0":
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/@ngxs/store/-/store-3.7.0.tgz#e46387219dae610c685accc119ae42e351afcaa4"
+ integrity sha512-w9fG/DhKBgH1VJMKSoeNW9x9ycD9/Dzy+VkpFD8Jv0JBNX0MRgP+5KQQe3ZKwnJ+7S0UV/99JvJaWgxc/WOvPw==
+ dependencies:
+ tslib "^1.9.0"
+
"@nodelib/fs.scandir@2.1.3":
version "2.1.3"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b"