diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 395f88351a..366972c165 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-dotnet@master
with:
- dotnet-version: 3.1.100
+ dotnet-version: 3.1.102
- name: Build All
run: .\build-all.ps1
diff --git a/docs/en/Index.md b/docs/en/Index.md
index 5fe44625ad..949c04eb24 100644
--- a/docs/en/Index.md
+++ b/docs/en/Index.md
@@ -6,9 +6,21 @@ Explore the navigation menu to deep dive in the documentation.
## Getting Started
-The easiest way to start a new web application with the ABP Framework is to use the [getting started](Getting-Started.md) tutorial.
+The easiest way to start a new web application with the ABP Framework is to use the [getting started](Getting-Started.md) guide.
-Then you can continue with the [web application development tutorial](Tutorials/Part-1.md).
+## Tutorials / Articles
+
+### Web Application Development
+
+[Web application development tutorial](Tutorials/Part-1.md) is a complete tutorial to develop a full stack application using the ABP Framework.
+
+### ABP Community Articles
+
+See also the [ABP Community](https://community.abp.io/) articles.
+
+## Samples
+
+See the [sample projects](Samples/Index.md) built with the ABP Framework.
## Source Code
diff --git a/docs/en/UI/Angular/Component-Replacement.md b/docs/en/UI/Angular/Component-Replacement.md
index fd34fcbae7..d79a67829c 100644
--- a/docs/en/UI/Angular/Component-Replacement.md
+++ b/docs/en/UI/Angular/Component-Replacement.md
@@ -8,27 +8,22 @@ The reason that you **can replace** but **cannot customize** default ABP compone
Create a new component that you want to use instead of an ABP component. Add that component to `declarations` and `entryComponents` in the `AppModule`.
-Then, open the `app.component.ts` and dispatch the `AddReplaceableComponent` action to replace your component with an ABP component as shown below:
+Then, open the `app.component.ts` and execute the `add` method of `ReplaceableComponentsService` to replace your component with an ABP component as shown below:
```js
-import { AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent action
+import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum
-import { Store } from '@ngxs/store'; // imported Store
//...
@Component(/* component metadata */)
export class AppComponent {
constructor(
- private store: Store // injected Store
- )
- {
- // dispatched the AddReplaceableComponent action
- this.store.dispatch(
- new AddReplaceableComponent({
- component: YourNewRoleComponent,
- key: eIdentityComponents.Roles,
- }),
- );
+ private replaceableComponents: ReplaceableComponentsService, // injected the service
+ ) {
+ this.replaceableComponents.add({
+ component: YourNewRoleComponent,
+ key: eIdentityComponents.Roles,
+ });
}
}
```
@@ -59,28 +54,24 @@ Add the following code in your layout template (`my-layout.component.html`) wher
Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js
-import { AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent
+import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents enum for component keys
-import { Store } from '@ngxs/store'; // imported Store
import { MyApplicationLayoutComponent } from './my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent
@Component(/* component metadata */)
export class AppComponent {
constructor(
- private store: Store, // injected Store
+ private replaceableComponents: ReplaceableComponentsService, // injected the service
) {
- // dispatched the AddReplaceableComponent action
- this.store.dispatch(
- new AddReplaceableComponent({
- component: MyApplicationLayoutComponent,
- key: eThemeBasicComponents.ApplicationLayout,
- }),
- );
+ this.replaceableComponents.add({
+ component: MyApplicationLayoutComponent,
+ key: eThemeBasicComponents.ApplicationLayout,
+ });
}
}
```
-> If you like to replace a layout component at runtime (e.g: changing the layout by pressing a button), pass the second parameter of the AddReplaceableComponent action as true. DynamicLayoutComponent loads content using a router-outlet. When the second parameter of AddReplaceableComponent is true, the route will be refreshed, so use it with caution. Your component state will be gone and any initiation logic (including HTTP requests) will be repeated.
+> If you like to replace a layout component at runtime (e.g: changing the layout by pressing a button), pass the second parameter of the `add` method of `ReplaceableComponentsService` as true. DynamicLayoutComponent loads content using a router-outlet. When the second parameter of the `add` method is true, the route will be refreshed, so use it with caution. Your component state will be gone and any initiation logic (including HTTP requests) will be repeated.
### Layout Components
@@ -123,26 +114,22 @@ export class LogoComponent {}
Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js
-import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent
-import { Store } from '@ngxs/store'; // imported Store
+import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { LogoComponent } from './logo/logo.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//...
@Component(/* component metadata */)
export class AppComponent implements OnInit {
- constructor(..., private store: Store) {} // injected Store
+ constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() {
//...
- // added dispatch
- this.store.dispatch(
- new AddReplaceableComponent({
+ this.replaceableComponents.add({
component: LogoComponent,
key: eThemeBasicComponents.Logo,
- }),
- );
+ });
}
}
```
@@ -166,7 +153,7 @@ yarn ng generate component routes --entryComponent
Open the generated `routes.component.ts` in `src/app/routes` folder and replace its content with the following:
```js
-import { ABP, ReplaceableComponents } from '@abp/ng.core';
+import { ABP } from '@abp/ng.core';
import {
Component,
HostBinding,
@@ -309,26 +296,22 @@ Open the generated `routes.component.html` in `src/app/routes` folder and replac
Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js
-import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent
-import { Store } from '@ngxs/store'; // imported Store
+import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { RoutesComponent } from './routes/routes.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//...
@Component(/* component metadata */)
export class AppComponent implements OnInit {
- constructor(..., private store: Store) {} // injected Store
+ constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() {
//...
- // added dispatch
- this.store.dispatch(
- new AddReplaceableComponent({
+ this.replaceableComponents.add({
component: RoutesComponent,
key: eThemeBasicComponents.Routes,
- }),
- );
+ });
}
}
```
@@ -511,26 +494,22 @@ Open the generated `nav-items.component.html` in `src/app/nav-items` folder and
Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js
-import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent
-import { Store } from '@ngxs/store'; // imported Store
+import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { NavItemsComponent } from './nav-items/nav-items.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//...
@Component(/* component metadata */)
export class AppComponent implements OnInit {
- constructor(..., private store: Store) {} // injected Store
+ constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() {
//...
- // added dispatch
- this.store.dispatch(
- new AddReplaceableComponent({
+ this.replaceableComponents.add({
component: NavItemsComponent,
key: eThemeBasicComponents.NavItems,
- }),
- );
+ });
}
}
```
diff --git a/docs/en/UI/Angular/Permission-Management-Component-Replacement.md b/docs/en/UI/Angular/Permission-Management-Component-Replacement.md
index acdfffb948..9ae712cedd 100644
--- a/docs/en/UI/Angular/Permission-Management-Component-Replacement.md
+++ b/docs/en/UI/Angular/Permission-Management-Component-Replacement.md
@@ -473,24 +473,20 @@ Open the generated `permission-management.component.html` in `src/app/permission
Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js
-import { AddReplaceableComponent } from '@abp/ng.core';
+import { ReplaceableComponentsService } from '@abp/ng.core';
import { ePermissionManagementComponents } from '@abp/ng.permission-management';
import { Component, OnInit } from '@angular/core';
-import { Store } from '@ngxs/store';
import { PermissionManagementComponent } from './permission-management/permission-management.component';
//...
export class AppComponent implements OnInit {
- constructor(private store: Store) {} // injected store
+ constructor(private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() {
- // added dispatching the AddReplaceableComponent action
- this.store.dispatch(
- new AddReplaceableComponent({
+ this.replaceableComponents.add({
component: PermissionManagementComponent,
key: ePermissionManagementComponents.PermissionManagement,
- })
- );
+ });
}
}
```
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.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/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/index.ts b/npm/ng-packs/packages/core/src/lib/utils/index.ts
index 383261fc07..429a59d0b1 100644
--- a/npm/ng-packs/packages/core/src/lib/utils/index.ts
+++ b/npm/ng-packs/packages/core/src/lib/utils/index.ts
@@ -6,6 +6,7 @@ export * from './factory-utils';
export * from './form-utils';
export * from './generator-utils';
export * from './initial-utils';
+export * from './internal-store-utils';
export * from './lazy-load-utils';
export * from './localization-utils';
export * from './multi-tenancy-utils';
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/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"