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/setup-dotnet@master
with:
dotnet-version: 3.1.100
dotnet-version: 3.1.102
- name: Build All
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
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

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`.
Then, open the `app.component.ts` and dispatch the `AddReplaceableComponent` action to replace your component with an ABP component as shown below:
Then, open the `app.component.ts` and execute the `add` method of `ReplaceableComponentsService` to replace your component with an ABP component as shown below:
```js
import { AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent action
import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum
import { Store } from '@ngxs/store'; // imported Store
//...
@Component(/* component metadata */)
export class AppComponent {
constructor(
private store: Store // injected Store
)
{
// dispatched the AddReplaceableComponent action
this.store.dispatch(
new AddReplaceableComponent({
component: YourNewRoleComponent,
key: eIdentityComponents.Roles,
}),
);
private replaceableComponents: ReplaceableComponentsService, // injected the service
) {
this.replaceableComponents.add({
component: YourNewRoleComponent,
key: eIdentityComponents.Roles,
});
}
}
```
@ -59,28 +54,24 @@ Add the following code in your layout template (`my-layout.component.html`) wher
Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js
import { AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent
import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents enum for component keys
import { Store } from '@ngxs/store'; // imported Store
import { MyApplicationLayoutComponent } from './my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent
@Component(/* component metadata */)
export class AppComponent {
constructor(
private store: Store, // injected Store
private replaceableComponents: ReplaceableComponentsService, // injected the service
) {
// dispatched the AddReplaceableComponent action
this.store.dispatch(
new AddReplaceableComponent({
component: MyApplicationLayoutComponent,
key: eThemeBasicComponents.ApplicationLayout,
}),
);
this.replaceableComponents.add({
component: MyApplicationLayoutComponent,
key: eThemeBasicComponents.ApplicationLayout,
});
}
}
```
> If you like to replace a layout component at runtime (e.g: changing the layout by pressing a button), pass the second parameter of the AddReplaceableComponent action as true. DynamicLayoutComponent loads content using a router-outlet. When the second parameter of AddReplaceableComponent is true, the route will be refreshed, so use it with caution. Your component state will be gone and any initiation logic (including HTTP requests) will be repeated.
> If you like to replace a layout component at runtime (e.g: changing the layout by pressing a button), pass the second parameter of the `add` method of `ReplaceableComponentsService` as true. DynamicLayoutComponent loads content using a router-outlet. When the second parameter of the `add` method is true, the route will be refreshed, so use it with caution. Your component state will be gone and any initiation logic (including HTTP requests) will be repeated.
### Layout Components
@ -123,26 +114,22 @@ export class LogoComponent {}
Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent
import { Store } from '@ngxs/store'; // imported Store
import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { LogoComponent } from './logo/logo.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//...
@Component(/* component metadata */)
export class AppComponent implements OnInit {
constructor(..., private store: Store) {} // injected Store
constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() {
//...
// added dispatch
this.store.dispatch(
new AddReplaceableComponent({
this.replaceableComponents.add({
component: LogoComponent,
key: eThemeBasicComponents.Logo,
}),
);
});
}
}
```
@ -166,7 +153,7 @@ yarn ng generate component routes --entryComponent
Open the generated `routes.component.ts` in `src/app/routes` folder and replace its content with the following:
```js
import { ABP, ReplaceableComponents } from '@abp/ng.core';
import { ABP } from '@abp/ng.core';
import {
Component,
HostBinding,
@ -309,26 +296,22 @@ Open the generated `routes.component.html` in `src/app/routes` folder and replac
Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent
import { Store } from '@ngxs/store'; // imported Store
import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { RoutesComponent } from './routes/routes.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//...
@Component(/* component metadata */)
export class AppComponent implements OnInit {
constructor(..., private store: Store) {} // injected Store
constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() {
//...
// added dispatch
this.store.dispatch(
new AddReplaceableComponent({
this.replaceableComponents.add({
component: RoutesComponent,
key: eThemeBasicComponents.Routes,
}),
);
});
}
}
```
@ -511,26 +494,22 @@ Open the generated `nav-items.component.html` in `src/app/nav-items` folder and
Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent
import { Store } from '@ngxs/store'; // imported Store
import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { NavItemsComponent } from './nav-items/nav-items.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//...
@Component(/* component metadata */)
export class AppComponent implements OnInit {
constructor(..., private store: Store) {} // injected Store
constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() {
//...
// added dispatch
this.store.dispatch(
new AddReplaceableComponent({
this.replaceableComponents.add({
component: NavItemsComponent,
key: eThemeBasicComponents.NavItems,
}),
);
});
}
}
```

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

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 Name { get; set; }
public string SurName { get; set; }
public string Email { get; set; }
public bool EmailVerified { get; set; }
public string PhoneNumber { get; set; }
public bool PhoneNumberVerified { get; set; }
public string[] Roles { get; set; }
}
}

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

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

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

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

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

@ -13,6 +13,16 @@ namespace Volo.Abp.Security.Claims
/// </summary>
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>
/// Default: <see cref="ClaimTypes.NameIdentifier"/>
/// </summary>
@ -48,7 +58,6 @@ namespace Volo.Abp.Security.Claims
/// </summary>
public static string TenantId { get; set; } = "tenantid";
/// <summary>
/// Default: "editionid".
/// </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 Name => this.FindClaimValue(AbpClaimTypes.Name);
public virtual string SurName => this.FindClaimValue(AbpClaimTypes.SurName);
public virtual string PhoneNumber => this.FindClaimValue(AbpClaimTypes.PhoneNumber);
public virtual bool PhoneNumberVerified => string.Equals(this.FindClaimValue(AbpClaimTypes.PhoneNumberVerified), "true", StringComparison.InvariantCultureIgnoreCase);
@ -56,4 +60,4 @@ namespace Volo.Abp.Users
return FindClaims(AbpClaimTypes.Role).Any(c => c.Value == roleName);
}
}
}
}

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

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

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

@ -2,9 +2,11 @@
using System.Collections.Generic;
using System.Text;
using Volo.Abp.TestApp.Testing;
using Xunit;
namespace Volo.Abp.MongoDB.DataFiltering
{
[Collection(MongoTestCollection.Name)]
public class HardDelete_Tests : HardDelete_Tests<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
{
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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
@ -25,9 +26,9 @@ namespace Volo.Abp.FeatureManagement
Options = options.Value;
}
public virtual async Task<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
{
@ -45,6 +46,14 @@ namespace Volo.Abp.FeatureManagement
foreach (var featureDefinition in group.GetFeaturesWithChildren())
{
if (providerName == TenantFeatureValueProvider.ProviderName &&
CurrentTenant.Id == null &&
providerKey == null &&
!featureDefinition.IsAvailableToHost)
{
continue;
}
var feature = await FeatureManager.GetOrNullWithProviderAsync(featureDefinition.Name, providerName, providerKey);
groupDto.Features.Add(new FeatureDto
{
@ -70,9 +79,9 @@ namespace Volo.Abp.FeatureManagement
return result;
}
public virtual async Task UpdateAsync([NotNull] string providerName, [NotNull] string providerKey, UpdateFeaturesDto input)
public virtual async Task UpdateAsync([NotNull] string providerName, string providerKey, UpdateFeaturesDto input)
{
await CheckProviderPolicy(providerName);
await CheckProviderPolicy(providerName, providerKey);
foreach (var feature in input.Features)
{
@ -93,12 +102,20 @@ namespace Volo.Abp.FeatureManagement
}
}
protected virtual async Task CheckProviderPolicy(string providerName)
protected virtual async Task CheckProviderPolicy(string providerName, string providerKey)
{
var policyName = Options.ProviderPolicies.GetOrDefault(providerName);
if (policyName.IsNullOrEmpty())
string policyName;
if (providerName == TenantFeatureValueProvider.ProviderName && CurrentTenant.Id == null && providerKey == null )
{
policyName = "FeatureManagement.ManageHostFeatures";
}
else
{
throw new AbpException($"No policy defined to get/set permissions for the provider '{policyName}'. Use {nameof(FeatureManagementOptions)} to map the policy.");
policyName = Options.ProviderPolicies.GetOrDefault(providerName);
if (policyName.IsNullOrEmpty())
{
throw new AbpException($"No policy defined to get/set permissions for the provider '{policyName}'. Use {nameof(FeatureManagementOptions)} to map the policy.");
}
}
await AuthorizationService.CheckAsync(policyName);

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

@ -2,6 +2,8 @@
"culture": "en",
"texts": {
"Features": "Features",
"NoFeatureFoundMessage": "There isn't any available feature."
"NoFeatureFoundMessage": "There isn't any available feature.",
"Permission:FeatureManagement": "Feature management",
"Permission:FeatureManagement.ManageHostFeatures": "Manage Host features"
}
}
}

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

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

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",
"texts": {
"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",
"texts": {
"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.FeatureManagement.Localization;
using Volo.Abp.Features;
using Volo.Abp.Localization;
using Volo.Abp.Localization.ExceptionHandling;
using Volo.Abp.Modularity;
using Volo.Abp.VirtualFileSystem;
namespace Volo.Abp.FeatureManagement
{

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

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

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.ObjectExtending;
using Volo.Abp.ObjectExtending.Modularity;
using Volo.Abp.Security.Claims;
using Volo.Abp.Users;
namespace Volo.Abp.Identity
@ -36,7 +37,7 @@ namespace Volo.Abp.Identity
options.EtoMappings.Add<IdentityRole, IdentityRoleEto>(typeof(AbpIdentityDomainModule));
options.EtoMappings.Add<OrganizationUnit, OrganizationUnitEto>(typeof(AbpIdentityDomainModule));
});
var identityBuilder = context.Services.AddAbpIdentity(options =>
{
options.User.RequireUniqueEmail = true;
@ -45,6 +46,13 @@ namespace Volo.Abp.Identity
context.Services.AddObjectAccessor(identityBuilder);
context.Services.ExecutePreConfiguredActions(identityBuilder);
Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserIdClaimType = AbpClaimTypes.UserId;
options.ClaimsIdentity.UserNameClaimType = AbpClaimTypes.UserName;
options.ClaimsIdentity.RoleClaimType = AbpClaimTypes.Role;
});
AddAbpIdentityOptionsFactory(context.Services);
}
@ -67,7 +75,7 @@ namespace Volo.Abp.Identity
IdentityModuleExtensionConsts.EntityNames.ClaimType,
typeof(IdentityClaimType)
);
ModuleExtensionConfigurationHelper.ApplyEntityConfigurationToEntity(
IdentityModuleExtensionConsts.ModuleName,
IdentityModuleExtensionConsts.EntityNames.OrganizationUnit,
@ -81,4 +89,4 @@ namespace Volo.Abp.Identity
services.Replace(ServiceDescriptor.Scoped<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.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
@ -6,6 +7,7 @@ using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Security.Claims;
using Volo.Abp.Uow;
using Volo.Abp.Users;
namespace Volo.Abp.Identity
{
@ -13,11 +15,11 @@ namespace Volo.Abp.Identity
{
public AbpUserClaimsPrincipalFactory(
UserManager<IdentityUser> userManager,
RoleManager<IdentityRole> roleManager,
IOptions<IdentityOptions> options)
RoleManager<IdentityRole> roleManager,
IOptions<IdentityOptions> options)
: base(
userManager,
roleManager,
userManager,
roleManager,
options)
{
}
@ -26,14 +28,34 @@ namespace Volo.Abp.Identity
public override async Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
{
var principal = await base.CreateAsync(user);
var identity = principal.Identities.First();
if (user.TenantId.HasValue)
{
principal.Identities
.First()
.AddClaim(new Claim(AbpClaimTypes.TenantId, user.TenantId.ToString()));
identity.AddIfNotContains(new Claim(AbpClaimTypes.TenantId, user.TenantId.ToString()));
}
if (!user.Name.IsNullOrWhiteSpace())
{
identity.AddIfNotContains(new Claim(AbpClaimTypes.Name, user.Name));
}
if (!user.Surname.IsNullOrWhiteSpace())
{
identity.AddIfNotContains(new Claim(AbpClaimTypes.SurName, user.Surname));
}
if (!user.PhoneNumber.IsNullOrWhiteSpace())
{
identity.AddIfNotContains(new Claim(AbpClaimTypes.PhoneNumber, user.PhoneNumber));
}
identity.AddIfNotContains(new Claim(AbpClaimTypes.PhoneNumberVerified, user.PhoneNumberConfirmed.ToString()));
if (!user.Email.IsNullOrWhiteSpace())
{
identity.AddIfNotContains(new Claim(AbpClaimTypes.Email, user.Email));
}
identity.AddIfNotContains(new Claim(AbpClaimTypes.EmailVerified, user.EmailConfirmed.ToString()));
return principal;
}
}

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.Collections.Generic;
using System.Text;
using Xunit;
namespace Volo.Abp.Identity.MongoDB
{
[Collection(MongoTestCollection.Name)]
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",
"DisplayName:DefaultConnectionString": "Default Connection String",
"DisplayName:UseSharedDatabase": "Use the Shared Database",
"ManageHostFeatures": "Manage Host features",
"Permission:TenantManagement": "Tenant management",
"Permission:Create": "Create",
"Permission:Edit": "Edit",
"Permission:Delete": "Delete",
"Permission:ManageConnectionStrings": "Manage connection strings",
"Permission:ManageFeatures": "Manage features",
"DisplayName:AdminEmailAddress": "Admin Email Address",
"DisplayName:AdminPassword": "Admin Password"
"DisplayName:AdminEmailAddress": "Admin Email Address",
"DisplayName:AdminPassword": "Admin Password"
}
}
}

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

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

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

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

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

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.Mvc.Localization
@using Volo.Abp.AspNetCore.Mvc.UI.Layout
@using Volo.Abp.FeatureManagement
@using Volo.Abp.TenantManagement
@using Volo.Abp.TenantManagement.Localization
@using Volo.Abp.TenantManagement.Web.Navigation
@ -29,9 +30,13 @@
<abp-card-title>@L["Tenants"]</abp-card-title>
</abp-column>
<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))
{
<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-row>
@ -39,4 +44,4 @@
<abp-card-body>
<abp-table striped-rows="true" class="nowrap"></abp-table>
</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
);
$(function () {
var _$wrapper = $('#TenantsWrapper');
@ -128,5 +128,12 @@
e.preventDefault();
_createModal.open();
});
_$wrapper.find('button[name=ManageHostFeatures]').click(function (e) {
e.preventDefault();
_featuresModal.open({
providerName: 'T'
});
});
});
})();

10
npm/ng-packs/package.json

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

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

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

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

@ -1,7 +1,8 @@
import { ReplaceableComponents } from '../models/replaceable-components';
// tslint:disable: max-line-length
/**
* @see usage: https://github.com/abpframework/abp/pull/2522#issue-358333183
* @deprecated To be deleted in v4.0. Use ReplaceableComponentsService instead. See the doc (https://docs.abp.io/en/abp/latest/UI/Angular/Component-Replacement)
*/
export class AddReplaceableComponent {
static readonly type = '[ReplaceableComponents] Add';

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

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

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

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

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

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

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

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 { Router } from '@angular/router';
import { Injectable, isDevMode } from '@angular/core';
import { Action, createSelector, Selector, State, StateContext } from '@ngxs/store';
import snq from 'snq';
import { AddReplaceableComponent } from '../actions/replaceable-components.actions';
import { ReplaceableComponents } from '../models/replaceable-components';
import { noop } from '../utils/common-utils';
import { ReplaceableComponentsService } from '../services/replaceable-components.service';
function logDeprecationMsg() {
if (isDevMode()) {
console.warn(`
ReplacableComponentsState has been deprecated. Use ReplaceableComponentsService instead.
See the doc https://docs.abp.io/en/abp/latest/UI/Angular/Component-Replacement
`);
}
}
// tslint:disable: max-line-length
/**
* @deprecated To be deleted in v4.0. Use ReplaceableComponentsService instead. See the doc (https://docs.abp.io/en/abp/latest/UI/Angular/Component-Replacement)
*/
@State<ReplaceableComponents.State>({
name: 'ReplaceableComponentsState',
defaults: { replaceableComponents: [] } as ReplaceableComponents.State,
@ -16,6 +28,7 @@ export class ReplaceableComponentsState {
static getAll({
replaceableComponents,
}: ReplaceableComponents.State): ReplaceableComponents.ReplaceableComponent[] {
logDeprecationMsg();
return replaceableComponents || [];
}
@ -23,6 +36,7 @@ export class ReplaceableComponentsState {
const selector = createSelector(
[ReplaceableComponentsState],
(state: ReplaceableComponents.State): ReplaceableComponents.ReplaceableComponent => {
logDeprecationMsg();
return snq(() => state.replaceableComponents.find(component => component.key === key));
},
);
@ -30,29 +44,15 @@ export class ReplaceableComponentsState {
return selector;
}
constructor(private ngZone: NgZone, private router: Router) {}
// TODO: Create a shared service for route reload and more
private reloadRoute() {
const { shouldReuseRoute } = this.router.routeReuseStrategy;
const setRouteReuse = (reuse: typeof shouldReuseRoute) => {
this.router.routeReuseStrategy.shouldReuseRoute = reuse;
};
setRouteReuse(() => false);
this.router.navigated = false;
this.ngZone.run(async () => {
await this.router.navigateByUrl(this.router.url).catch(noop);
setRouteReuse(shouldReuseRoute);
});
}
constructor(private service: ReplaceableComponentsService) {}
@Action(AddReplaceableComponent)
replaceableComponentsAction(
{ getState, patchState }: StateContext<ReplaceableComponents.State>,
{ payload, reload }: AddReplaceableComponent,
) {
logDeprecationMsg();
let { replaceableComponents } = getState();
const index = snq(
@ -69,6 +69,6 @@ export class ReplaceableComponentsState {
replaceableComponents,
});
if (reload) this.reloadRoute();
this.service.add(payload, reload);
}
}

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 { eLayoutType } from '../enums/common';
import { ABP } from '../models';
import { ApplicationConfigurationService, RoutesService } from '../services';
import {
ApplicationConfigurationService,
RoutesService,
ReplaceableComponentsService,
} from '../services';
import { ReplaceableComponentsState } from '../states';
@Component({
@ -78,33 +82,7 @@ const routes: ABP.Route[] = [
},
];
const storeData = {
ReplaceableComponentsState: {
replaceableComponents: [
{
key: 'Theme.ApplicationLayoutComponent',
component: DummyApplicationLayoutComponent,
},
{
key: 'Theme.AccountLayoutComponent',
component: DummyAccountLayoutComponent,
},
{
key: 'Theme.EmptyLayoutComponent',
component: DummyEmptyLayoutComponent,
},
],
},
};
describe('DynamicLayoutComponent', () => {
const mockActions: Actions = NEVER;
const mockStore = ({
selectSnapshot() {
return true;
},
} as unknown) as Store;
const createComponent = createRoutingFactory({
component: RouterOutletComponent,
stubsEnabled: false,
@ -113,10 +91,16 @@ describe('DynamicLayoutComponent', () => {
providers: [
{
provide: RoutesService,
useFactory: () => new RoutesService(mockActions, mockStore),
useFactory: () =>
new RoutesService(NEVER, ({
selectSnapshot() {
return true;
},
} as unknown) as Store),
},
ReplaceableComponentsService,
],
imports: [RouterModule, DummyLayoutModule, NgxsModule.forRoot([ReplaceableComponentsState])],
imports: [RouterModule, DummyLayoutModule, NgxsModule.forRoot()],
routes: [
{ path: '', component: RouterOutletComponent },
{
@ -163,15 +147,26 @@ describe('DynamicLayoutComponent', () => {
});
let spectator: SpectatorRouting<RouterOutletComponent>;
let store: Store;
let replaceableComponents: ReplaceableComponentsService;
beforeEach(async () => {
spectator = createComponent();
store = spectator.inject(Store);
replaceableComponents = spectator.inject(ReplaceableComponentsService);
const routesService = spectator.inject(RoutesService);
routesService.add(routes);
store.reset(storeData);
replaceableComponents.add({
key: 'Theme.ApplicationLayoutComponent',
component: DummyApplicationLayoutComponent,
});
replaceableComponents.add({
key: 'Theme.AccountLayoutComponent',
component: DummyAccountLayoutComponent,
});
replaceableComponents.add({
key: 'Theme.EmptyLayoutComponent',
component: DummyEmptyLayoutComponent,
});
});
it('should handle application layout from parent abp route and display it', async () => {
@ -204,8 +199,8 @@ describe('DynamicLayoutComponent', () => {
});
it('should not display any layout when layouts are empty', async () => {
store.reset({ ...storeData, ReplaceableComponentsState: {} });
const spy = jest.spyOn(replaceableComponents, 'get');
spy.mockReturnValue(null);
spectator.detectChanges();
spectator.router.navigateByUrl('/withoutLayout');

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

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

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 './environment-utils';
export * from './factory-utils';
export * from './file-utils';
export * from './form-utils';
export * from './generator-utils';
export * from './initial-utils';
export * from './internal-store-utils';
export * from './lazy-load-utils';
export * from './localization-utils';
export * from './multi-tenancy-utils';

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 { RoutesService } from '../services/routes.service';
import { TreeNode } from './tree-utils';
import { noop } from './common-utils';
import { NgZone } from '@angular/core';
export function findRoute(routes: RoutesService, path: string): TreeNode<ABP.Route> {
const node = routes.find(route => route.path === path);
@ -23,3 +25,18 @@ export function getRoutePath(router: Router, url = router.url) {
return '/' + (primaryGroup || emptyGroup).segments.map(({ path }) => path).join('/');
}
export function reloadRoute(router: Router, ngZone: NgZone) {
const { shouldReuseRoute } = router.routeReuseStrategy;
const setRouteReuse = (reuse: typeof shouldReuseRoute) => {
router.routeReuseStrategy.shouldReuseRoute = reuse;
};
setRouteReuse(() => false);
router.navigated = false;
ngZone.run(async () => {
await router.navigateByUrl(router.url).catch(noop);
setRouteReuse(shouldReuseRoute);
});
}

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

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

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

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 { ColumnMode, DatatableComponent } from '@swimlane/ngx-datatable';
import { DOCUMENT } from '@angular/common';
import { AfterViewInit, Directive, HostBinding, Inject, Input, OnDestroy } from '@angular/core';
import { ColumnMode, DatatableComponent, ScrollerComponent } from '@swimlane/ngx-datatable';
import { fromEvent, Subscription } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
@Directive({
// tslint:disable-next-line
selector: 'ngx-datatable[default]',
exportAs: 'ngxDatatableDefault',
})
export class NgxDatatableDefaultDirective {
export class NgxDatatableDefaultDirective implements AfterViewInit, OnDestroy {
private subscription = new Subscription();
private resizeDiff = 0;
@Input() class = 'material bordered';
@HostBinding('class')
@ -14,7 +21,7 @@ export class NgxDatatableDefaultDirective {
return `ngx-datatable ${this.class}`;
}
constructor(private table: DatatableComponent) {
constructor(private table: DatatableComponent, @Inject(DOCUMENT) private document: MockDocument) {
this.table.columnMode = ColumnMode.force;
this.table.footerHeight = 50;
this.table.headerHeight = 50;
@ -22,4 +29,56 @@ export class NgxDatatableDefaultDirective {
this.table.scrollbarH = true;
this.table.virtualization = false;
}
private fixHorizontalGap(scroller: ScrollerComponent) {
const { body, documentElement } = this.document;
if (documentElement.scrollHeight !== documentElement.clientHeight) {
if (this.resizeDiff === 0) {
this.resizeDiff = window.innerWidth - body.offsetWidth;
scroller.scrollWidth -= this.resizeDiff;
}
} else {
scroller.scrollWidth += this.resizeDiff;
this.resizeDiff = 0;
}
}
private fixStyleOnWindowResize() {
// avoided @HostListener('window:resize') in favor of performance
const subscription = fromEvent(window, 'resize')
.pipe(debounceTime(500))
.subscribe(() => {
const { scroller } = this.table.bodyComponent;
if (!scroller) return;
this.fixHorizontalGap(scroller);
});
this.subscription.add(subscription);
}
ngAfterViewInit() {
this.fixStyleOnWindowResize();
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
// fix: https://github.com/angular/angular/issues/20351
interface MockDocument {
body: MockBody;
documentElement: MockDocumentElement;
}
interface MockBody {
offsetWidth: number;
}
interface MockDocumentElement {
clientHeight: number;
scrollHeight: number;
}

37
npm/ng-packs/yarn.lock

@ -2338,17 +2338,17 @@
dependencies:
tslib "^1.9.0"
"@ngxs/devtools-plugin@^3.6.2":
version "3.6.2"
resolved "https://registry.yarnpkg.com/@ngxs/devtools-plugin/-/devtools-plugin-3.6.2.tgz#aa0a4835f90fb905951d7712dc3ce508cbc15a2c"
integrity sha512-0UUZlpXgEtrHoWNeVQXEvUyC6pW8nTACpqJgecuBjYJMa5imCCUSXdrpear8ztJuWwpLqMUSGk5cICNhKqK59g==
"@ngxs/devtools-plugin@^3.7.0":
version "3.7.0"
resolved "https://registry.yarnpkg.com/@ngxs/devtools-plugin/-/devtools-plugin-3.7.0.tgz#5b6b3e63411da527fcee1e8280714e1b95a838c7"
integrity sha512-jjq91AbnnhzSm4QRUd7M0Y+HnUYnsSTVwUy8c1BsH8rGQ9c77xIveQaF2UBngPaDKQzTyzdVO2rV7twy3W2/fg==
dependencies:
tslib "^1.9.0"
"@ngxs/logger-plugin@^3.6.2":
version "3.6.2"
resolved "https://registry.yarnpkg.com/@ngxs/logger-plugin/-/logger-plugin-3.6.2.tgz#1a8132a0b1eb95cad79736120fd50a331fe7347b"
integrity sha512-qF4ci9Ua7jL7r5EhZBEb0JB0HuGsTgFShK3OVTYSXU9+XfSb+5vrDLtmTshmBqQy6dWCd+2xq4LzBgc8IKMRuQ==
"@ngxs/logger-plugin@^3.7.0":
version "3.7.0"
resolved "https://registry.yarnpkg.com/@ngxs/logger-plugin/-/logger-plugin-3.7.0.tgz#a27d6bb27360fc2677773c57868740b8cd33cc41"
integrity sha512-yr3NXXJEqJnxSuKiHaTbNmoprKKcrIS1PUIlOm9nvKsOmyrGskxu+MYCzUifemXDIyXSP2u0OBNPrVUumcXyhg==
dependencies:
tslib "^1.9.0"
@ -2359,6 +2359,13 @@
dependencies:
tslib "^1.9.0"
"@ngxs/router-plugin@^3.7.0":
version "3.7.0"
resolved "https://registry.yarnpkg.com/@ngxs/router-plugin/-/router-plugin-3.7.0.tgz#9d0595d8ec12e6143eb9f2f726fcc7cdec8cd635"
integrity sha512-nmArryNIBLWqIKLWelOQhgxEC3evLPDNDkE+PEYvwoASC4NP5rHIqJv0borJAWBCwo10t5wPeT1417vBQex5aQ==
dependencies:
tslib "^1.9.0"
"@ngxs/storage-plugin@^3.6.2":
version "3.6.2"
resolved "https://registry.yarnpkg.com/@ngxs/storage-plugin/-/storage-plugin-3.6.2.tgz#6fe2168891382c635406df02308f67b585efc60a"
@ -2366,6 +2373,13 @@
dependencies:
tslib "^1.9.0"
"@ngxs/storage-plugin@^3.7.0":
version "3.7.0"
resolved "https://registry.yarnpkg.com/@ngxs/storage-plugin/-/storage-plugin-3.7.0.tgz#3a0bc75f5dbba17a18e37b69cfde5d470cfceb45"
integrity sha512-j5rGYfhi1S+sAky956DIs+6AYP9FMBWL2Uz+omKBS/i42mjhyD26UfApORjwCOyW5PCb4Tq3B14ZPxyAaSi/OA==
dependencies:
tslib "^1.9.0"
"@ngxs/store@^3.6.2":
version "3.6.2"
resolved "https://registry.yarnpkg.com/@ngxs/store/-/store-3.6.2.tgz#cfba63dc1e5bd422e89e54b3332cd69818510624"
@ -2373,6 +2387,13 @@
dependencies:
tslib "^1.9.0"
"@ngxs/store@^3.7.0":
version "3.7.0"
resolved "https://registry.yarnpkg.com/@ngxs/store/-/store-3.7.0.tgz#e46387219dae610c685accc119ae42e351afcaa4"
integrity sha512-w9fG/DhKBgH1VJMKSoeNW9x9ycD9/Dzy+VkpFD8Jv0JBNX0MRgP+5KQQe3ZKwnJ+7S0UV/99JvJaWgxc/WOvPw==
dependencies:
tslib "^1.9.0"
"@nodelib/fs.scandir@2.1.3":
version "2.1.3"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b"

Loading…
Cancel
Save