Browse Source

Merge branch 'dev' into blazor-ui

pull/5399/head
Halil İbrahim Kalkan 6 years ago
parent
commit
afe50100f8
  1. 2
      .github/workflows/build-and-test.yml
  2. 16
      docs/en/Index.md
  3. 77
      docs/en/UI/Angular/Component-Replacement.md
  4. 12
      docs/en/UI/Angular/Permission-Management-Component-Replacement.md
  5. 10
      framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentUserDto.cs
  6. 8
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/TagHelpers/AbpComponentDemoSectionTagHelper.cs
  7. 5
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs
  8. 30
      framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinition.cs
  9. 11
      framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/AbpClaimTypes.cs
  10. 6
      framework/src/Volo.Abp.Security/Volo/Abp/Users/CurrentUser.cs
  11. 8
      framework/src/Volo.Abp.Security/Volo/Abp/Users/ICurrentUser.cs
  12. 2
      framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/DataFiltering/HardDelete_Tests.cs
  13. 6
      global.json
  14. 16
      modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeatureManagementPermissions.cs
  15. 28
      modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeaturePermissionDefinitionProvider.cs
  16. 4
      modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/IFeatureAppService.cs
  17. 33
      modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs
  18. 6
      modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/en.json
  19. 6
      modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/tr.json
  20. 6
      modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/zh-Hans.json
  21. 6
      modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/zh-Hant.json
  22. 2
      modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs
  23. 2
      modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/FeaturesController.cs
  24. 3
      modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml.cs
  25. 14
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityDomainModule.cs
  26. 38
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpUserClaimsPrincipalFactory.cs
  27. 21
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/ClaimsIdentityExtensions.cs
  28. 2
      modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/OrganizationUnitRepository_Tests.cs
  29. 7
      modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en.json
  30. 3
      modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/tr.json
  31. 3
      modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/zh-Hans.json
  32. 3
      modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/zh-Hant.json
  33. 9
      modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.cshtml
  34. 9
      modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.js
  35. 10
      npm/ng-packs/package.json
  36. 6
      npm/ng-packs/packages/core/package.json
  37. 3
      npm/ng-packs/packages/core/src/lib/actions/replaceable-components.actions.ts
  38. 9
      npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts
  39. 9
      npm/ng-packs/packages/core/src/lib/components/replaceable-route-container.component.ts
  40. 9
      npm/ng-packs/packages/core/src/lib/directives/replaceable-template.directive.ts
  41. 9
      npm/ng-packs/packages/core/src/lib/models/utility.ts
  42. 1
      npm/ng-packs/packages/core/src/lib/services/index.ts
  43. 57
      npm/ng-packs/packages/core/src/lib/services/replaceable-components.service.ts
  44. 42
      npm/ng-packs/packages/core/src/lib/states/replaceable-components.state.ts
  45. 63
      npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts
  46. 108
      npm/ng-packs/packages/core/src/lib/tests/internal-store.spec.ts
  47. 21
      npm/ng-packs/packages/core/src/lib/tests/replaceable-route-container.component.spec.ts
  48. 23
      npm/ng-packs/packages/core/src/lib/tests/replaceable-template.directive.spec.ts
  49. 20
      npm/ng-packs/packages/core/src/lib/utils/file-utils.ts
  50. 2
      npm/ng-packs/packages/core/src/lib/utils/index.ts
  51. 36
      npm/ng-packs/packages/core/src/lib/utils/internal-store-utils.ts
  52. 17
      npm/ng-packs/packages/core/src/lib/utils/route-utils.ts
  53. 39
      npm/ng-packs/packages/theme-basic/src/lib/providers/styles.provider.ts
  54. 5
      npm/ng-packs/packages/theme-shared/src/lib/constants/styles.ts
  55. 67
      npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts
  56. 37
      npm/ng-packs/yarn.lock

2
.github/workflows/build-and-test.yml

@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-dotnet@master - uses: actions/setup-dotnet@master
with: with:
dotnet-version: 3.1.100 dotnet-version: 3.1.102
- name: Build All - name: Build All
run: .\build-all.ps1 run: .\build-all.ps1

16
docs/en/Index.md

@ -6,9 +6,21 @@ Explore the navigation menu to deep dive in the documentation.
## Getting Started ## 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 ## Source Code

77
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`. 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 ```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 { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum
import { Store } from '@ngxs/store'; // imported Store
//... //...
@Component(/* component metadata */) @Component(/* component metadata */)
export class AppComponent { export class AppComponent {
constructor( constructor(
private store: Store // injected Store private replaceableComponents: ReplaceableComponentsService, // injected the service
) ) {
{ this.replaceableComponents.add({
// dispatched the AddReplaceableComponent action component: YourNewRoleComponent,
this.store.dispatch( key: eIdentityComponents.Roles,
new AddReplaceableComponent({ });
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: Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js ```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 { 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 import { MyApplicationLayoutComponent } from './my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent
@Component(/* component metadata */) @Component(/* component metadata */)
export class AppComponent { export class AppComponent {
constructor( constructor(
private store: Store, // injected Store private replaceableComponents: ReplaceableComponentsService, // injected the service
) { ) {
// dispatched the AddReplaceableComponent action this.replaceableComponents.add({
this.store.dispatch( component: MyApplicationLayoutComponent,
new AddReplaceableComponent({ key: eThemeBasicComponents.ApplicationLayout,
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 ### Layout Components
@ -123,26 +114,22 @@ export class LogoComponent {}
Open `app.component.ts` in `src/app` folder and modify it as shown below: Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js ```js
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { Store } from '@ngxs/store'; // imported Store
import { LogoComponent } from './logo/logo.component'; // imported NavItemsComponent import { LogoComponent } from './logo/logo.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//... //...
@Component(/* component metadata */) @Component(/* component metadata */)
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
constructor(..., private store: Store) {} // injected Store constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() { ngOnInit() {
//... //...
// added dispatch this.replaceableComponents.add({
this.store.dispatch(
new AddReplaceableComponent({
component: LogoComponent, component: LogoComponent,
key: eThemeBasicComponents.Logo, 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: Open the generated `routes.component.ts` in `src/app/routes` folder and replace its content with the following:
```js ```js
import { ABP, ReplaceableComponents } from '@abp/ng.core'; import { ABP } from '@abp/ng.core';
import { import {
Component, Component,
HostBinding, 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: Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js ```js
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { Store } from '@ngxs/store'; // imported Store
import { RoutesComponent } from './routes/routes.component'; // imported NavItemsComponent import { RoutesComponent } from './routes/routes.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//... //...
@Component(/* component metadata */) @Component(/* component metadata */)
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
constructor(..., private store: Store) {} // injected Store constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() { ngOnInit() {
//... //...
// added dispatch this.replaceableComponents.add({
this.store.dispatch(
new AddReplaceableComponent({
component: RoutesComponent, component: RoutesComponent,
key: eThemeBasicComponents.Routes, 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: Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js ```js
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { Store } from '@ngxs/store'; // imported Store
import { NavItemsComponent } from './nav-items/nav-items.component'; // imported NavItemsComponent import { NavItemsComponent } from './nav-items/nav-items.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//... //...
@Component(/* component metadata */) @Component(/* component metadata */)
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
constructor(..., private store: Store) {} // injected Store constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() { ngOnInit() {
//... //...
// added dispatch this.replaceableComponents.add({
this.store.dispatch(
new AddReplaceableComponent({
component: NavItemsComponent, component: NavItemsComponent,
key: eThemeBasicComponents.NavItems, key: eThemeBasicComponents.NavItems,
}), });
);
} }
} }
``` ```

12
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: Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js ```js
import { AddReplaceableComponent } from '@abp/ng.core'; import { ReplaceableComponentsService } from '@abp/ng.core';
import { ePermissionManagementComponents } from '@abp/ng.permission-management'; import { ePermissionManagementComponents } from '@abp/ng.permission-management';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Store } from '@ngxs/store';
import { PermissionManagementComponent } from './permission-management/permission-management.component'; import { PermissionManagementComponent } from './permission-management/permission-management.component';
//... //...
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
constructor(private store: Store) {} // injected store constructor(private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() { ngOnInit() {
// added dispatching the AddReplaceableComponent action this.replaceableComponents.add({
this.store.dispatch(
new AddReplaceableComponent({
component: PermissionManagementComponent, component: PermissionManagementComponent,
key: ePermissionManagementComponents.PermissionManagement, key: ePermissionManagementComponents.PermissionManagement,
}) });
);
} }
} }
``` ```

10
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 UserName { get; set; }
public string Name { get; set; }
public string SurName { get; set; }
public string Email { 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; } public string[] Roles { get; set; }
} }
} }

8
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/TagHelpers/AbpComponentDemoSectionTagHelper.cs

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

5
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, Id = _currentUser.Id,
TenantId = _currentUser.TenantId, TenantId = _currentUser.TenantId,
UserName = _currentUser.UserName, UserName = _currentUser.UserName,
SurName = _currentUser.SurName,
Name = _currentUser.Name,
Email = _currentUser.Email, Email = _currentUser.Email,
EmailVerified = _currentUser.EmailVerified,
PhoneNumber = _currentUser.PhoneNumber,
PhoneNumberVerified = _currentUser.PhoneNumberVerified,
Roles = _currentUser.Roles Roles = _currentUser.Roles
}; };
} }

30
framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinition.cs

@ -51,6 +51,12 @@ namespace Volo.Abp.Features
/// </summary> /// </summary>
public bool IsVisibleToClients { get; set; } public bool IsVisibleToClients { get; set; }
/// <summary>
/// Can host use this feature.
/// Default: true.
/// </summary>
public bool IsAvailableToHost { get; set; }
/// <summary> /// <summary>
/// A list of allowed providers to get/set value of this feature. /// A list of allowed providers to get/set value of this feature.
/// An empty list indicates that all providers are allowed. /// An empty list indicates that all providers are allowed.
@ -93,7 +99,8 @@ namespace Volo.Abp.Features
ILocalizableString displayName = null, ILocalizableString displayName = null,
ILocalizableString description = null, ILocalizableString description = null,
IStringValueType valueType = null, IStringValueType valueType = null,
bool isVisibleToClients = true) bool isVisibleToClients = true,
bool isAvailableToHost = true)
{ {
Name = name; Name = name;
DefaultValue = defaultValue; DefaultValue = defaultValue;
@ -101,6 +108,7 @@ namespace Volo.Abp.Features
Description = description; Description = description;
ValueType = valueType; ValueType = valueType;
IsVisibleToClients = isVisibleToClients; IsVisibleToClients = isVisibleToClients;
IsAvailableToHost = isAvailableToHost;
Properties = new Dictionary<string, object>(); Properties = new Dictionary<string, object>();
AllowedProviders = new List<string>(); AllowedProviders = new List<string>();
@ -136,20 +144,22 @@ namespace Volo.Abp.Features
/// </summary> /// </summary>
/// <returns>Returns a newly created child feature</returns> /// <returns>Returns a newly created child feature</returns>
public FeatureDefinition CreateChild( public FeatureDefinition CreateChild(
string name, string name,
string defaultValue = null, string defaultValue = null,
ILocalizableString displayName = null, ILocalizableString displayName = null,
ILocalizableString description = null, ILocalizableString description = null,
IStringValueType valueType = null, IStringValueType valueType = null,
bool isVisibleToClients = true) bool isVisibleToClients = true,
bool isAvailableToHost = true)
{ {
var feature = new FeatureDefinition( var feature = new FeatureDefinition(
name, name,
defaultValue, defaultValue,
displayName, displayName,
description, description,
valueType, valueType,
isVisibleToClients) isVisibleToClients,
isAvailableToHost)
{ {
Parent = this Parent = this
}; };
@ -175,4 +185,4 @@ namespace Volo.Abp.Features
return $"[{nameof(FeatureDefinition)}: {Name}]"; return $"[{nameof(FeatureDefinition)}: {Name}]";
} }
} }
} }

11
framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/AbpClaimTypes.cs

@ -13,6 +13,16 @@ namespace Volo.Abp.Security.Claims
/// </summary> /// </summary>
public static string UserName { get; set; } = ClaimTypes.Name; public static string UserName { get; set; } = ClaimTypes.Name;
/// <summary>
/// Default: <see cref="ClaimTypes.GivenName"/>
/// </summary>
public static string Name { get; set; } = ClaimTypes.GivenName;
/// <summary>
/// Default: <see cref="ClaimTypes.Surname"/>
/// </summary>
public static string SurName { get; set; } = ClaimTypes.Surname;
/// <summary> /// <summary>
/// Default: <see cref="ClaimTypes.NameIdentifier"/> /// Default: <see cref="ClaimTypes.NameIdentifier"/>
/// </summary> /// </summary>
@ -48,7 +58,6 @@ namespace Volo.Abp.Security.Claims
/// </summary> /// </summary>
public static string TenantId { get; set; } = "tenantid"; public static string TenantId { get; set; } = "tenantid";
/// <summary> /// <summary>
/// Default: "editionid". /// Default: "editionid".
/// </summary> /// </summary>

6
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 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 string PhoneNumber => this.FindClaimValue(AbpClaimTypes.PhoneNumber);
public virtual bool PhoneNumberVerified => string.Equals(this.FindClaimValue(AbpClaimTypes.PhoneNumberVerified), "true", StringComparison.InvariantCultureIgnoreCase); 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); return FindClaims(AbpClaimTypes.Role).Any(c => c.Value == roleName);
} }
} }
} }

8
framework/src/Volo.Abp.Security/Volo/Abp/Users/ICurrentUser.cs

@ -14,9 +14,15 @@ namespace Volo.Abp.Users
[CanBeNull] [CanBeNull]
string UserName { get; } string UserName { get; }
[CanBeNull]
string Name { get; }
[CanBeNull]
string SurName { get; }
[CanBeNull] [CanBeNull]
string PhoneNumber { get; } string PhoneNumber { get; }
bool PhoneNumberVerified { get; } bool PhoneNumberVerified { get; }
[CanBeNull] [CanBeNull]

2
framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/DataFiltering/HardDelete_Tests.cs

@ -2,9 +2,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Volo.Abp.TestApp.Testing; using Volo.Abp.TestApp.Testing;
using Xunit;
namespace Volo.Abp.MongoDB.DataFiltering namespace Volo.Abp.MongoDB.DataFiltering
{ {
[Collection(MongoTestCollection.Name)]
public class HardDelete_Tests : HardDelete_Tests<AbpMongoDbTestModule> public class HardDelete_Tests : HardDelete_Tests<AbpMongoDbTestModule>
{ {
} }

6
global.json

@ -0,0 +1,6 @@
{
"sdk": {
"version": "3.1.102",
"rollForward": "latestFeature"
}
}

16
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));
}
}
}

28
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<AbpFeatureManagementResource>(name);
}
}
}

4
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 public interface IFeatureAppService : IApplicationService
{ {
Task<GetFeatureListResultDto> GetAsync([NotNull] string providerName, [NotNull] string providerKey); Task<GetFeatureListResultDto> 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);
} }
} }

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

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -25,9 +26,9 @@ namespace Volo.Abp.FeatureManagement
Options = options.Value; Options = options.Value;
} }
public virtual async Task<GetFeatureListResultDto> GetAsync([NotNull] string providerName, [NotNull] string providerKey) public virtual async Task<GetFeatureListResultDto> GetAsync([NotNull] string providerName, string providerKey)
{ {
await CheckProviderPolicy(providerName); await CheckProviderPolicy(providerName, providerKey);
var result = new GetFeatureListResultDto var result = new GetFeatureListResultDto
{ {
@ -45,6 +46,14 @@ namespace Volo.Abp.FeatureManagement
foreach (var featureDefinition in group.GetFeaturesWithChildren()) 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); var feature = await FeatureManager.GetOrNullWithProviderAsync(featureDefinition.Name, providerName, providerKey);
groupDto.Features.Add(new FeatureDto groupDto.Features.Add(new FeatureDto
{ {
@ -70,9 +79,9 @@ namespace Volo.Abp.FeatureManagement
return result; 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) 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); string policyName;
if (policyName.IsNullOrEmpty()) 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); await AuthorizationService.CheckAsync(policyName);

6
modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/en.json

@ -2,6 +2,8 @@
"culture": "en", "culture": "en",
"texts": { "texts": {
"Features": "Features", "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"
} }
} }

6
modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/tr.json

@ -2,6 +2,8 @@
"culture": "tr", "culture": "tr",
"texts": { "texts": {
"Features": "Özellikler", "Features": "Özellikler",
"NoFeatureFoundMessage": "Hiç özellik yok." "NoFeatureFoundMessage": "Hiç özellik yok.",
"Permission:FeatureManagement": "Özellik yönetimi",
"Permission:FeatureManagement.ManageHostFeatures": "Host özelliklerini düzenle"
} }
} }

6
modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/zh-Hans.json

@ -2,6 +2,8 @@
"culture": "zh-Hans", "culture": "zh-Hans",
"texts": { "texts": {
"Features": "功能", "Features": "功能",
"NoFeatureFoundMessage": "没有可用的功能." "NoFeatureFoundMessage": "没有可用的功能.",
"Permission:FeatureManagement": "特性管理",
"Permission:FeatureManagement.ManageHostFeatures": "管理Host特性"
} }
} }

6
modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/Localization/Domain/zh-Hant.json

@ -2,6 +2,8 @@
"culture": "zh-Hant", "culture": "zh-Hant",
"texts": { "texts": {
"Features": "功能", "Features": "功能",
"NoFeatureFoundMessage": "沒有可用的功能." "NoFeatureFoundMessage": "沒有可用的功能.",
"Permission:FeatureManagement": "功能管理",
"Permission:FeatureManagement.ManageHostFeatures": "管理Host功能"
} }
} }

2
modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs

@ -1,10 +1,8 @@
using Volo.Abp.Caching; using Volo.Abp.Caching;
using Volo.Abp.FeatureManagement.Localization; using Volo.Abp.FeatureManagement.Localization;
using Volo.Abp.Features; using Volo.Abp.Features;
using Volo.Abp.Localization;
using Volo.Abp.Localization.ExceptionHandling; using Volo.Abp.Localization.ExceptionHandling;
using Volo.Abp.Modularity; using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
namespace Volo.Abp.FeatureManagement namespace Volo.Abp.FeatureManagement
{ {

2
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); return FeatureAppService.UpdateAsync(providerName, providerKey, input);
} }
} }
} }

3
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)] [BindProperty(SupportsGet = true)]
public string ProviderName { get; set; } public string ProviderName { get; set; }
[Required]
[HiddenInput] [HiddenInput]
[BindProperty(SupportsGet = true)] [BindProperty(SupportsGet = true)]
public string ProviderKey { get; set; } public string ProviderKey { get; set; }
@ -83,8 +82,6 @@ namespace Volo.Abp.FeatureManagement.Web.Pages.FeatureManagement
public string Value { get; set; } public string Value { get; set; }
public string ProviderName { get; set; }
public bool BoolValue { get; set; } public bool BoolValue { get; set; }
public string Type { get; set; } public string Type { get; set; }

14
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.Modularity;
using Volo.Abp.ObjectExtending; using Volo.Abp.ObjectExtending;
using Volo.Abp.ObjectExtending.Modularity; using Volo.Abp.ObjectExtending.Modularity;
using Volo.Abp.Security.Claims;
using Volo.Abp.Users; using Volo.Abp.Users;
namespace Volo.Abp.Identity namespace Volo.Abp.Identity
@ -36,7 +37,7 @@ namespace Volo.Abp.Identity
options.EtoMappings.Add<IdentityRole, IdentityRoleEto>(typeof(AbpIdentityDomainModule)); options.EtoMappings.Add<IdentityRole, IdentityRoleEto>(typeof(AbpIdentityDomainModule));
options.EtoMappings.Add<OrganizationUnit, OrganizationUnitEto>(typeof(AbpIdentityDomainModule)); options.EtoMappings.Add<OrganizationUnit, OrganizationUnitEto>(typeof(AbpIdentityDomainModule));
}); });
var identityBuilder = context.Services.AddAbpIdentity(options => var identityBuilder = context.Services.AddAbpIdentity(options =>
{ {
options.User.RequireUniqueEmail = true; options.User.RequireUniqueEmail = true;
@ -45,6 +46,13 @@ namespace Volo.Abp.Identity
context.Services.AddObjectAccessor(identityBuilder); context.Services.AddObjectAccessor(identityBuilder);
context.Services.ExecutePreConfiguredActions(identityBuilder); context.Services.ExecutePreConfiguredActions(identityBuilder);
Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserIdClaimType = AbpClaimTypes.UserId;
options.ClaimsIdentity.UserNameClaimType = AbpClaimTypes.UserName;
options.ClaimsIdentity.RoleClaimType = AbpClaimTypes.Role;
});
AddAbpIdentityOptionsFactory(context.Services); AddAbpIdentityOptionsFactory(context.Services);
} }
@ -67,7 +75,7 @@ namespace Volo.Abp.Identity
IdentityModuleExtensionConsts.EntityNames.ClaimType, IdentityModuleExtensionConsts.EntityNames.ClaimType,
typeof(IdentityClaimType) typeof(IdentityClaimType)
); );
ModuleExtensionConfigurationHelper.ApplyEntityConfigurationToEntity( ModuleExtensionConfigurationHelper.ApplyEntityConfigurationToEntity(
IdentityModuleExtensionConsts.ModuleName, IdentityModuleExtensionConsts.ModuleName,
IdentityModuleExtensionConsts.EntityNames.OrganizationUnit, IdentityModuleExtensionConsts.EntityNames.OrganizationUnit,
@ -81,4 +89,4 @@ namespace Volo.Abp.Identity
services.Replace(ServiceDescriptor.Scoped<IOptions<IdentityOptions>, OptionsManager<IdentityOptions>>()); services.Replace(ServiceDescriptor.Scoped<IOptions<IdentityOptions>, OptionsManager<IdentityOptions>>());
} }
} }
} }

38
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.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
@ -6,6 +7,7 @@ using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Security.Claims; using Volo.Abp.Security.Claims;
using Volo.Abp.Uow; using Volo.Abp.Uow;
using Volo.Abp.Users;
namespace Volo.Abp.Identity namespace Volo.Abp.Identity
{ {
@ -13,11 +15,11 @@ namespace Volo.Abp.Identity
{ {
public AbpUserClaimsPrincipalFactory( public AbpUserClaimsPrincipalFactory(
UserManager<IdentityUser> userManager, UserManager<IdentityUser> userManager,
RoleManager<IdentityRole> roleManager, RoleManager<IdentityRole> roleManager,
IOptions<IdentityOptions> options) IOptions<IdentityOptions> options)
: base( : base(
userManager, userManager,
roleManager, roleManager,
options) options)
{ {
} }
@ -26,14 +28,34 @@ namespace Volo.Abp.Identity
public override async Task<ClaimsPrincipal> CreateAsync(IdentityUser user) public override async Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
{ {
var principal = await base.CreateAsync(user); var principal = await base.CreateAsync(user);
var identity = principal.Identities.First();
if (user.TenantId.HasValue) if (user.TenantId.HasValue)
{ {
principal.Identities identity.AddIfNotContains(new Claim(AbpClaimTypes.TenantId, user.TenantId.ToString()));
.First()
.AddClaim(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; return principal;
} }
} }

21
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;
}
}
}

2
modules/identity/test/Volo.Abp.Identity.MongoDB.Tests/Volo/Abp/Identity/MongoDB/OrganizationUnitRepository_Tests.cs

@ -1,9 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Xunit;
namespace Volo.Abp.Identity.MongoDB namespace Volo.Abp.Identity.MongoDB
{ {
[Collection(MongoTestCollection.Name)]
public class OrganizationUnitRepository_Tests : OrganizationUnitRepository_Tests<AbpIdentityMongoDbTestModule> public class OrganizationUnitRepository_Tests : OrganizationUnitRepository_Tests<AbpIdentityMongoDbTestModule>
{ {
} }

7
modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en.json

@ -10,13 +10,14 @@
"ConnectionStrings": "Connection Strings", "ConnectionStrings": "Connection Strings",
"DisplayName:DefaultConnectionString": "Default Connection String", "DisplayName:DefaultConnectionString": "Default Connection String",
"DisplayName:UseSharedDatabase": "Use the Shared Database", "DisplayName:UseSharedDatabase": "Use the Shared Database",
"ManageHostFeatures": "Manage Host features",
"Permission:TenantManagement": "Tenant management", "Permission:TenantManagement": "Tenant management",
"Permission:Create": "Create", "Permission:Create": "Create",
"Permission:Edit": "Edit", "Permission:Edit": "Edit",
"Permission:Delete": "Delete", "Permission:Delete": "Delete",
"Permission:ManageConnectionStrings": "Manage connection strings", "Permission:ManageConnectionStrings": "Manage connection strings",
"Permission:ManageFeatures": "Manage features", "Permission:ManageFeatures": "Manage features",
"DisplayName:AdminEmailAddress": "Admin Email Address", "DisplayName:AdminEmailAddress": "Admin Email Address",
"DisplayName:AdminPassword": "Admin Password" "DisplayName:AdminPassword": "Admin Password"
} }
} }

3
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", "ConnectionStrings": "Bağlantı cümlesi",
"DisplayName:DefaultConnectionString": "Varsayılan bağlantı cümlesi", "DisplayName:DefaultConnectionString": "Varsayılan bağlantı cümlesi",
"DisplayName:UseSharedDatabase": "Paylaşılan veritabanını kullan", "DisplayName:UseSharedDatabase": "Paylaşılan veritabanını kullan",
"ManageHostFeatures": "Toplantı Sahibi özelliklerini yönetin",
"Permission:TenantManagement": "Müşteri yönetimi", "Permission:TenantManagement": "Müşteri yönetimi",
"Permission:Create": "Oluşturma", "Permission:Create": "Oluşturma",
"Permission:Edit": "Düzenleme", "Permission:Edit": "Düzenleme",
@ -19,4 +20,4 @@
"DisplayName:AdminEmailAddress": "Admin Eposta Adresi", "DisplayName:AdminEmailAddress": "Admin Eposta Adresi",
"DisplayName:AdminPassword": "Admin Şifresi" "DisplayName:AdminPassword": "Admin Şifresi"
} }
} }

3
modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/zh-Hans.json

@ -10,6 +10,7 @@
"ConnectionStrings": "连接字符串", "ConnectionStrings": "连接字符串",
"DisplayName:DefaultConnectionString": "默认连接字符串", "DisplayName:DefaultConnectionString": "默认连接字符串",
"DisplayName:UseSharedDatabase": "使用共享数据库", "DisplayName:UseSharedDatabase": "使用共享数据库",
"ManageHostFeatures": "管理Host特性",
"Permission:TenantManagement": "租户管理", "Permission:TenantManagement": "租户管理",
"Permission:Create": "创建", "Permission:Create": "创建",
"Permission:Edit": "编辑", "Permission:Edit": "编辑",
@ -17,4 +18,4 @@
"Permission:ManageConnectionStrings": "管理连接字符串", "Permission:ManageConnectionStrings": "管理连接字符串",
"Permission:ManageFeatures": "管理功能" "Permission:ManageFeatures": "管理功能"
} }
} }

3
modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/zh-Hant.json

@ -10,13 +10,14 @@
"ConnectionStrings": "資料庫連線字串", "ConnectionStrings": "資料庫連線字串",
"DisplayName:DefaultConnectionString": "預設資料庫連線字串", "DisplayName:DefaultConnectionString": "預設資料庫連線字串",
"DisplayName:UseSharedDatabase": "使用共用資料庫", "DisplayName:UseSharedDatabase": "使用共用資料庫",
"ManageHostFeatures": "管理Host功能",
"Permission:TenantManagement": "租戶管理", "Permission:TenantManagement": "租戶管理",
"Permission:Create": "新增", "Permission:Create": "新增",
"Permission:Edit": "編輯", "Permission:Edit": "編輯",
"Permission:Delete": "刪除", "Permission:Delete": "刪除",
"Permission:ManageConnectionStrings": "管理資料庫連線字串", "Permission:ManageConnectionStrings": "管理資料庫連線字串",
"Permission:ManageFeatures": "管理功能", "Permission:ManageFeatures": "管理功能",
"DisplayName:AdminEmailAddress": "管理者信箱", "DisplayName:AdminEmailAddress": "管理者信箱",
"DisplayName:AdminPassword": "管理者密碼" "DisplayName:AdminPassword": "管理者密碼"
} }
} }

9
modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.cshtml

@ -2,6 +2,7 @@
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Mvc.Localization @using Microsoft.AspNetCore.Mvc.Localization
@using Volo.Abp.AspNetCore.Mvc.UI.Layout @using Volo.Abp.AspNetCore.Mvc.UI.Layout
@using Volo.Abp.FeatureManagement
@using Volo.Abp.TenantManagement @using Volo.Abp.TenantManagement
@using Volo.Abp.TenantManagement.Localization @using Volo.Abp.TenantManagement.Localization
@using Volo.Abp.TenantManagement.Web.Navigation @using Volo.Abp.TenantManagement.Web.Navigation
@ -29,9 +30,13 @@
<abp-card-title>@L["Tenants"]</abp-card-title> <abp-card-title>@L["Tenants"]</abp-card-title>
</abp-column> </abp-column>
<abp-column size-md="_6" class="text-right"> <abp-column size-md="_6" class="text-right">
@if (await Authorization.IsGrantedAsync(FeatureManagementPermissions.ManageHostFeatures))
{
<abp-button button-type="Primary" name="ManageHostFeatures" icon="cog" text="@L["ManageHostFeatures"].Value" />
}
@if (await Authorization.IsGrantedAsync(TenantManagementPermissions.Tenants.Create)) @if (await Authorization.IsGrantedAsync(TenantManagementPermissions.Tenants.Create))
{ {
<abp-button button-type="Primary" name="CreateTenant" icon="plus" text="@L["NewTenant"].Value" /> <abp-button button-type="Primary" name="CreateTenant" icon="plus" text="@L["NewTenant"].Value"/>
} }
</abp-column> </abp-column>
</abp-row> </abp-row>
@ -39,4 +44,4 @@
<abp-card-body> <abp-card-body>
<abp-table striped-rows="true" class="nowrap"></abp-table> <abp-table striped-rows="true" class="nowrap"></abp-table>
</abp-card-body> </abp-card-body>
</abp-card> </abp-card>

9
modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.js

@ -100,7 +100,7 @@
}, },
0 //adds as the first contributor 0 //adds as the first contributor
); );
$(function () { $(function () {
var _$wrapper = $('#TenantsWrapper'); var _$wrapper = $('#TenantsWrapper');
@ -128,5 +128,12 @@
e.preventDefault(); e.preventDefault();
_createModal.open(); _createModal.open();
}); });
_$wrapper.find('button[name=ManageHostFeatures]').click(function (e) {
e.preventDefault();
_featuresModal.open({
providerName: 'T'
});
});
}); });
})(); })();

10
npm/ng-packs/package.json

@ -55,11 +55,11 @@
"@ng-bootstrap/ng-bootstrap": "^7.0.0", "@ng-bootstrap/ng-bootstrap": "^7.0.0",
"@ngneat/spectator": "^5.13.0", "@ngneat/spectator": "^5.13.0",
"@ngx-validate/core": "^0.0.11", "@ngx-validate/core": "^0.0.11",
"@ngxs/devtools-plugin": "^3.6.2", "@ngxs/devtools-plugin": "^3.7.0",
"@ngxs/logger-plugin": "^3.6.2", "@ngxs/logger-plugin": "^3.7.0",
"@ngxs/router-plugin": "^3.6.2", "@ngxs/router-plugin": "^3.7.0",
"@ngxs/storage-plugin": "^3.6.2", "@ngxs/storage-plugin": "^3.7.0",
"@ngxs/store": "^3.6.2", "@ngxs/store": "^3.7.0",
"@schematics/angular": "~10.0.5", "@schematics/angular": "~10.0.5",
"@swimlane/ngx-datatable": "^17.1.0", "@swimlane/ngx-datatable": "^17.1.0",
"@types/jest": "^25.2.3", "@types/jest": "^25.2.3",

6
npm/ng-packs/packages/core/package.json

@ -9,9 +9,9 @@
"dependencies": { "dependencies": {
"@abp/utils": "^3.1.0", "@abp/utils": "^3.1.0",
"@angular/localize": "~10.0.10", "@angular/localize": "~10.0.10",
"@ngxs/router-plugin": "^3.6.2", "@ngxs/router-plugin": "^3.7.0",
"@ngxs/storage-plugin": "^3.6.2", "@ngxs/storage-plugin": "^3.7.0",
"@ngxs/store": "^3.6.2", "@ngxs/store": "^3.7.0",
"angular-oauth2-oidc": "^10.0.0", "angular-oauth2-oidc": "^10.0.0",
"just-clone": "^3.1.0", "just-clone": "^3.1.0",
"just-compare": "^1.3.0", "just-compare": "^1.3.0",

3
npm/ng-packs/packages/core/src/lib/actions/replaceable-components.actions.ts

@ -1,7 +1,8 @@
import { ReplaceableComponents } from '../models/replaceable-components'; 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 { export class AddReplaceableComponent {
static readonly type = '[ReplaceableComponents] Add'; static readonly type = '[ReplaceableComponents] Add';

9
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 { Component, Injector, Optional, SkipSelf, Type } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { Store } from '@ngxs/store';
import { eLayoutType } from '../enums/common'; import { eLayoutType } from '../enums/common';
import { ABP } from '../models'; import { ABP } from '../models';
import { ReplaceableComponents } from '../models/replaceable-components'; import { ReplaceableComponents } from '../models/replaceable-components';
import { LocalizationService } from '../services/localization.service'; import { LocalizationService } from '../services/localization.service';
import { ReplaceableComponentsService } from '../services/replaceable-components.service';
import { RoutesService } from '../services/routes.service'; import { RoutesService } from '../services/routes.service';
import { SubscriptionService } from '../services/subscription.service'; import { SubscriptionService } from '../services/subscription.service';
import { ReplaceableComponentsState } from '../states/replaceable-components.state';
import { findRoute, getRoutePath } from '../utils/route-utils'; import { findRoute, getRoutePath } from '../utils/route-utils';
import { TreeNode } from '../utils/tree-utils'; import { TreeNode } from '../utils/tree-utils';
@ -37,7 +36,7 @@ export class DynamicLayoutComponent {
constructor( constructor(
injector: Injector, injector: Injector,
private localizationService: LocalizationService, private localizationService: LocalizationService,
private store: Store, private replaceableComponents: ReplaceableComponentsService,
private subscription: SubscriptionService, private subscription: SubscriptionService,
@Optional() @SkipSelf() dynamicLayoutComponent: DynamicLayoutComponent, @Optional() @SkipSelf() dynamicLayoutComponent: DynamicLayoutComponent,
) { ) {
@ -67,7 +66,7 @@ export class DynamicLayoutComponent {
if (!expectedLayout) expectedLayout = eLayoutType.empty; if (!expectedLayout) expectedLayout = eLayoutType.empty;
const key = this.layouts.get(expectedLayout); 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 { private getComponent(key: string): ReplaceableComponents.ReplaceableComponent {
return this.store.selectSnapshot(ReplaceableComponentsState.getComponent(key)); return this.replaceableComponents.get(key);
} }
} }

9
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 { Component, OnInit, Type } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Store } from '@ngxs/store';
import { distinctUntilChanged } from 'rxjs/operators'; import { distinctUntilChanged } from 'rxjs/operators';
import { ReplaceableComponents } from '../models/replaceable-components'; import { ReplaceableComponents } from '../models/replaceable-components';
import { ReplaceableComponentsService } from '../services/replaceable-components.service';
import { SubscriptionService } from '../services/subscription.service'; import { SubscriptionService } from '../services/subscription.service';
import { ReplaceableComponentsState } from '../states/replaceable-components.state';
@Component({ @Component({
selector: 'abp-replaceable-route-container', selector: 'abp-replaceable-route-container',
@ -22,7 +21,7 @@ export class ReplaceableRouteContainerComponent implements OnInit {
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private store: Store, private replaceableComponents: ReplaceableComponentsService,
private subscription: SubscriptionService, private subscription: SubscriptionService,
) {} ) {}
@ -31,8 +30,8 @@ export class ReplaceableRouteContainerComponent implements OnInit {
this.componentKey = (this.route.snapshot.data this.componentKey = (this.route.snapshot.data
.replaceableComponent as ReplaceableComponents.RouteData).key; .replaceableComponent as ReplaceableComponents.RouteData).key;
const component$ = this.store const component$ = this.replaceableComponents
.select(ReplaceableComponentsState.getComponent(this.componentKey)) .get$(this.componentKey)
.pipe(distinctUntilChanged()); .pipe(distinctUntilChanged());
this.subscription.addOne( this.subscription.addOne(

9
npm/ng-packs/packages/core/src/lib/directives/replaceable-template.directive.ts

@ -10,15 +10,14 @@ import {
Type, Type,
ViewContainerRef, ViewContainerRef,
} from '@angular/core'; } from '@angular/core';
import { Store } from '@ngxs/store';
import compare from 'just-compare'; import compare from 'just-compare';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { filter } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
import snq from 'snq'; import snq from 'snq';
import { ABP } from '../models/common'; import { ABP } from '../models/common';
import { ReplaceableComponents } from '../models/replaceable-components'; import { ReplaceableComponents } from '../models/replaceable-components';
import { ReplaceableComponentsService } from '../services/replaceable-components.service';
import { SubscriptionService } from '../services/subscription.service'; import { SubscriptionService } from '../services/subscription.service';
import { ReplaceableComponentsState } from '../states/replaceable-components.state';
@Directive({ selector: '[abpReplaceableTemplate]', providers: [SubscriptionService] }) @Directive({ selector: '[abpReplaceableTemplate]', providers: [SubscriptionService] })
export class ReplaceableTemplateDirective implements OnInit, OnChanges { export class ReplaceableTemplateDirective implements OnInit, OnChanges {
@ -45,7 +44,7 @@ export class ReplaceableTemplateDirective implements OnInit, OnChanges {
private templateRef: TemplateRef<any>, private templateRef: TemplateRef<any>,
private cfRes: ComponentFactoryResolver, private cfRes: ComponentFactoryResolver,
private vcRef: ViewContainerRef, private vcRef: ViewContainerRef,
private store: Store, private replaceableComponents: ReplaceableComponentsService,
private subscription: SubscriptionService, private subscription: SubscriptionService,
) { ) {
this.context = { this.context = {
@ -58,8 +57,8 @@ export class ReplaceableTemplateDirective implements OnInit, OnChanges {
} }
ngOnInit() { ngOnInit() {
const component$ = this.store const component$ = this.replaceableComponents
.select(ReplaceableComponentsState.getComponent(this.data.componentKey)) .get$(this.data.componentKey)
.pipe( .pipe(
filter( filter(
(res = {} as ReplaceableComponents.ReplaceableComponent) => (res = {} as ReplaceableComponents.ReplaceableComponent) =>

9
npm/ng-packs/packages/core/src/lib/models/utility.ts

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

1
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 './multi-tenancy.service';
export * from './profile-state.service'; export * from './profile-state.service';
export * from './profile.service'; export * from './profile.service';
export * from './replaceable-components.service';
export * from './rest.service'; export * from './rest.service';
export * from './routes.service'; export * from './routes.service';
export * from './session-state.service'; export * from './session-state.service';

57
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<ReplaceableComponents.ReplaceableComponent[]>;
get replaceableComponents$(): Observable<ReplaceableComponents.ReplaceableComponent[]> {
return this.store.sliceState(state => state);
}
get replaceableComponents(): ReplaceableComponents.ReplaceableComponent[] {
return this.store.state;
}
get onUpdate$(): Observable<ReplaceableComponents.ReplaceableComponent[]> {
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<ReplaceableComponents.ReplaceableComponent> {
return this.replaceableComponents$.pipe(
map(components => components.find(component => component.key === replaceableComponentKey)),
);
}
}

42
npm/ng-packs/packages/core/src/lib/states/replaceable-components.state.ts

@ -1,11 +1,23 @@
import { Injectable, NgZone } from '@angular/core'; import { Injectable, isDevMode } from '@angular/core';
import { Router } from '@angular/router';
import { Action, createSelector, Selector, State, StateContext } from '@ngxs/store'; import { Action, createSelector, Selector, State, StateContext } from '@ngxs/store';
import snq from 'snq'; import snq from 'snq';
import { AddReplaceableComponent } from '../actions/replaceable-components.actions'; import { AddReplaceableComponent } from '../actions/replaceable-components.actions';
import { ReplaceableComponents } from '../models/replaceable-components'; 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<ReplaceableComponents.State>({ @State<ReplaceableComponents.State>({
name: 'ReplaceableComponentsState', name: 'ReplaceableComponentsState',
defaults: { replaceableComponents: [] } as ReplaceableComponents.State, defaults: { replaceableComponents: [] } as ReplaceableComponents.State,
@ -16,6 +28,7 @@ export class ReplaceableComponentsState {
static getAll({ static getAll({
replaceableComponents, replaceableComponents,
}: ReplaceableComponents.State): ReplaceableComponents.ReplaceableComponent[] { }: ReplaceableComponents.State): ReplaceableComponents.ReplaceableComponent[] {
logDeprecationMsg();
return replaceableComponents || []; return replaceableComponents || [];
} }
@ -23,6 +36,7 @@ export class ReplaceableComponentsState {
const selector = createSelector( const selector = createSelector(
[ReplaceableComponentsState], [ReplaceableComponentsState],
(state: ReplaceableComponents.State): ReplaceableComponents.ReplaceableComponent => { (state: ReplaceableComponents.State): ReplaceableComponents.ReplaceableComponent => {
logDeprecationMsg();
return snq(() => state.replaceableComponents.find(component => component.key === key)); return snq(() => state.replaceableComponents.find(component => component.key === key));
}, },
); );
@ -30,29 +44,15 @@ export class ReplaceableComponentsState {
return selector; return selector;
} }
constructor(private ngZone: NgZone, private router: Router) {} constructor(private service: ReplaceableComponentsService) {}
// 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);
});
}
@Action(AddReplaceableComponent) @Action(AddReplaceableComponent)
replaceableComponentsAction( replaceableComponentsAction(
{ getState, patchState }: StateContext<ReplaceableComponents.State>, { getState, patchState }: StateContext<ReplaceableComponents.State>,
{ payload, reload }: AddReplaceableComponent, { payload, reload }: AddReplaceableComponent,
) { ) {
logDeprecationMsg();
let { replaceableComponents } = getState(); let { replaceableComponents } = getState();
const index = snq( const index = snq(
@ -69,6 +69,6 @@ export class ReplaceableComponentsState {
replaceableComponents, replaceableComponents,
}); });
if (reload) this.reloadRoute(); this.service.add(payload, reload);
} }
} }

63
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 { DynamicLayoutComponent, RouterOutletComponent } from '../components';
import { eLayoutType } from '../enums/common'; import { eLayoutType } from '../enums/common';
import { ABP } from '../models'; import { ABP } from '../models';
import { ApplicationConfigurationService, RoutesService } from '../services'; import {
ApplicationConfigurationService,
RoutesService,
ReplaceableComponentsService,
} from '../services';
import { ReplaceableComponentsState } from '../states'; import { ReplaceableComponentsState } from '../states';
@Component({ @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', () => { describe('DynamicLayoutComponent', () => {
const mockActions: Actions = NEVER;
const mockStore = ({
selectSnapshot() {
return true;
},
} as unknown) as Store;
const createComponent = createRoutingFactory({ const createComponent = createRoutingFactory({
component: RouterOutletComponent, component: RouterOutletComponent,
stubsEnabled: false, stubsEnabled: false,
@ -113,10 +91,16 @@ describe('DynamicLayoutComponent', () => {
providers: [ providers: [
{ {
provide: RoutesService, 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: [ routes: [
{ path: '', component: RouterOutletComponent }, { path: '', component: RouterOutletComponent },
{ {
@ -163,15 +147,26 @@ describe('DynamicLayoutComponent', () => {
}); });
let spectator: SpectatorRouting<RouterOutletComponent>; let spectator: SpectatorRouting<RouterOutletComponent>;
let store: Store; let replaceableComponents: ReplaceableComponentsService;
beforeEach(async () => { beforeEach(async () => {
spectator = createComponent(); spectator = createComponent();
store = spectator.inject(Store); replaceableComponents = spectator.inject(ReplaceableComponentsService);
const routesService = spectator.inject(RoutesService); const routesService = spectator.inject(RoutesService);
routesService.add(routes); 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 () => { 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 () => { 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.detectChanges();
spectator.router.navigateByUrl('/withoutLayout'); spectator.router.navigateByUrl('/withoutLayout');

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

21
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 { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Store } from '@ngxs/store'; import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
import { of, Subject, BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { ReplaceableRouteContainerComponent } from '../components/replaceable-route-container.component'; import { ReplaceableRouteContainerComponent } from '../components/replaceable-route-container.component';
import { ReplaceableComponentsState } from '../states'; import { ReplaceableComponentsService } from '../services/replaceable-components.service';
@Component({ @Component({
selector: 'abp-external-component', selector: 'abp-external-component',
@ -30,18 +29,18 @@ const activatedRouteMock = {
}; };
describe('ReplaceableRouteContainerComponent', () => { describe('ReplaceableRouteContainerComponent', () => {
const selectResponse = new BehaviorSubject(undefined);
const mockSelect = jest.fn(() => selectResponse);
let spectator: SpectatorHost<ReplaceableRouteContainerComponent>; let spectator: SpectatorHost<ReplaceableRouteContainerComponent>;
const get$Res = new BehaviorSubject(undefined);
const createHost = createHostFactory({ const createHost = createHostFactory({
component: ReplaceableRouteContainerComponent, component: ReplaceableRouteContainerComponent,
providers: [ providers: [
{ provide: ActivatedRoute, useValue: activatedRouteMock }, { provide: ActivatedRoute, useValue: activatedRouteMock },
{ provide: Store, useValue: { select: mockSelect } }, { provide: ReplaceableComponentsService, useValue: { get$: () => get$Res } },
], ],
declarations: [ExternalComponent, DefaultComponent], declarations: [ExternalComponent, DefaultComponent],
entryComponents: [DefaultComponent, ExternalComponent], entryComponents: [DefaultComponent, ExternalComponent],
mocks: [Router],
}); });
beforeEach(() => { beforeEach(() => {
@ -55,11 +54,11 @@ describe('ReplaceableRouteContainerComponent', () => {
}); });
it("should display the external component if it's available in store.", () => { it("should display the external component if it's available in store.", () => {
selectResponse.next({ component: ExternalComponent }); get$Res.next({ component: ExternalComponent });
spectator.detectChanges(); spectator.detectChanges();
expect(spectator.query('p')).toHaveText('external'); expect(spectator.query('p')).toHaveText('external');
selectResponse.next({ component: null }); get$Res.next({ component: null });
spectator.detectChanges(); spectator.detectChanges();
expect(spectator.query('p')).toHaveText('default'); expect(spectator.query('p')).toHaveText('default');
}); });

23
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 { Component, EventEmitter, Inject, Input, OnInit, Optional, Output } from '@angular/core';
import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest'; import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest';
import { Store } from '@ngxs/store'; import { Store } from '@ngxs/store';
import { Subject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { ReplaceableTemplateDirective } from '../directives'; import { ReplaceableTemplateDirective } from '../directives';
import { ReplaceableComponents } from '../models'; import { ReplaceableComponents } from '../models';
import { Router } from '@angular/router';
import { ReplaceableComponentsService } from '../services/replaceable-components.service';
@Component({ @Component({
selector: 'abp-default-component', selector: 'abp-default-component',
@ -48,15 +50,15 @@ class ExternalComponent {
} }
describe('ReplaceableTemplateDirective', () => { describe('ReplaceableTemplateDirective', () => {
const selectResponse = new Subject();
const mockSelect = jest.fn(() => selectResponse);
let spectator: SpectatorDirective<ReplaceableTemplateDirective>; let spectator: SpectatorDirective<ReplaceableTemplateDirective>;
const get$Res = new BehaviorSubject(undefined);
const createDirective = createDirectiveFactory({ const createDirective = createDirectiveFactory({
directive: ReplaceableTemplateDirective, directive: ReplaceableTemplateDirective,
providers: [{ provide: Store, useValue: { select: mockSelect } }],
declarations: [DefaultComponent, ExternalComponent], declarations: [DefaultComponent, ExternalComponent],
entryComponents: [ExternalComponent], entryComponents: [ExternalComponent],
mocks: [Router],
providers: [{ provide: ReplaceableComponentsService, useValue: { get$: () => get$Res } }],
}); });
describe('without external component', () => { describe('without external component', () => {
@ -72,7 +74,7 @@ describe('ReplaceableTemplateDirective', () => {
`, `,
{ hostProps: { oneWay: { label: 'Test' }, twoWay: false, twoWayChange, someOutput } }, { hostProps: { oneWay: { label: 'Test' }, twoWay: false, twoWayChange, someOutput } },
); );
selectResponse.next(undefined);
const component = spectator.query(DefaultComponent); const component = spectator.query(DefaultComponent);
spectator.directive.context.initTemplate(component); spectator.directive.context.initTemplate(component);
spectator.detectChanges(); spectator.detectChanges();
@ -114,7 +116,8 @@ describe('ReplaceableTemplateDirective', () => {
`, `,
{ hostProps: { oneWay: { label: 'Test' }, twoWay: false, twoWayChange, someOutput } }, { 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()); afterEach(() => twoWayChange.mockClear());
@ -150,7 +153,7 @@ describe('ReplaceableTemplateDirective', () => {
const externalComponent = spectator.query(ExternalComponent); const externalComponent = spectator.query(ExternalComponent);
spectator.setHostInput({ oneWay: 'test' }); spectator.setHostInput({ oneWay: 'test' });
externalComponent.data.inputs.twoWay = true; externalComponent.data.inputs.twoWay = true;
selectResponse.next({ component: null, key: 'TestModule.TestComponent' }); get$Res.next({ component: null, key: 'TestModule.TestComponent' });
spectator.detectChanges(); spectator.detectChanges();
const component = spectator.query(DefaultComponent); const component = spectator.query(DefaultComponent);
spectator.directive.context.initTemplate(component); spectator.directive.context.initTemplate(component);
@ -161,14 +164,14 @@ describe('ReplaceableTemplateDirective', () => {
}); });
it('should reset default component subscriptions', () => { 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); const component = spectator.query(DefaultComponent);
spectator.directive.context.initTemplate(component); spectator.directive.context.initTemplate(component);
spectator.detectChanges(); spectator.detectChanges();
const unsubscribe = jest.fn(() => {}); const unsubscribe = jest.fn(() => {});
spectator.directive.defaultComponentSubscriptions.twoWayChange.unsubscribe = unsubscribe; spectator.directive.defaultComponentSubscriptions.twoWayChange.unsubscribe = unsubscribe;
selectResponse.next({ component: ExternalComponent, key: 'TestModule.TestComponent' }); get$Res.next({ component: ExternalComponent, key: 'TestModule.TestComponent' });
expect(unsubscribe).toHaveBeenCalled(); expect(unsubscribe).toHaveBeenCalled();
}); });
}); });

20
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);
}

2
npm/ng-packs/packages/core/src/lib/utils/index.ts

@ -3,9 +3,11 @@ export * from './common-utils';
export * from './date-utils'; export * from './date-utils';
export * from './environment-utils'; export * from './environment-utils';
export * from './factory-utils'; export * from './factory-utils';
export * from './file-utils';
export * from './form-utils'; export * from './form-utils';
export * from './generator-utils'; export * from './generator-utils';
export * from './initial-utils'; export * from './initial-utils';
export * from './internal-store-utils';
export * from './lazy-load-utils'; export * from './lazy-load-utils';
export * from './localization-utils'; export * from './localization-utils';
export * from './multi-tenancy-utils'; export * from './multi-tenancy-utils';

36
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<State> {
private state$ = new BehaviorSubject<State>(this.initialState);
private update$ = new Subject<DeepPartial<State>>();
get state() {
return this.state$.value;
}
sliceState = <Slice>(
selector: (state: State) => Slice,
compareFn: (s1: Slice, s2: Slice) => boolean = compare,
) => this.state$.pipe(map(selector), distinctUntilChanged(compareFn));
sliceUpdate = <Slice>(
selector: (state: DeepPartial<State>) => Slice,
filterFn = (x: Slice) => x !== undefined,
) => this.update$.pipe(map(selector), filter(filterFn));
constructor(private initialState: State) {}
patch(state: DeepPartial<State>) {
this.state$.next(deepMerge(this.state, state));
this.update$.next(state);
}
reset() {
this.patch(this.initialState);
}
}

17
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 { ABP } from '../models/common';
import { RoutesService } from '../services/routes.service'; import { RoutesService } from '../services/routes.service';
import { TreeNode } from './tree-utils'; import { TreeNode } from './tree-utils';
import { noop } from './common-utils';
import { NgZone } from '@angular/core';
export function findRoute(routes: RoutesService, path: string): TreeNode<ABP.Route> { export function findRoute(routes: RoutesService, path: string): TreeNode<ABP.Route> {
const node = routes.find(route => route.path === path); 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('/'); 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);
});
}

39
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 { APP_INITIALIZER } from '@angular/core';
import { Store } from '@ngxs/store'; import { Store } from '@ngxs/store';
import { AccountLayoutComponent } from '../components/account-layout/account-layout.component'; import { AccountLayoutComponent } from '../components/account-layout/account-layout.component';
@ -11,32 +11,33 @@ export const BASIC_THEME_STYLES_PROVIDERS = [
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
useFactory: configureStyles, useFactory: configureStyles,
deps: [DomInsertionService, Store], deps: [DomInsertionService, ReplaceableComponentsService],
multi: true, multi: true,
}, },
]; ];
export function configureStyles(domInsertion: DomInsertionService, store: Store) { export function configureStyles(
domInsertion: DomInsertionService,
replaceableComponents: ReplaceableComponentsService,
) {
return () => { return () => {
domInsertion.insertContent(CONTENT_STRATEGY.AppendStyleToHead(styles)); domInsertion.insertContent(CONTENT_STRATEGY.AppendStyleToHead(styles));
initLayouts(store); initLayouts(replaceableComponents);
}; };
} }
function initLayouts(store: Store) { function initLayouts(replaceableComponents: ReplaceableComponentsService) {
store.dispatch([ replaceableComponents.add({
new AddReplaceableComponent({ key: eThemeBasicComponents.ApplicationLayout,
key: eThemeBasicComponents.ApplicationLayout, component: ApplicationLayoutComponent,
component: ApplicationLayoutComponent, });
}), replaceableComponents.add({
new AddReplaceableComponent({ key: eThemeBasicComponents.AccountLayout,
key: eThemeBasicComponents.AccountLayout, component: AccountLayoutComponent,
component: AccountLayoutComponent, });
}), replaceableComponents.add({
new AddReplaceableComponent({ key: eThemeBasicComponents.EmptyLayout,
key: eThemeBasicComponents.EmptyLayout, component: EmptyLayoutComponent,
component: EmptyLayoutComponent, });
}),
]);
} }

5
npm/ng-packs/packages/theme-shared/src/lib/constants/styles.ts

@ -32,6 +32,11 @@ export default `
min-width: 215px; min-width: 215px;
} }
.datatable-scroll {
margin-bottom: 5px !important;
width: unset !important;
}
.ui-table-scrollable-body::-webkit-scrollbar { .ui-table-scrollable-body::-webkit-scrollbar {
height: 5px !important; height: 5px !important;
width: 5px !important; width: 5px !important;

67
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 { DOCUMENT } from '@angular/common';
import { ColumnMode, DatatableComponent } from '@swimlane/ngx-datatable'; 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({ @Directive({
// tslint:disable-next-line // tslint:disable-next-line
selector: 'ngx-datatable[default]', selector: 'ngx-datatable[default]',
exportAs: 'ngxDatatableDefault', exportAs: 'ngxDatatableDefault',
}) })
export class NgxDatatableDefaultDirective { export class NgxDatatableDefaultDirective implements AfterViewInit, OnDestroy {
private subscription = new Subscription();
private resizeDiff = 0;
@Input() class = 'material bordered'; @Input() class = 'material bordered';
@HostBinding('class') @HostBinding('class')
@ -14,7 +21,7 @@ export class NgxDatatableDefaultDirective {
return `ngx-datatable ${this.class}`; 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.columnMode = ColumnMode.force;
this.table.footerHeight = 50; this.table.footerHeight = 50;
this.table.headerHeight = 50; this.table.headerHeight = 50;
@ -22,4 +29,56 @@ export class NgxDatatableDefaultDirective {
this.table.scrollbarH = true; this.table.scrollbarH = true;
this.table.virtualization = false; 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;
} }

37
npm/ng-packs/yarn.lock

@ -2338,17 +2338,17 @@
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
"@ngxs/devtools-plugin@^3.6.2": "@ngxs/devtools-plugin@^3.7.0":
version "3.6.2" version "3.7.0"
resolved "https://registry.yarnpkg.com/@ngxs/devtools-plugin/-/devtools-plugin-3.6.2.tgz#aa0a4835f90fb905951d7712dc3ce508cbc15a2c" resolved "https://registry.yarnpkg.com/@ngxs/devtools-plugin/-/devtools-plugin-3.7.0.tgz#5b6b3e63411da527fcee1e8280714e1b95a838c7"
integrity sha512-0UUZlpXgEtrHoWNeVQXEvUyC6pW8nTACpqJgecuBjYJMa5imCCUSXdrpear8ztJuWwpLqMUSGk5cICNhKqK59g== integrity sha512-jjq91AbnnhzSm4QRUd7M0Y+HnUYnsSTVwUy8c1BsH8rGQ9c77xIveQaF2UBngPaDKQzTyzdVO2rV7twy3W2/fg==
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
"@ngxs/logger-plugin@^3.6.2": "@ngxs/logger-plugin@^3.7.0":
version "3.6.2" version "3.7.0"
resolved "https://registry.yarnpkg.com/@ngxs/logger-plugin/-/logger-plugin-3.6.2.tgz#1a8132a0b1eb95cad79736120fd50a331fe7347b" resolved "https://registry.yarnpkg.com/@ngxs/logger-plugin/-/logger-plugin-3.7.0.tgz#a27d6bb27360fc2677773c57868740b8cd33cc41"
integrity sha512-qF4ci9Ua7jL7r5EhZBEb0JB0HuGsTgFShK3OVTYSXU9+XfSb+5vrDLtmTshmBqQy6dWCd+2xq4LzBgc8IKMRuQ== integrity sha512-yr3NXXJEqJnxSuKiHaTbNmoprKKcrIS1PUIlOm9nvKsOmyrGskxu+MYCzUifemXDIyXSP2u0OBNPrVUumcXyhg==
dependencies: dependencies:
tslib "^1.9.0" tslib "^1.9.0"
@ -2359,6 +2359,13 @@
dependencies: dependencies:
tslib "^1.9.0" 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": "@ngxs/storage-plugin@^3.6.2":
version "3.6.2" version "3.6.2"
resolved "https://registry.yarnpkg.com/@ngxs/storage-plugin/-/storage-plugin-3.6.2.tgz#6fe2168891382c635406df02308f67b585efc60a" resolved "https://registry.yarnpkg.com/@ngxs/storage-plugin/-/storage-plugin-3.6.2.tgz#6fe2168891382c635406df02308f67b585efc60a"
@ -2366,6 +2373,13 @@
dependencies: dependencies:
tslib "^1.9.0" 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": "@ngxs/store@^3.6.2":
version "3.6.2" version "3.6.2"
resolved "https://registry.yarnpkg.com/@ngxs/store/-/store-3.6.2.tgz#cfba63dc1e5bd422e89e54b3332cd69818510624" resolved "https://registry.yarnpkg.com/@ngxs/store/-/store-3.6.2.tgz#cfba63dc1e5bd422e89e54b3332cd69818510624"
@ -2373,6 +2387,13 @@
dependencies: dependencies:
tslib "^1.9.0" 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": "@nodelib/fs.scandir@2.1.3":
version "2.1.3" version "2.1.3"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b"

Loading…
Cancel
Save