Browse Source

Merge branch 'dev' of https://github.com/abpframework/abp into pr/2277

pull/2625/head
Galip Tolga Erdem 7 years ago
parent
commit
a529a3df9e
  1. 57
      docs/en/FluentValidation.md
  2. 3
      docs/en/Modules/Blogging.md
  3. 35
      docs/en/Validation.md
  4. 3
      docs/zh-Hans/FluentValidation.md
  5. 5
      docs/zh-Hans/Logging.md
  6. 164
      docs/zh-Hans/Validation.md
  7. 9
      docs/zh-Hans/docs-nav.json
  8. 16
      framework/Volo.Abp.sln
  9. 1
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/AbpValidationActionFilter.cs
  10. 13
      framework/src/Volo.Abp.AspNetCore.Serilog/Microsoft/AspNetCore/Builder/AbpAspNetCoreSerilogApplicationBuilderExtensions.cs
  11. 27
      framework/src/Volo.Abp.AspNetCore.Serilog/Properties/launchSettings.json
  12. 27
      framework/src/Volo.Abp.AspNetCore.Serilog/Volo.Abp.AspNetCore.Serilog.csproj
  13. 13
      framework/src/Volo.Abp.AspNetCore.Serilog/Volo/Abp/AspNetCore/Serilog/AbpAspNetCoreSerilogModule.cs
  14. 30
      framework/src/Volo.Abp.AspNetCore.Serilog/Volo/Abp/AspNetCore/Serilog/AbpAspNetCoreSerilogOptions.cs
  15. 69
      framework/src/Volo.Abp.AspNetCore.Serilog/Volo/Abp/AspNetCore/Serilog/AbpSerilogMiddleware.cs
  16. 1
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NuGetPackageTarget.cs
  17. 4
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectFinder.cs
  18. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs
  19. 4
      framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/zh-Hans.json
  20. 2
      framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.csproj
  21. 24
      framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo.Abp.AspNetCore.Serilog.Tests.csproj
  22. 39
      framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo/Abp/AspNetCore/App/AbpSerilogTestModule.cs
  23. 19
      framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo/Abp/AspNetCore/App/CollectingSink.cs
  24. 27
      framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo/Abp/AspNetCore/App/SerilogTestController.cs
  25. 20
      framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo/Abp/AspNetCore/App/Startup.cs
  26. 31
      framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo/Abp/AspNetCore/Serilog/AbpSerilogTestBase.cs
  27. 97
      framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo/Abp/AspNetCore/Serilog/Serilog_Enrichers_Tests.cs
  28. 2
      modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/zh-Hans.json
  29. 7
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs
  30. 9
      modules/blogging/README.md
  31. 2
      modules/blogging/app/Volo.BloggingTestApp/Volo.BloggingTestApp.csproj
  32. 2
      modules/client-simulation/demo/Volo.ClientSimulation.Demo/Volo.ClientSimulation.Demo.csproj
  33. 20
      modules/docs/README.md
  34. 2
      modules/docs/app/VoloDocs.Web/VoloDocs.Web.csproj
  35. BIN
      modules/docs/screenshot.png
  36. BIN
      modules/docs/screenshot2.png
  37. 2
      modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentityUserConsts.cs
  38. 4
      modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/zh-Hans.json
  39. 16
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityRole.cs
  40. 8
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityRoleNameChangedEvent.cs
  41. 2
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityRoleStore.cs
  42. 1
      modules/identity/src/Volo.Abp.PermissionManagement.Domain.Identity/Volo.Abp.PermissionManagement.Domain.Identity.csproj
  43. 43
      modules/identity/src/Volo.Abp.PermissionManagement.Domain.Identity/Volo/Abp/PermissionManagement/Identity/RoleUpdateEventHandler.cs
  44. 58
      modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/RoleChangingEvents_Test.cs
  45. 4
      modules/permission-management/Volo.Abp.PermissionManagement.sln
  46. 2
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/IPermissionManager.cs
  47. 2
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionGrant.cs
  48. 6
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionManager.cs
  49. 2
      modules/permission-management/test/Volo.Abp.PermissionManagement.Application.Tests/Volo.Abp.PermissionManagement.Application.Tests.csproj
  50. 0
      modules/permission-management/test/Volo.Abp.PermissionManagement.Tests/Volo.Abp.PermissionManagement.Domain.Tests.csproj
  51. 15
      modules/permission-management/test/Volo.Abp.PermissionManagement.Tests/Volo/Abp/PermissionManagement/PermissionManager_Tests.cs
  52. 3
      npm/ng-packs/packages/core/src/lib/actions/replaceable-components.actions.ts
  53. 3
      npm/ng-packs/packages/core/src/lib/states/config.state.ts
  54. 2
      npm/ng-packs/packages/core/src/lib/states/session.state.ts
  55. 2
      npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/account-layout.component.ts
  56. 29
      npm/ng-packs/packages/theme-basic/src/lib/components/application-layout/application-layout.component.html
  57. 2
      npm/ng-packs/packages/theme-basic/src/lib/components/empty-layout/empty-layout.component.ts
  58. 6
      npm/ng-packs/packages/theme-basic/src/lib/constants/styles.ts
  59. 56
      npm/ng-packs/packages/theme-shared/src/lib/abstracts/toaster.ts
  60. 1
      npm/ng-packs/packages/theme-shared/src/lib/animations/index.ts
  61. 17
      npm/ng-packs/packages/theme-shared/src/lib/animations/toast.animations.ts
  62. 34
      npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.html
  63. 120
      npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.scss
  64. 76
      npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.ts
  65. 25
      npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.scss
  66. 46
      npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts
  67. 11
      npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.html
  68. 13
      npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.scss
  69. 46
      npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.ts
  70. 16
      npm/ng-packs/packages/theme-shared/src/lib/components/toast/toast.component.html
  71. 80
      npm/ng-packs/packages/theme-shared/src/lib/components/toast/toast.component.scss
  72. 74
      npm/ng-packs/packages/theme-shared/src/lib/components/toast/toast.component.ts
  73. 130
      npm/ng-packs/packages/theme-shared/src/lib/constants/styles.ts
  74. 16
      npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts
  75. 22
      npm/ng-packs/packages/theme-shared/src/lib/models/toaster.ts
  76. 66
      npm/ng-packs/packages/theme-shared/src/lib/services/confirmation.service.ts
  77. 119
      npm/ng-packs/packages/theme-shared/src/lib/services/toaster.service.ts
  78. 45
      npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts
  79. 12
      npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts
  80. 10
      npm/ng-packs/packages/theme-shared/src/lib/tests/modal.component.spec.ts
  81. 49
      npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts
  82. 3
      npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts
  83. 1
      nupkg/common.ps1
  84. 2
      samples/BookStore-Angular-MongoDb/aspnet-core/src/Acme.BookStore.HttpApi.Host/Acme.BookStore.HttpApi.Host.csproj
  85. 2
      samples/BookStore-Modular/application/src/Acme.BookStore.Web/Acme.BookStore.Web.csproj
  86. 2
      samples/BookStore-Modular/modules/book-management/host/Acme.BookStore.BookManagement.HttpApi.Host/Acme.BookStore.BookManagement.HttpApi.Host.csproj
  87. 2
      samples/BookStore-Modular/modules/book-management/host/Acme.BookStore.BookManagement.IdentityServer/Acme.BookStore.BookManagement.IdentityServer.csproj
  88. 2
      samples/BookStore-Modular/modules/book-management/host/Acme.BookStore.BookManagement.Web.Host/Acme.BookStore.BookManagement.Web.Host.csproj
  89. 2
      samples/BookStore-Modular/modules/book-management/host/Acme.BookStore.BookManagement.Web.Unified/Acme.BookStore.BookManagement.Web.Unified.csproj
  90. 4
      samples/BookStore/src/Acme.BookStore.Application/BookStoreApplicationModule.cs
  91. 2
      samples/BookStore/src/Acme.BookStore.Web/Acme.BookStore.Web.csproj
  92. 2
      samples/DashboardDemo/src/DashboardDemo.Web/DashboardDemo.Web.csproj
  93. 2
      samples/MicroserviceDemo/applications/AuthServer.Host/AuthServer.Host.csproj
  94. 2
      samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminApp.Host.csproj
  95. 2
      samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSite.Host.csproj
  96. 2
      samples/MicroserviceDemo/gateways/BackendAdminAppGateway.Host/BackendAdminAppGateway.Host.csproj
  97. 2
      samples/MicroserviceDemo/gateways/InternalGateway.Host/InternalGateway.Host.csproj
  98. 2
      samples/MicroserviceDemo/gateways/PublicWebSiteGateway.Host/PublicWebSiteGateway.Host.csproj
  99. 2
      samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingService.Host.csproj
  100. 2
      samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityService.Host.csproj

57
docs/en/FluentValidation.md

@ -1,3 +1,58 @@
# FluentValidation Integration # FluentValidation Integration
TODO ABP [Validation](Validation.md) infrastructure is extensible. [Volo.Abp.FluentValidation](https://www.nuget.org/packages/Volo.Abp.FluentValidation) NuGet package extends the validation system to work with the [FluentValidation](https://fluentvalidation.net/) library.
## Installation
It is suggested to use the [ABP CLI](CLI.md) to install this package.
### Using the ABP CLI
Open a command line window in the folder of the project (.csproj file) and type the following command:
````bash
abp add-package Volo.Abp.FluentValidation
````
### Manual Installation
If you want to manually install;
1. Add the [Volo.Abp.FluentValidation](https://www.nuget.org/packages/Volo.Abp.FluentValidation) NuGet package to your project:
````
Install-Package Volo.Abp.FluentValidation
````
2. Add the `AbpFluentValidationModule` to the dependency list of your module:
````csharp
[DependsOn(
//...other dependencies
typeof(AbpFluentValidationModule) //Add the FluentValidation module
)]
public class YourModule : AbpModule
{
}
````
## Using the FluentValidation
Follow [the FluentValidation documentation](https://fluentvalidation.net/) to create validator classes. Example:
````csharp
public class CreateUpdateBookDtoValidator : AbstractValidator<CreateUpdateBookDto>
{
public CreateUpdateBookDtoValidator()
{
RuleFor(x => x.Name).Length(3, 10);
RuleFor(x => x.Price).ExclusiveBetween(0.0f, 999.0f);
}
}
````
ABP will automatically find this class and associate with the `CreateUpdateBookDto` on object validation.
## See Also
* [Validation System](Validation.md)

3
docs/en/Modules/Blogging.md

@ -0,0 +1,3 @@
# IdentityServer Module
TODO

35
docs/en/Validation.md

@ -120,6 +120,39 @@ Once ABP determines a validation error, it throws an exception of type `AbpValid
* Log level of the `AbpValidationException` is set to `Warning`. It logs all the validation errors to the [logging system](Logging.md). * Log level of the `AbpValidationException` is set to `Warning`. It logs all the validation errors to the [logging system](Logging.md).
* `AbpValidationException` is automatically caught by the ABP framework and converted to a usable error into with HTTP 400 status code. See the [exception handling](Exception-Handling.md) document for more. * `AbpValidationException` is automatically caught by the ABP framework and converted to a usable error into with HTTP 400 status code. See the [exception handling](Exception-Handling.md) document for more.
## Advanced Topics
### IObjectValidator
In addition to the automatic validation, you may want to manually validate an object. In this case, [inject](Dependency-Injection.md) and use the `IObjectValidator` service:
* `Validate` method validates the given object based on the validation rules and throws an `AbpValidationException` if it is not in a valid state.
* `GetErrors` doesn't throw an exception, but only returns the validation errors.
`IObjectValidator` is implemented by the `ObjectValidator` by default. `ObjectValidator` is extensible; you can implement `IObjectValidationContributor` interface to contribute a custom logic. Example:
````csharp
public class MyObjectValidationContributor
: IObjectValidationContributor, ITransientDependency
{
public void AddErrors(ObjectValidationContext context)
{
//Get the validating object
var obj = context.ValidatingObject;
//Add the validation errors if available
context.Errors.Add(...);
}
}
````
* Remember to register your class to the [DI](Dependency-Injection.md) (implementing `ITransientDependency` does it just like in this example)
* ABP will automatically discover your class and use on any type of object validation (including automatic method call validation).
### IMethodInvocationValidator
`IMethodInvocationValidator` is used to validate a method call. It internally uses the `IObjectValidator` to validate objects passes to the method call. You normally don't need to this service since it is automatically used by the framework, but you may want to reuse or replace it on your application in rare cases.
## FluentValidation Integration ## FluentValidation Integration
Volo.Abp.FluentValidation package integrates the FluentValidation library to the validation system. See the [FluentValidation Integration document](FluentValidation.md) for more. Volo.Abp.FluentValidation package integrates the FluentValidation library to the validation system (by implementing the `IObjectValidationContributor`). See the [FluentValidation Integration document](FluentValidation.md) for more.

3
docs/zh-Hans/FluentValidation.md

@ -0,0 +1,3 @@
# FluentValidation 集成
TODO

5
docs/zh-Hans/Logging.md

@ -0,0 +1,5 @@
# 日志
ABP框架没有实现任何日志基础设施. 它使用[ASP.NET Core日志系统](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/logging).
> .NET Core 的日志系统是独立于ASP.NET Core的,它可以在任何类型的应用程序中使用.

164
docs/zh-Hans/Validation.md

@ -1,3 +1,163 @@
## Validation # 验证
待添加 验证系统用于验证对于特定的控制器操作或服务的方法的用户输入或客户端请求.
ABP与ASP.NET Core模型验证系统系统兼容,[模型验证文档](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation)中的内容对于基于ABP应用程序同样有效.所以本文主要集中在ABP特征,而不是重复微软文档.
ABP增加了以下优点:
* 定义 `IValidationEnabled` 向任意类添加自动验证. 所有的[应用服务](Application-Services.md)都实现了该接口,所以它们会被自动验证.
* 自动将数据注解属性的验证错误信息本地化.
* 提供可扩展的服务来验证方法调用或对象的状态。
* 提供[FluentValidation](https://fluentvalidation.net/)的集成.
## 验证DTO
本节简要介绍了验证系统.有关详细信息请参阅[ASP.NET Core验证文档](https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/validation).
### 数据注解 Attribute
使用数据注解是一种以声明式对[DTO](Data-Transfer-Objects.md)进行验证的简单方法.
示例 :
````csharp
public class CreateBookDto
{
[Required]
[StringLength(100)]
public string Name { get; set; }
[Required]
[StringLength(1000)]
public string Description { get; set; }
[Range(0, 999.99)]
public decimal Price { get; set; }
}
````
当使用该类作为[应用服务](Application-Services.md)或控制器的参数时,将对其自动验证并抛出本地化异常(由ABP框架[处理](Exception-Handling.md)).
### IValidatableObject
`IValidatableObject` can be implemented by a DTO to perform custom validation logic. `CreateBookDto` in the following example implements this interface and checks if the `Name` is equals to the `Description` and returns a validation error in this case.
可以将DTO实现 `IValidatableObject` 接口进行自定义验证逻辑. 下面的示例中 `CreateBookDto` 实现了这个接口,并检查 `Name` 是否等于 `Description` 并返回一个验证错误.
````csharp
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Acme.BookStore
{
public class CreateBookDto : IValidatableObject
{
[Required]
[StringLength(100)]
public string Name { get; set; }
[Required]
[StringLength(1000)]
public string Description { get; set; }
[Range(0, 999.99)]
public decimal Price { get; set; }
public IEnumerable<ValidationResult> Validate(
ValidationContext validationContext)
{
if (Name == Description)
{
yield return new ValidationResult(
"Name and Description can not be the same!",
new[] { "Name", "Description" }
);
}
}
}
}
````
#### 解析服务
如果你需要从[依赖注入系统](Dependency-Injection.md)解析服务,可以使用 `ValidationContext` 对象.
例:
````csharp
var myService = validationContext.GetRequiredService<IMyService>();
````
> 虽然可以在 `Validate` 方法中解析服务实现任何可能性,但在DTO中实现领域验证逻辑不是一个很好的做法. 应保持简单的DTO,他们的目的是传输数据(DTO:数据传输对象).
## 验证基础设施
本节介绍了ABP框架提供的一些额外的服务.
### IValidationEnabled 接口
`IValidationEnabled` 是可以由任何类来实现的空标记接口(注册到[DI](Dependency-Injection.md)并从中解析),让ABP框架为该类执行验证系统.
示例 :
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Validation;
namespace Acme.BookStore
{
public class MyService : ITransientDependency, IValidationEnabled
{
public virtual async Task DoItAsync(MyInput input)
{
//...
}
}
}
````
> ABP框架使用[动态代理/拦截](Dynamic-Proxying-Interceptors.md)系统来执行验证.为了使其工作,你的方法应该是 **virtual** 的,服务应该被注入并通过接口(如`IMyService`)使用.
### AbpValidationException
一旦ABP确定了一个验证错误,它就会抛出类型为 `AbpValidationException` 的异常. 你的应用程序代码可以抛出 `AbpValidationException`,但大多数情况不会使用它.
* `ValidationErrors` 是 `AbpValidationException` 的属性,它包含了验证错误列表.
* `AbpValidationException` 的日志级别设置为 `Warning`. 它将所有验证错误记录到[日志系统](Logging.md).
* `AbpValidationException` 由ABP框架自动捕获并将HTTP状态码设置为400转换成可用的错误响应. 参阅[异常处理](Exception-Handling.md)文档了解更多.
## 高级主题
### IObjectValidator
除了自动验证你可能需要手动验证对象,这种情况下[注入](Dependency-Injection.md)并使用 `IObjectValidator` 服务:
* `Validate` 方法根据验证​​规则验证给定对象,如果对象没有被验证通过会抛出 `AbpValidationException` 异常.
* `GetErrors` 不会抛出异常,只返回验证错误.
`IObjectValidator` 默认由 `ObjectValidator` 实现. `ObjectValidator`是可扩展的; 可以实现`IObjectValidationContributor`接口提供自定义逻辑.
示例 :
````csharp
public class MyObjectValidationContributor
: IObjectValidationContributor, ITransientDependency
{
public void AddErrors(ObjectValidationContext context)
{
//Get the validating object
var obj = context.ValidatingObject;
//Add the validation errors if available
context.Errors.Add(...);
}
}
````
* 记录将类注册到[DI](Dependency-Injection.md)(实现`ITransientDependency` 如同本例)
* ABP会自动发现验证类,并用于任何类型的对象验证(包括自动方法调用验证).
### IMethodInvocationValidator
`IMethodInvocationValidator` 用于验证方法调用. 它在内部使用 `IObjectValidator` 来验证传递给方法调用的对象. 由于框架会自动使用此服务,通常你并不需要此服务,但在少数情况下你可能在应用程序中重用或替换它.
## FluentValidation Integration
Volo.Abp.FluentValidation 包将FluentValidation库集成到了验证系统(通过实现 `IObjectValidationContributor`). 请参阅[FluentValidation集成文档](FluentValidation.md)了解更多信息.

9
docs/zh-Hans/docs-nav.json

@ -75,7 +75,14 @@
"path": "Exception-Handling.md" "path": "Exception-Handling.md"
}, },
{ {
"text": "验证" "text": "验证",
"path": "Validation.md",
"items": [
{
"text": "FluentValidation集成",
"path": "FluentValidation.md"
}
]
}, },
{ {
"text": "授权" "text": "授权"

16
framework/Volo.Abp.sln

@ -1,4 +1,4 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16 # Visual Studio Version 16
VisualStudioVersion = 16.0.28922.388 VisualStudioVersion = 16.0.28922.388
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
@ -256,6 +256,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Minify", "src\Volo
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Minify.Tests", "test\Volo.Abp.Minify.Tests\Volo.Abp.Minify.Tests.csproj", "{E69182B3-350A-43F5-A935-5EBBEBECEF97}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Minify.Tests", "test\Volo.Abp.Minify.Tests\Volo.Abp.Minify.Tests.csproj", "{E69182B3-350A-43F5-A935-5EBBEBECEF97}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Serilog", "src\Volo.Abp.AspNetCore.Serilog\Volo.Abp.AspNetCore.Serilog.csproj", "{3B801003-BE74-49ED-9749-DA5E99F45EBF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.Serilog.Tests", "test\Volo.Abp.AspNetCore.Serilog.Tests\Volo.Abp.AspNetCore.Serilog.Tests.csproj", "{9CAA07ED-FE5C-4427-A6FA-6C6CB5B4CC62}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -762,6 +766,14 @@ Global
{E69182B3-350A-43F5-A935-5EBBEBECEF97}.Debug|Any CPU.Build.0 = Debug|Any CPU {E69182B3-350A-43F5-A935-5EBBEBECEF97}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E69182B3-350A-43F5-A935-5EBBEBECEF97}.Release|Any CPU.ActiveCfg = Release|Any CPU {E69182B3-350A-43F5-A935-5EBBEBECEF97}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E69182B3-350A-43F5-A935-5EBBEBECEF97}.Release|Any CPU.Build.0 = Release|Any CPU {E69182B3-350A-43F5-A935-5EBBEBECEF97}.Release|Any CPU.Build.0 = Release|Any CPU
{3B801003-BE74-49ED-9749-DA5E99F45EBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B801003-BE74-49ED-9749-DA5E99F45EBF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B801003-BE74-49ED-9749-DA5E99F45EBF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B801003-BE74-49ED-9749-DA5E99F45EBF}.Release|Any CPU.Build.0 = Release|Any CPU
{9CAA07ED-FE5C-4427-A6FA-6C6CB5B4CC62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9CAA07ED-FE5C-4427-A6FA-6C6CB5B4CC62}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9CAA07ED-FE5C-4427-A6FA-6C6CB5B4CC62}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9CAA07ED-FE5C-4427-A6FA-6C6CB5B4CC62}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -892,6 +904,8 @@ Global
{73559227-EBF0-475F-835B-1FF0CD9132AA} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {73559227-EBF0-475F-835B-1FF0CD9132AA} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{928DC30D-C078-4BB4-A9F8-FE7252C67DC6} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {928DC30D-C078-4BB4-A9F8-FE7252C67DC6} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{E69182B3-350A-43F5-A935-5EBBEBECEF97} = {447C8A77-E5F0-4538-8687-7383196D04EA} {E69182B3-350A-43F5-A935-5EBBEBECEF97} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{3B801003-BE74-49ED-9749-DA5E99F45EBF} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{9CAA07ED-FE5C-4427-A6FA-6C6CB5B4CC62} = {447C8A77-E5F0-4538-8687-7383196D04EA}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5}

1
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/AbpValidationActionFilter.cs

@ -1,7 +1,6 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Volo.Abp.Aspects;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Mvc.Validation namespace Volo.Abp.AspNetCore.Mvc.Validation

13
framework/src/Volo.Abp.AspNetCore.Serilog/Microsoft/AspNetCore/Builder/AbpAspNetCoreSerilogApplicationBuilderExtensions.cs

@ -0,0 +1,13 @@
using Volo.Abp.AspNetCore.Serilog;
namespace Microsoft.AspNetCore.Builder
{
public static class AbpAspNetCoreSerilogApplicationBuilderExtensions
{
public static IApplicationBuilder UseAbpSerilogEnrichers(this IApplicationBuilder app)
{
return app
.UseMiddleware<AbpSerilogMiddleware>();
}
}
}

27
framework/src/Volo.Abp.AspNetCore.Serilog/Properties/launchSettings.json

@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:61851/",
"sslPort": 44301
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Serilog": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

27
framework/src/Volo.Abp.AspNetCore.Serilog/Volo.Abp.AspNetCore.Serilog.csproj

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyName>Volo.Abp.AspNetCore.Serilog</AssemblyName>
<PackageId>Volo.Abp.AspNetCore.Serilog</PackageId>
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<IsPackable>true</IsPackable>
<OutputType>Library</OutputType>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.AspNetCore\Volo.Abp.AspNetCore.csproj" />
<ProjectReference Include="..\Volo.Abp.MultiTenancy\Volo.Abp.MultiTenancy.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="2.8.0" />
</ItemGroup>
</Project>

13
framework/src/Volo.Abp.AspNetCore.Serilog/Volo/Abp/AspNetCore/Serilog/AbpAspNetCoreSerilogModule.cs

@ -0,0 +1,13 @@
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.AspNetCore.Serilog
{
[DependsOn(
typeof(AbpMultiTenancyModule),
typeof(AbpAspNetCoreModule)
)]
public class AbpAspNetCoreSerilogModule : AbpModule
{
}
}

30
framework/src/Volo.Abp.AspNetCore.Serilog/Volo/Abp/AspNetCore/Serilog/AbpAspNetCoreSerilogOptions.cs

@ -0,0 +1,30 @@
namespace Volo.Abp.AspNetCore.Serilog
{
public class AbpAspNetCoreSerilogOptions
{
public AllEnricherPropertyNames EnricherPropertyNames { get; } = new AllEnricherPropertyNames();
public class AllEnricherPropertyNames
{
/// <summary>
/// Default value: "TenantId".
/// </summary>
public string TenantId { get; set; } = "TenantId";
/// <summary>
/// Default value: "UserId".
/// </summary>
public string UserId { get; set; } = "UserId";
/// <summary>
/// Default value: "ClientId".
/// </summary>
public string ClientId { get; set; } = "ClientId";
/// <summary>
/// Default value: "CorrelationId".
/// </summary>
public string CorrelationId { get; set; } = "CorrelationId";
}
}
}

69
framework/src/Volo.Abp.AspNetCore.Serilog/Volo/Abp/AspNetCore/Serilog/AbpSerilogMiddleware.cs

@ -0,0 +1,69 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Serilog.Context;
using Serilog.Core;
using Serilog.Core.Enrichers;
using Volo.Abp.Clients;
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Tracing;
using Volo.Abp.Users;
namespace Volo.Abp.AspNetCore.Serilog
{
public class AbpSerilogMiddleware : IMiddleware, ITransientDependency
{
private readonly ICurrentClient _currentClient;
private readonly ICurrentTenant _currentTenant;
private readonly ICurrentUser _currentUser;
private readonly ICorrelationIdProvider _correlationIdProvider;
private readonly AbpAspNetCoreSerilogOptions _options;
public AbpSerilogMiddleware(
ICurrentTenant currentTenant,
ICurrentUser currentUser,
ICurrentClient currentClient,
ICorrelationIdProvider correlationIdProvider,
IOptions<AbpAspNetCoreSerilogOptions> options)
{
_currentTenant = currentTenant;
_currentUser = currentUser;
_currentClient = currentClient;
_correlationIdProvider = correlationIdProvider;
_options = options.Value;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var enrichers = new List<ILogEventEnricher>();
if (_currentTenant?.Id != null)
{
enrichers.Add(new PropertyEnricher(_options.EnricherPropertyNames.TenantId, _currentTenant.Id));
}
if (_currentUser?.Id != null)
{
enrichers.Add(new PropertyEnricher(_options.EnricherPropertyNames.UserId, _currentUser.Id));
}
if (_currentClient?.Id != null)
{
enrichers.Add(new PropertyEnricher(_options.EnricherPropertyNames.ClientId, _currentClient.Id));
}
var correlationId = _correlationIdProvider.Get();
if (!string.IsNullOrEmpty(correlationId))
{
enrichers.Add(new PropertyEnricher(_options.EnricherPropertyNames.CorrelationId, correlationId));
}
using (LogContext.Push(enrichers.ToArray()))
{
await next(context).ConfigureAwait(false);
}
}
}
}

1
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NuGetPackageTarget.cs

@ -2,6 +2,7 @@
{ {
public enum NuGetPackageTarget : byte public enum NuGetPackageTarget : byte
{ {
Undefined = 0,
DomainShared = 1, DomainShared = 1,
Domain = 2, Domain = 2,
ApplicationContracts = 3, ApplicationContracts = 3,

4
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectFinder.cs

@ -2,11 +2,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
namespace Volo.Abp.Cli.ProjectModification namespace Volo.Abp.Cli.ProjectModification
{ {
public static class ProjectFinder public static class ProjectFinder
{ {
[CanBeNull]
public static string FindNuGetTargetProjectFile(string[] projectFiles, NuGetPackageTarget target) public static string FindNuGetTargetProjectFile(string[] projectFiles, NuGetPackageTarget target)
{ {
if (!projectFiles.Any()) if (!projectFiles.Any())
@ -40,7 +42,7 @@ namespace Volo.Abp.Cli.ProjectModification
case NuGetPackageTarget.HttpApiClient: case NuGetPackageTarget.HttpApiClient:
return FindProjectEndsWith(projectFiles, assemblyNames, ".HttpApi.Client"); return FindProjectEndsWith(projectFiles, assemblyNames, ".HttpApi.Client");
default: default:
throw new ApplicationException($"{nameof(NuGetPackageTarget)}.{target} has not implemented!"); return null;
} }
} }

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs

@ -68,7 +68,7 @@ namespace Volo.Abp.Cli.ProjectModification
var targetProjectFile = ProjectFinder.FindNuGetTargetProjectFile(projectFiles, nugetPackage.Target); var targetProjectFile = ProjectFinder.FindNuGetTargetProjectFile(projectFiles, nugetPackage.Target);
if (targetProjectFile == null) if (targetProjectFile == null)
{ {
Logger.LogDebug($"Target project is not available for NuGet package '{nugetPackage.Name}'"); Logger.LogDebug($"Target project is not available for this NuGet package '{nugetPackage.Name}'");
continue; continue;
} }

4
framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/zh-Hans.json

@ -8,7 +8,7 @@
"The {0} field only accepts files with the following extensions: {1}": "{0}字段只允许以下扩展名的文件: {1}", "The {0} field only accepts files with the following extensions: {1}": "{0}字段只允许以下扩展名的文件: {1}",
"The field {0} must be a string or array type with a maximum length of '{1}'.": "字段{0}必须是最大长度为'{1}'的字符串或数组.", "The field {0} must be a string or array type with a maximum length of '{1}'.": "字段{0}必须是最大长度为'{1}'的字符串或数组.",
"The field {0} must be a string or array type with a minimum length of '{1}'.": "字段{0}必须是最小长度为'{1}'的字符串或数组.", "The field {0} must be a string or array type with a minimum length of '{1}'.": "字段{0}必须是最小长度为'{1}'的字符串或数组.",
"The {0} field is not a valid phone number.": "字段{0}不是有效的身份证号码.", "The {0} field is not a valid phone number.": "字段{0}不是有效的手机号码.",
"The field {0} must be between {1} and {2}.": "字段{0}值必须在{1}和{2}范围内.", "The field {0} must be between {1} and {2}.": "字段{0}值必须在{1}和{2}范围内.",
"The field {0} must match the regular expression '{1}'.": "字段{0}必须匹配正则表达式'{1}'.", "The field {0} must match the regular expression '{1}'.": "字段{0}必须匹配正则表达式'{1}'.",
"The {0} field is required.": "字段{0}不可为空.", "The {0} field is required.": "字段{0}不可为空.",
@ -22,7 +22,7 @@
"ThisFieldOnlyAcceptsFilesWithTheFollowingExtensions:{0}": "字段只允许以下扩展名的文件: {0}", "ThisFieldOnlyAcceptsFilesWithTheFollowingExtensions:{0}": "字段只允许以下扩展名的文件: {0}",
"ThisFieldMustBeAStringOrArrayTypeWithAMaximumLengthoOf{0}": "字段必须是最大长度为'{0}'的字符串或数组.", "ThisFieldMustBeAStringOrArrayTypeWithAMaximumLengthoOf{0}": "字段必须是最大长度为'{0}'的字符串或数组.",
"ThisFieldMustBeAStringOrArrayTypeWithAMinimumLengthOf{0}": "字段必须是最小长度为'{0}'的字符串或数组.", "ThisFieldMustBeAStringOrArrayTypeWithAMinimumLengthOf{0}": "字段必须是最小长度为'{0}'的字符串或数组.",
"ThisFieldIsNotAValidPhoneNumber.": "字段不是有效的身份证号码.", "ThisFieldIsNotAValidPhoneNumber.": "字段不是有效的手机号码.",
"ThisFieldMustBeBetween{0}And{1}": "字段值必须在{0}和{1}范围内.", "ThisFieldMustBeBetween{0}And{1}": "字段值必须在{0}和{1}范围内.",
"ThisFieldMustMatchTheRegularExpression{0}": "字段必须匹配正则表达式'{0}'.", "ThisFieldMustMatchTheRegularExpression{0}": "字段必须匹配正则表达式'{0}'.",
"ThisFieldIsRequired.": "字段不可为空.", "ThisFieldIsRequired.": "字段不可为空.",

2
framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.csproj

@ -9,7 +9,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" /> <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
</ItemGroup> </ItemGroup>

24
framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo.Abp.AspNetCore.Serilog.Tests.csproj

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.test.props" />
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyName>Volo.Abp.AspNetCore.Serilog.Tests</AssemblyName>
<PackageId>Volo.Abp.AspNetCore.Serilog.Tests</PackageId>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Volo.Abp.AspNetCore.MultiTenancy\Volo.Abp.AspNetCore.MultiTenancy.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.AspNetCore.Mvc\Volo.Abp.AspNetCore.Mvc.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.AspNetCore.Serilog\Volo.Abp.AspNetCore.Serilog.csproj" />
<ProjectReference Include="..\Volo.Abp.AspNetCore.Tests\Volo.Abp.AspNetCore.Tests.csproj" />
</ItemGroup>
</Project>

39
framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo/Abp/AspNetCore/App/AbpSerilogTestModule.cs

@ -0,0 +1,39 @@
using Microsoft.AspNetCore.Builder;
using Volo.Abp.AspNetCore.MultiTenancy;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Serilog;
using Volo.Abp.AspNetCore.TestBase;
using Volo.Abp.Autofac;
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.AspNetCore.App
{
[DependsOn(
typeof(AbpAspNetCoreTestBaseModule),
typeof(AbpAspNetCoreMvcModule),
typeof(AbpAspNetCoreMultiTenancyModule),
typeof(AbpAspNetCoreSerilogModule),
typeof(AbpAutofacModule)
)]
public class AbpSerilogTestModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpMultiTenancyOptions>(options => { options.IsEnabled = true; });
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
app.UseCorrelationId();
app.UseRouting();
app.UseAuthorization();
app.UseMultiTenancy();
app.UseAuditing();
app.UseAbpSerilogEnrichers();
app.UseMvcWithDefaultRouteAndArea();
}
}
}

19
framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo/Abp/AspNetCore/App/CollectingSink.cs

@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Linq;
using Serilog.Core;
using Serilog.Events;
namespace Volo.Abp.AspNetCore.App
{
public class CollectingSink : ILogEventSink
{
public List<LogEvent> Events { get; } = new List<LogEvent>();
public LogEvent SingleEvent => Events.Single();
public void Emit(LogEvent logEvent)
{
Events.Add(logEvent);
}
}
}

27
framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo/Abp/AspNetCore/App/SerilogTestController.cs

@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Tracing;
namespace Volo.Abp.AspNetCore.App
{
public class SerilogTestController : AbpController
{
private readonly ICorrelationIdProvider _correlationIdProvider;
public SerilogTestController(ICorrelationIdProvider correlationIdProvider)
{
_correlationIdProvider = correlationIdProvider;
}
public ActionResult Index()
{
return Content("Index-Result");
}
public ActionResult CorrelationId()
{
return Content(_correlationIdProvider.Get());
}
}
}

20
framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo/Abp/AspNetCore/App/Startup.cs

@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Volo.Abp.AspNetCore.App
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddApplication<AbpSerilogTestModule>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env,ILoggerFactory loggerFactory)
{
app.InitializeApplication();
}
}
}

31
framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo/Abp/AspNetCore/Serilog/AbpSerilogTestBase.cs

@ -0,0 +1,31 @@
using System.Linq;
using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Events;
using Volo.Abp.AspNetCore.App;
namespace Volo.Abp.AspNetCore.Serilog
{
public class AbpSerilogTestBase : AbpAspNetCoreTestBase<App.Startup>
{
protected readonly CollectingSink CollectingSink = new CollectingSink();
protected override IHostBuilder CreateHostBuilder()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Sink(CollectingSink)
.CreateLogger();
return base.CreateHostBuilder()
.UseSerilog();
}
protected LogEvent GetLogEvent(string text)
{
return CollectingSink.Events.FirstOrDefault(m => m.MessageTemplate.Text == text);
}
}
}

97
framework/test/Volo.Abp.AspNetCore.Serilog.Tests/Volo/Abp/AspNetCore/Serilog/Serilog_Enrichers_Tests.cs

@ -0,0 +1,97 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Serilog.Events;
using Shouldly;
using Volo.Abp.AspNetCore.App;
using Volo.Abp.AspNetCore.MultiTenancy;
using Volo.Abp.MultiTenancy;
using Volo.Abp.MultiTenancy.ConfigurationStore;
using Xunit;
namespace Volo.Abp.AspNetCore.Serilog
{
public class Serilog_Enrichers_Tests : AbpSerilogTestBase
{
private const string ExecutedEndpointLogEventText = "Executed endpoint '{EndpointName}'";
private readonly Guid _testTenantId = Guid.NewGuid();
private readonly string _testTenantName = "acme";
private readonly AbpAspNetCoreMultiTenancyOptions _tenancyOptions;
private readonly AbpAspNetCoreSerilogOptions _serilogOptions;
private readonly ILogger<Serilog_Enrichers_Tests> _logger;
public Serilog_Enrichers_Tests()
{
_tenancyOptions = ServiceProvider.GetRequiredService<IOptions<AbpAspNetCoreMultiTenancyOptions>>().Value;
_serilogOptions =
ServiceProvider.GetRequiredService<IOptions<AbpAspNetCoreSerilogOptions>>().Value;
_logger = ServiceProvider.GetRequiredService<ILogger<Serilog_Enrichers_Tests>>();
}
protected override IHostBuilder CreateHostBuilder()
{
return base.CreateHostBuilder().ConfigureServices(services =>
{
services.Configure<AbpDefaultTenantStoreOptions>(options =>
{
options.Tenants = new[]
{
new TenantConfiguration(_testTenantId, _testTenantName)
};
});
});
}
[Fact]
public async Task TenantId_Not_Set_Test()
{
var url = GetUrl<SerilogTestController>(nameof(SerilogTestController.Index));
var result = await GetResponseAsStringAsync(url).ConfigureAwait(false);
var executedLogEvent = GetLogEvent(ExecutedEndpointLogEventText);
executedLogEvent.ShouldNotBeNull();
executedLogEvent.Properties.ContainsKey(_serilogOptions.EnricherPropertyNames.TenantId)
.ShouldBe(false);
}
[Fact]
public async Task TenantId_Set_Test()
{
var url =
GetUrl<SerilogTestController>(nameof(SerilogTestController.Index)) +
$"?{_tenancyOptions.TenantKey}={_testTenantName}";
var result = await GetResponseAsStringAsync(url).ConfigureAwait(false);
var executedLogEvent = GetLogEvent(ExecutedEndpointLogEventText);
executedLogEvent.ShouldNotBeNull();
executedLogEvent.Properties.ContainsKey(_serilogOptions.EnricherPropertyNames.TenantId)
.ShouldBe(true);
((ScalarValue) executedLogEvent.Properties[_serilogOptions.EnricherPropertyNames.TenantId]).Value
.ShouldBe(_testTenantId);
}
[Fact]
public async Task CorrelationId_Enrichers_Test()
{
var url = GetUrl<SerilogTestController>(nameof(SerilogTestController.CorrelationId));
var result = await GetResponseAsStringAsync(url).ConfigureAwait(false);
var executedLogEvent = GetLogEvent(ExecutedEndpointLogEventText);
executedLogEvent.ShouldNotBeNull();
executedLogEvent.Properties.ContainsKey(_serilogOptions.EnricherPropertyNames.CorrelationId)
.ShouldNotBeNull();
((ScalarValue) executedLogEvent.Properties[_serilogOptions.EnricherPropertyNames.CorrelationId]).Value
.ShouldBe(result);
}
}
}

2
modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/zh-Hans.json

@ -29,7 +29,7 @@
"DisplayName:Surname": "姓", "DisplayName:Surname": "姓",
"DisplayName:Password": "密码", "DisplayName:Password": "密码",
"DisplayName:EmailAddress": "电子邮件地址", "DisplayName:EmailAddress": "电子邮件地址",
"DisplayName:PhoneNumber": "电话号码", "DisplayName:PhoneNumber": "手机号码",
"PersonalSettings": "个人设置", "PersonalSettings": "个人设置",
"PersonalSettingsSaved": "个人设置已保存", "PersonalSettingsSaved": "个人设置已保存",
"PasswordChanged": "修改密码", "PasswordChanged": "修改密码",

7
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Account.Settings; using Volo.Abp.Account.Settings;
using Volo.Abp.Identity;
using Volo.Abp.Settings; using Volo.Abp.Settings;
using Volo.Abp.Uow; using Volo.Abp.Uow;
using IdentityUser = Volo.Abp.Identity.IdentityUser; using IdentityUser = Volo.Abp.Identity.IdentityUser;
@ -55,16 +56,16 @@ namespace Volo.Abp.Account.Web.Pages.Account
public class PostInput public class PostInput
{ {
[Required] [Required]
[StringLength(32)] [StringLength(IdentityUserConsts.MaxUserNameLength)]
public string UserName { get; set; } public string UserName { get; set; }
[Required] [Required]
[EmailAddress] [EmailAddress]
[StringLength(255)] [StringLength(IdentityUserConsts.MaxEmailLength)]
public string EmailAddress { get; set; } public string EmailAddress { get; set; }
[Required] [Required]
[StringLength(32)] [StringLength(IdentityUserConsts.MaxPasswordLength)]
[DataType(DataType.Password)] [DataType(DataType.Password)]
public string Password { get; set; } public string Password { get; set; }
} }

9
modules/blogging/README.md

@ -1,3 +1,10 @@
# Blogging Module # Blogging Module
This module is used for ABP blog: https://abp.io/blog/abp/ This module adds a simple blog to your ABP application;
* Allows to create multiple blogs in a single application.
* Supports the Markdown format.
* Allows to write comment for a post.
* Allows to assign tags to the blog posts.
See the [blog.abp.io](https://blog.abp.io/) website as a live example of the blogging module.

2
modules/blogging/app/Volo.BloggingTestApp/Volo.BloggingTestApp.csproj

@ -12,7 +12,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0"/> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0"/>

2
modules/client-simulation/demo/Volo.ClientSimulation.Demo/Volo.ClientSimulation.Demo.csproj

@ -8,7 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
</ItemGroup> </ItemGroup>

20
modules/docs/README.md

@ -1,13 +1,15 @@
# Docs Module # Docs Module
This module is used to create technical documentation web sites. [abp.io](https://abp.io) web site uses this module for its documentation. This module is used to create technical documentation web sites;
### Screenshot * Built-in **GitHub integration**: Directly write and manage documents on GitHub.
* **Versioning** support directly integrated to GitHub releases.
* Supports **multi-language** (with fallback support to the default language).
* Supports the **Markdown** and HTML formats.
* Provides a **navigation** and an **outline** section.
* Allows to host **multiple projects** documentation in a single application.
* Links to the file on GitHub, so anyone can easily contribute by clicking to the **Edit link**.
* In addition to the GitHub source, allows to simply use a folder as the documentation source.
![screenshot](screenshot.png) ABP Framework uses this module to host its documentation. See [docs.abp.io](https://docs.abp.io/). A screenshot from the ABP documentation:
### Main Features ![screenshot](screenshot2.png)
* Retrieves documents from a Github repository.
* Supports Markdown document formatting.
* Supports versioning (integrated to Github releases).
* Supports multiple projects.

2
modules/docs/app/VoloDocs.Web/VoloDocs.Web.csproj

@ -14,7 +14,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" />
</ItemGroup> </ItemGroup>

BIN
modules/docs/screenshot.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

BIN
modules/docs/screenshot2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

2
modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/IdentityUserConsts.cs

@ -18,7 +18,7 @@ namespace Volo.Abp.Identity
public const int MaxPhoneNumberLength = AbpUserConsts.MaxPhoneNumberLength; public const int MaxPhoneNumberLength = AbpUserConsts.MaxPhoneNumberLength;
public const int MaxPasswordLength = 32; public const int MaxPasswordLength = 128;
public const int MaxPasswordHashLength = 256; public const int MaxPasswordHashLength = 256;

4
modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/zh-Hans.json

@ -82,7 +82,7 @@
"DisplayName:Abp.Identity.Lockout.LockoutDuration": "锁定时间(秒)", "DisplayName:Abp.Identity.Lockout.LockoutDuration": "锁定时间(秒)",
"DisplayName:Abp.Identity.Lockout.MaxFailedAccessAttempts": "最大失败访问尝试次数", "DisplayName:Abp.Identity.Lockout.MaxFailedAccessAttempts": "最大失败访问尝试次数",
"DisplayName:Abp.Identity.SignIn.RequireConfirmedEmail": "要求验证的电子邮箱", "DisplayName:Abp.Identity.SignIn.RequireConfirmedEmail": "要求验证的电子邮箱",
"DisplayName:Abp.Identity.SignIn.RequireConfirmedPhoneNumber": "要求验证的电话号码", "DisplayName:Abp.Identity.SignIn.RequireConfirmedPhoneNumber": "要求验证的手机号码",
"DisplayName:Abp.Identity.User.IsUserNameUpdateEnabled": "启用用户名更新", "DisplayName:Abp.Identity.User.IsUserNameUpdateEnabled": "启用用户名更新",
"DisplayName:Abp.Identity.User.IsEmailUpdateEnabled": "启用电子邮箱更新", "DisplayName:Abp.Identity.User.IsEmailUpdateEnabled": "启用电子邮箱更新",
"Description:Abp.Identity.Password.RequiredLength": "密码的最小长度.", "Description:Abp.Identity.Password.RequiredLength": "密码的最小长度.",
@ -95,7 +95,7 @@
"Description:Abp.Identity.Lockout.LockoutDuration": "当锁定发生时用户被的锁定的时间(秒).", "Description:Abp.Identity.Lockout.LockoutDuration": "当锁定发生时用户被的锁定的时间(秒).",
"Description:Abp.Identity.Lockout.MaxFailedAccessAttempts": "如果启用锁定, 当用户被锁定前失败的访问尝试次数.", "Description:Abp.Identity.Lockout.MaxFailedAccessAttempts": "如果启用锁定, 当用户被锁定前失败的访问尝试次数.",
"Description:Abp.Identity.SignIn.RequireConfirmedEmail": "登录时是否需要验证的电子邮箱.", "Description:Abp.Identity.SignIn.RequireConfirmedEmail": "登录时是否需要验证的电子邮箱.",
"Description:Abp.Identity.SignIn.RequireConfirmedPhoneNumber": "登录时是否需要验证的电话号码.", "Description:Abp.Identity.SignIn.RequireConfirmedPhoneNumber": "登录时是否需要验证的手机号码.",
"Description:Abp.Identity.User.IsUserNameUpdateEnabled": "是否允许用户更新用户名.", "Description:Abp.Identity.User.IsUserNameUpdateEnabled": "是否允许用户更新用户名.",
"Description:Abp.Identity.User.IsEmailUpdateEnabled": "是否允许用户更新电子邮箱." "Description:Abp.Identity.User.IsEmailUpdateEnabled": "是否允许用户更新电子邮箱."

16
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityRole.cs

@ -100,6 +100,22 @@ namespace Volo.Abp.Identity
Claims.RemoveAll(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value); Claims.RemoveAll(c => c.ClaimType == claim.Type && c.ClaimValue == claim.Value);
} }
public virtual void ChangeName(string name)
{
Check.NotNullOrWhiteSpace(name, nameof(name));
var oldName = Name;
Name = name;
AddLocalEvent(
new IdentityRoleNameChangedEvent
{
IdentityRole = this,
OldName = oldName
}
);
}
public override string ToString() public override string ToString()
{ {
return $"{base.ToString()}, Name = {Name}"; return $"{base.ToString()}, Name = {Name}";

8
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityRoleNameChangedEvent.cs

@ -0,0 +1,8 @@
namespace Volo.Abp.Identity
{
public class IdentityRoleNameChangedEvent
{
public IdentityRole IdentityRole { get; set; }
public string OldName { get; set; }
}
}

2
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityRoleStore.cs

@ -165,7 +165,7 @@ namespace Volo.Abp.Identity
Check.NotNull(role, nameof(role)); Check.NotNull(role, nameof(role));
role.Name = roleName; role.ChangeName(roleName);
return Task.CompletedTask; return Task.CompletedTask;
} }

1
modules/identity/src/Volo.Abp.PermissionManagement.Domain.Identity/Volo.Abp.PermissionManagement.Domain.Identity.csproj

@ -16,6 +16,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Volo.Abp.Identity.Domain.Shared\Volo.Abp.Identity.Domain.Shared.csproj" /> <ProjectReference Include="..\Volo.Abp.Identity.Domain.Shared\Volo.Abp.Identity.Domain.Shared.csproj" />
<ProjectReference Include="..\..\..\permission-management\src\Volo.Abp.PermissionManagement.Domain\Volo.Abp.PermissionManagement.Domain.csproj" /> <ProjectReference Include="..\..\..\permission-management\src\Volo.Abp.PermissionManagement.Domain\Volo.Abp.PermissionManagement.Domain.csproj" />
<ProjectReference Include="..\Volo.Abp.Identity.Domain\Volo.Abp.Identity.Domain.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

43
modules/identity/src/Volo.Abp.PermissionManagement.Domain.Identity/Volo/Abp/PermissionManagement/Identity/RoleUpdateEventHandler.cs

@ -0,0 +1,43 @@
using System.Threading.Tasks;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events;
using Volo.Abp.EventBus;
using Volo.Abp.Identity;
namespace Volo.Abp.PermissionManagement.Identity
{
public class RoleUpdateEventHandler :
ILocalEventHandler<IdentityRoleNameChangedEvent>,
ITransientDependency
{
protected IIdentityRoleRepository RoleRepository { get; }
protected IPermissionManager PermissionManager { get; }
protected IPermissionGrantRepository PermissionGrantRepository { get; }
public RoleUpdateEventHandler(
IIdentityRoleRepository roleRepository,
IPermissionManager permissionManager,
IPermissionGrantRepository permissionGrantRepository)
{
RoleRepository = roleRepository;
PermissionManager = permissionManager;
PermissionGrantRepository = permissionGrantRepository;
}
public async Task HandleEventAsync(IdentityRoleNameChangedEvent eventData)
{
var role = await RoleRepository.FindAsync(eventData.IdentityRole.Id, false).ConfigureAwait(false);
if (role == null)
{
return;
}
var permissionGrantsInRole = await PermissionGrantRepository.GetListAsync(RolePermissionValueProvider.ProviderName, eventData.OldName).ConfigureAwait(false);
foreach (var permissionGrant in permissionGrantsInRole)
{
await PermissionManager.UpdateProviderKeyAsync(permissionGrant, eventData.IdentityRole.Name).ConfigureAwait(false);
}
}
}
}

58
modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/RoleChangingEvents_Test.cs

@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Identity;
using Shouldly;
using System.Threading.Tasks;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Guids;
using Volo.Abp.PermissionManagement;
using Volo.Abp.Uow;
using Xunit;
namespace Volo.Abp.Identity
{
public class RoleChangingEvents_Test : AbpIdentityDomainTestBase
{
protected readonly IIdentityRoleRepository RoleRepository;
protected readonly IPermissionGrantRepository PermissionGrantRepository;
protected readonly IdentityRoleManager RoleManager;
protected readonly ILookupNormalizer LookupNormalizer;
protected readonly IGuidGenerator GuidGenerator;
protected readonly IUnitOfWorkManager UowManager;
public RoleChangingEvents_Test()
{
RoleRepository = GetRequiredService<IIdentityRoleRepository>(); ;
PermissionGrantRepository = GetRequiredService<IPermissionGrantRepository>(); ;
RoleManager = GetRequiredService<IdentityRoleManager>(); ;
LookupNormalizer = GetRequiredService<ILookupNormalizer>(); ;
GuidGenerator = GetRequiredService<IGuidGenerator>();
UowManager = GetRequiredService<IUnitOfWorkManager>();
}
[Fact]
public async Task Role_Update_Event_Test()
{
var role = await RoleRepository
.FindByNormalizedNameAsync(LookupNormalizer.NormalizeName("moderator"))
.ConfigureAwait(false);
var permissionGrantsInRole = await PermissionGrantRepository.GetListAsync("R", role.Name).ConfigureAwait(false);
permissionGrantsInRole.ShouldNotBeNull();
permissionGrantsInRole.Count.ShouldBeGreaterThan(0);
var count = permissionGrantsInRole.Count;
using (var uow = UowManager.Begin())
{
var identityResult = await RoleManager.SetRoleNameAsync(role, "TestModerator").ConfigureAwait(false);
identityResult.Succeeded.ShouldBeTrue();
var xx = await RoleRepository.UpdateAsync(role).ConfigureAwait(false);
await uow.CompleteAsync().ConfigureAwait(false);
}
role = await RoleRepository.GetAsync(role.Id).ConfigureAwait(false);
role.Name.ShouldBe("TestModerator");
permissionGrantsInRole = await PermissionGrantRepository.GetListAsync("R", role.Name).ConfigureAwait(false);
permissionGrantsInRole.Count.ShouldBe(count);
}
}
}

4
modules/permission-management/Volo.Abp.PermissionManagement.sln

@ -19,7 +19,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.PermissionManageme
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.PermissionManagement.Web", "src\Volo.Abp.PermissionManagement.Web\Volo.Abp.PermissionManagement.Web.csproj", "{97A386F8-DAE0-4BEA-ADE5-1D57C3336515}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.PermissionManagement.Web", "src\Volo.Abp.PermissionManagement.Web\Volo.Abp.PermissionManagement.Web.csproj", "{97A386F8-DAE0-4BEA-ADE5-1D57C3336515}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.PermissionManagement.Tests", "test\Volo.Abp.PermissionManagement.Tests\Volo.Abp.PermissionManagement.Tests.csproj", "{48297098-79D0-413B-939C-6C432142C42D}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.PermissionManagement.Domain.Tests", "test\Volo.Abp.PermissionManagement.Tests\Volo.Abp.PermissionManagement.Domain.Tests.csproj", "{48297098-79D0-413B-939C-6C432142C42D}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.PermissionManagement.TestBase", "test\Volo.Abp.PermissionManagement.TestBase\Volo.Abp.PermissionManagement.TestBase.csproj", "{49259427-CAEB-4FAE-81E4-848F789487EA}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.PermissionManagement.TestBase", "test\Volo.Abp.PermissionManagement.TestBase\Volo.Abp.PermissionManagement.TestBase.csproj", "{49259427-CAEB-4FAE-81E4-848F789487EA}"
EndProject EndProject
@ -33,7 +33,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.PermissionManageme
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.PermissionManagement.HttpApi.Client", "src\Volo.Abp.PermissionManagement.HttpApi.Client\Volo.Abp.PermissionManagement.HttpApi.Client.csproj", "{1CD80519-9431-48DB-B0EA-291A73FF9F49}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.PermissionManagement.HttpApi.Client", "src\Volo.Abp.PermissionManagement.HttpApi.Client\Volo.Abp.PermissionManagement.HttpApi.Client.csproj", "{1CD80519-9431-48DB-B0EA-291A73FF9F49}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.PermissionManagement.Application.Tests", "test\Volo.Abp.PermissionManagement.Application.Tests\Volo.Abp.PermissionManagement.Application.Tests.csproj", "{A0F72F5F-3713-4E06-ADB7-15ADFDCB79B1}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.PermissionManagement.Application.Tests", "test\Volo.Abp.PermissionManagement.Application.Tests\Volo.Abp.PermissionManagement.Application.Tests.csproj", "{A0F72F5F-3713-4E06-ADB7-15ADFDCB79B1}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution

2
modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/IPermissionManager.cs

@ -13,5 +13,7 @@ namespace Volo.Abp.PermissionManagement
Task<List<PermissionWithGrantedProviders>> GetAllAsync([NotNull] string providerName, [NotNull] string providerKey); Task<List<PermissionWithGrantedProviders>> GetAllAsync([NotNull] string providerName, [NotNull] string providerKey);
Task SetAsync(string permissionName, string providerName, string providerKey, bool isGranted); Task SetAsync(string permissionName, string providerName, string providerKey, bool isGranted);
Task<PermissionGrant> UpdateProviderKeyAsync(PermissionGrant permissionGrant, string providerKey);
} }
} }

2
modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionGrant.cs

@ -17,7 +17,7 @@ namespace Volo.Abp.PermissionManagement
public virtual string ProviderName { get; protected set; } public virtual string ProviderName { get; protected set; }
[CanBeNull] [CanBeNull]
public virtual string ProviderKey { get; protected set; } public virtual string ProviderKey { get; protected internal set; }
protected PermissionGrant() protected PermissionGrant()
{ {

6
modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionManager.cs

@ -99,6 +99,12 @@ namespace Volo.Abp.PermissionManagement
await provider.SetAsync(permissionName, providerKey, isGranted).ConfigureAwait(false); await provider.SetAsync(permissionName, providerKey, isGranted).ConfigureAwait(false);
} }
public async Task<PermissionGrant> UpdateProviderKeyAsync(PermissionGrant permissionGrant, string providerKey)
{
permissionGrant.ProviderKey = providerKey;
return await PermissionGrantRepository.UpdateAsync(permissionGrant).ConfigureAwait(false);
}
protected virtual async Task<PermissionWithGrantedProviders> GetInternalAsync(PermissionDefinition permission, string providerName, string providerKey) protected virtual async Task<PermissionWithGrantedProviders> GetInternalAsync(PermissionDefinition permission, string providerName, string providerKey)
{ {
var result = new PermissionWithGrantedProviders(permission.Name, false); var result = new PermissionWithGrantedProviders(permission.Name, false);

2
modules/permission-management/test/Volo.Abp.PermissionManagement.Application.Tests/Volo.Abp.PermissionManagement.Application.Tests.csproj

@ -10,7 +10,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Volo.Abp.PermissionManagement.Application\Volo.Abp.PermissionManagement.Application.csproj" /> <ProjectReference Include="..\..\src\Volo.Abp.PermissionManagement.Application\Volo.Abp.PermissionManagement.Application.csproj" />
<ProjectReference Include="..\Volo.Abp.PermissionManagement.Tests\Volo.Abp.PermissionManagement.Tests.csproj" /> <ProjectReference Include="..\Volo.Abp.PermissionManagement.Tests\Volo.Abp.PermissionManagement.Domain.Tests.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

0
modules/permission-management/test/Volo.Abp.PermissionManagement.Tests/Volo.Abp.PermissionManagement.Tests.csproj → modules/permission-management/test/Volo.Abp.PermissionManagement.Tests/Volo.Abp.PermissionManagement.Domain.Tests.csproj

15
modules/permission-management/test/Volo.Abp.PermissionManagement.Tests/Volo/Abp/PermissionManagement/PermissionManager_Tests.cs

@ -104,5 +104,20 @@ namespace Volo.Abp.PermissionManagement
true).ConfigureAwait(false)).ConfigureAwait(false); true).ConfigureAwait(false)).ConfigureAwait(false);
} }
[Fact]
public async Task UpdateProviderKey()
{
await _permissionGrantRepository.InsertAsync(new PermissionGrant(
Guid.NewGuid(),
"MyPermission1",
"Test",
"Test")
).ConfigureAwait(false);
var permissionGrant = await _permissionGrantRepository.FindAsync("MyPermission1", "Test", "Test").ConfigureAwait(false);
permissionGrant.ProviderKey.ShouldBe("Test");
await _permissionManager.UpdateProviderKeyAsync(permissionGrant, "NewProviderKey").ConfigureAwait(false);
(await _permissionGrantRepository.FindAsync("MyPermission1", "Test", "NewProviderKey").ConfigureAwait(false)).ShouldNotBeNull();
}
} }
} }

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

@ -1,5 +1,8 @@
import { ReplaceableComponents } from '../models/replaceable-components'; import { ReplaceableComponents } from '../models/replaceable-components';
/**
* @see usage: https://github.com/abpframework/abp/pull/2522#issue-358333183
*/
export class AddReplaceableComponent { export class AddReplaceableComponent {
static readonly type = '[ReplaceableComponents] Add'; static readonly type = '[ReplaceableComponents] Add';
constructor(public payload: ReplaceableComponents.ReplaceableComponent) {} constructor(public payload: ReplaceableComponents.ReplaceableComponent) {}

3
npm/ng-packs/packages/core/src/lib/states/config.state.ts

@ -137,6 +137,7 @@ export class ConfigState {
key: string | Config.LocalizationWithDefault, key: string | Config.LocalizationWithDefault,
...interpolateParams: string[] ...interpolateParams: string[]
) { ) {
if (!key) key = '';
let defaultValue: string; let defaultValue: string;
if (typeof key !== 'string') { if (typeof key !== 'string') {
@ -144,8 +145,6 @@ export class ConfigState {
key = key.key; key = key.key;
} }
if (!key) key = '';
const keys = key.split('::') as string[]; const keys = key.split('::') as string[];
const selector = createSelector([ConfigState], (state: Config.State) => { const selector = createSelector([ConfigState], (state: Config.State) => {
if (!state.localization) return defaultValue || key; if (!state.localization) return defaultValue || key;

2
npm/ng-packs/packages/core/src/lib/states/session.state.ts

@ -67,7 +67,7 @@ export class SessionState {
this.store.dispatch(new ModifyOpenedTabCount('increase')); this.store.dispatch(new ModifyOpenedTabCount('increase'));
fromEvent(window, 'beforeunload').subscribe(() => { fromEvent(window, 'unload').subscribe(event => {
this.store.dispatch(new ModifyOpenedTabCount('decrease')); this.store.dispatch(new ModifyOpenedTabCount('decrease'));
}); });
}); });

2
npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/account-layout.component.ts

@ -6,7 +6,7 @@ import { eLayoutType } from '@abp/ng.core';
template: ` template: `
<router-outlet></router-outlet> <router-outlet></router-outlet>
<abp-confirmation></abp-confirmation> <abp-confirmation></abp-confirmation>
<abp-toast></abp-toast> <abp-toast-container right="30px" bottom="30px"></abp-toast-container>
`, `,
}) })
export class AccountLayoutComponent { export class AccountLayoutComponent {

29
npm/ng-packs/packages/theme-basic/src/lib/components/application-layout/application-layout.component.html

@ -34,7 +34,8 @@
<ng-template #defaultLink let-route> <ng-template #defaultLink let-route>
<li class="nav-item" *abpPermission="route.requiredPolicy"> <li class="nav-item" *abpPermission="route.requiredPolicy">
<a class="nav-link" [routerLink]="[route.url]" <a class="nav-link" [routerLink]="[route.url]"
><i *ngIf="route.iconClass" [ngClass]="route.iconClass"></i> {{ route.name | abpLocalization }}</a ><i *ngIf="route.iconClass" [ngClass]="route.iconClass"></i>
{{ route.name | abpLocalization }}</a
> >
</li> </li>
</ng-template> </ng-template>
@ -47,7 +48,9 @@
class="nav-item dropdown" class="nav-item dropdown"
display="static" display="static"
(click)=" (click)="
navbarRootDropdown.expand ? (navbarRootDropdown.expand = false) : (navbarRootDropdown.expand = true) navbarRootDropdown.expand
? (navbarRootDropdown.expand = false)
: (navbarRootDropdown.expand = true)
" "
> >
<a <a
@ -57,7 +60,8 @@
aria-expanded="false" aria-expanded="false"
href="javascript:void(0)" href="javascript:void(0)"
> >
<i *ngIf="route.iconClass" [ngClass]="route.iconClass"></i> {{ route.name | abpLocalization }} <i *ngIf="route.iconClass" [ngClass]="route.iconClass"></i>
{{ route.name | abpLocalization }}
</a> </a>
<div <div
#routeContainer #routeContainer
@ -146,12 +150,15 @@
</div> </div>
</nav> </nav>
<div [@slideFromBottom]="outlet && outlet.activatedRoute && outlet.activatedRoute.routeConfig.path" class="container"> <div
[@slideFromBottom]="outlet && outlet.activatedRoute && outlet.activatedRoute.routeConfig.path"
class="container"
>
<router-outlet #outlet="outlet"></router-outlet> <router-outlet #outlet="outlet"></router-outlet>
</div> </div>
<abp-confirmation></abp-confirmation> <abp-confirmation></abp-confirmation>
<abp-toast></abp-toast> <abp-toast-container right="30px" bottom="30px"></abp-toast-container>
<ng-template #appName> <ng-template #appName>
{{ appInfo.name }} {{ appInfo.name }}
@ -213,12 +220,12 @@
[class.d-block]="smallScreen" [class.d-block]="smallScreen"
[class.abp-mh-25]="smallScreen && currentUserDropdown.isOpen()" [class.abp-mh-25]="smallScreen && currentUserDropdown.isOpen()"
> >
<a class="dropdown-item" routerLink="/account/manage-profile"><i class="fa fa-cog mr-1"></i>{{ <a class="dropdown-item" routerLink="/account/manage-profile"
'AbpAccount::ManageYourProfile' | abpLocalization ><i class="fa fa-cog mr-1"></i>{{ 'AbpAccount::ManageYourProfile' | abpLocalization }}</a
}}</a> >
<a class="dropdown-item" href="javascript:void(0)" (click)="logout()"><i class="fa fa-power-off mr-1"></i>{{ <a class="dropdown-item" href="javascript:void(0)" (click)="logout()"
'AbpUi::Logout' | abpLocalization ><i class="fa fa-power-off mr-1"></i>{{ 'AbpUi::Logout' | abpLocalization }}</a
}}</a> >
</div> </div>
</div> </div>
</li> </li>

2
npm/ng-packs/packages/theme-basic/src/lib/components/empty-layout/empty-layout.component.ts

@ -6,7 +6,7 @@ import { eLayoutType } from '@abp/ng.core';
template: ` template: `
<router-outlet></router-outlet> <router-outlet></router-outlet>
<abp-confirmation></abp-confirmation> <abp-confirmation></abp-confirmation>
<abp-toast></abp-toast> <abp-toast-container right="30px" bottom="30px"></abp-toast-container>
`, `,
}) })
export class EmptyLayoutComponent { export class EmptyLayoutComponent {

6
npm/ng-packs/packages/theme-basic/src/lib/constants/styles.ts

@ -48,12 +48,6 @@ export default `
.container > .card { .container > .card {
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;
} }
.abp-confirm .abp-confirm-footer {
background-color: #f4f4f7 !important;
}
.abp-confirm .ui-toast-message-content {
background-color: #fff !important;
}
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
.navbar .dropdown:hover > .dropdown-menu { .navbar .dropdown:hover > .dropdown-menu {

56
npm/ng-packs/packages/theme-shared/src/lib/abstracts/toaster.ts

@ -1,56 +0,0 @@
import { MessageService } from 'primeng/components/common/messageservice';
import { Observable, Subject } from 'rxjs';
import { Toaster } from '../models/toaster';
import { Config } from '@abp/ng.core';
export abstract class AbstractToaster<T = Toaster.Options> {
status$: Subject<Toaster.Status>;
key = 'abpToast';
sticky = false;
constructor(protected messageService: MessageService) {}
info(message: Config.LocalizationParam, title: Config.LocalizationParam, options?: T): Observable<Toaster.Status> {
return this.show(message, title, 'info', options);
}
success(message: Config.LocalizationParam, title: Config.LocalizationParam, options?: T): Observable<Toaster.Status> {
return this.show(message, title, 'success', options);
}
warn(message: Config.LocalizationParam, title: Config.LocalizationParam, options?: T): Observable<Toaster.Status> {
return this.show(message, title, 'warn', options);
}
error(message: Config.LocalizationParam, title: Config.LocalizationParam, options?: T): Observable<Toaster.Status> {
return this.show(message, title, 'error', options);
}
protected show(
message: Config.LocalizationParam,
title: Config.LocalizationParam,
severity: Toaster.Severity,
options?: T,
): Observable<Toaster.Status> {
this.messageService.clear(this.key);
this.messageService.add({
severity,
detail: message || '',
summary: title || '',
...options,
key: this.key,
...(typeof (options || ({} as any)).sticky === 'undefined' && { sticky: this.sticky }),
});
this.status$ = new Subject<Toaster.Status>();
return this.status$;
}
clear(status?: Toaster.Status) {
this.messageService.clear(this.key);
this.status$.next(status || Toaster.Status.dismiss);
this.status$.complete();
}
}

1
npm/ng-packs/packages/theme-shared/src/lib/animations/index.ts

@ -3,3 +3,4 @@ export * from './collapse.animations';
export * from './fade.animations'; export * from './fade.animations';
export * from './modal.animations'; export * from './modal.animations';
export * from './slide.animations'; export * from './slide.animations';
export * from './toast.animations';

17
npm/ng-packs/packages/theme-shared/src/lib/animations/toast.animations.ts

@ -0,0 +1,17 @@
import { animate, query, style, transition, trigger } from '@angular/animations';
export const toastInOut = trigger('toastInOut', [
transition('* <=> *', [
query(
':enter',
[
style({ opacity: 0, transform: 'translateY(20px)' }),
animate('350ms ease', style({ opacity: 1, transform: 'translateY(0)' })),
],
{ optional: true },
),
query(':leave', animate('450ms ease', style({ opacity: 0 })), {
optional: true,
}),
]),
]);

34
npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.html

@ -0,0 +1,34 @@
<div class="confirmation show" *ngIf="visible">
<div class="confirmation-backdrop"></div>
<div class="confirmation-dialog">
<div class="icon-container" [ngClass]="data.severity" *ngIf="data.severity">
<i class="fa icon" [ngClass]="iconClass"></i>
</div>
<div class="content">
<h1 class="title" *ngIf="data.title">
{{ data.title | abpLocalization: data.options?.titleLocalizationParams }}
</h1>
<p class="message" *ngIf="data.message">
{{ data.message | abpLocalization: data.options?.messageLocalizationParams }}
</p>
</div>
<div class="footer">
<button
id="cancel"
class="confirmation-button confirmation-button-reject"
*ngIf="!data?.options?.hideCancelBtn"
(click)="close(reject)"
>
{{ data.options?.cancelText || 'AbpUi::Cancel' | abpLocalization }}
</button>
<button
id="confirm"
class="confirmation-button confirmation-button-approve"
*ngIf="!data?.options?.hideYesBtn"
(click)="close(confirm)"
>
{{ data.options?.yesText || 'AbpUi::Yes' | abpLocalization }}
</button>
</div>
</div>
</div>

120
npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.scss

@ -0,0 +1,120 @@
.confirmation {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: none;
place-items: center;
z-index: 1060;
&.show {
display: grid;
}
.confirmation-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(#000, 0.7);
z-index: 1061 !important;
}
.confirmation-dialog {
display: flex;
flex-direction: column;
margin: 20px auto;
padding: 0;
border: none;
border-radius: 10px;
width: 450px;
min-height: 300px;
background-color: #fff;
box-shadow: 0 0 10px -5px rgba(#000, 0.5);
z-index: 1062 !important;
.icon-container {
display: flex;
align-items: center;
justify-content: center;
margin: 0 0 10px 0;
padding: 20px;
.icon {
width: 100px;
height: 100px;
stroke-width: 1;
fill: #fff;
font-size: 80px;
text-align: center;
}
&.neutral .icon {
}
&.info .icon {
stroke: #2f96b4;
color: #2f96b4;
}
&.success .icon {
stroke: #51a351;
color: #51a351;
}
&.warning .icon {
stroke: #f89406;
color: #f89406;
}
&.error .icon {
stroke: #bd362f;
color: #bd362f;
}
}
.content {
flex-grow: 1;
display: block;
.title {
display: block;
margin: 0;
padding: 0;
font-size: 27px;
font-weight: 600;
text-align: center;
}
.message {
display: block;
margin: 10px auto;
padding: 20px;
color: #777;
font-size: 16px;
font-weight: 400;
text-align: center;
}
}
.footer {
display: flex;
align-items: center;
justify-content: flex-end;
margin: 10px 0 0 0;
padding: 20px;
width: 100%;
.confirmation-button {
display: inline-block;
margin: 0px 5px;
padding: 10px 20px;
border: none;
border-radius: 6px;
color: #777;
font-size: 14px;
font-weight: 600;
background-color: #eee;
&:hover {
background-color: darken(#eee, 5);
}
&-reject {
}
&-approve {
background-color: #2f96b4;
color: #fff;
&:hover {
background-color: darken(#2f96b4, 5);
}
}
}
}
}
}

76
npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.ts

@ -1,59 +1,47 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { ConfirmationService } from '../../services/confirmation.service'; import { ConfirmationService } from '../../services/confirmation.service';
import { Confirmation } from '../../models/confirmation';
import { LocalizationService } from '@abp/ng.core';
import { Toaster } from '../../models/toaster'; import { Toaster } from '../../models/toaster';
@Component({ @Component({
selector: 'abp-confirmation', selector: 'abp-confirmation',
// tslint:disable-next-line: component-max-inline-declarations templateUrl: './confirmation.component.html',
template: ` styleUrls: ['./confirmation.component.scss'],
<p-toast
position="center"
key="abpConfirmation"
(onClose)="close(dismiss)"
[modal]="true"
[baseZIndex]="1000"
styleClass="abp-confirm"
>
<ng-template let-message pTemplate="message">
<i class="fa fa-exclamation-circle abp-confirm-icon"></i>
<div *ngIf="message.summary" class="abp-confirm-summary">
{{ message.summary | abpLocalization: message.titleLocalizationParams }}
</div>
<div class="abp-confirm-body">
{{ message.detail | abpLocalization: message.messageLocalizationParams }}
</div>
<div class="abp-confirm-footer justify-content-center">
<button
*ngIf="!message.hideCancelBtn"
id="cancel"
type="button"
class="btn btn-sm btn-primary"
(click)="close(reject)"
>
{{ message.cancelText || message.cancelCopy || 'AbpIdentity::Cancel' | abpLocalization }}
</button>
<button
*ngIf="!message.hideYesBtn"
id="confirm"
type="button"
class="btn btn-sm btn-primary"
(click)="close(confirm)"
autofocus
>
<span>{{ message.yesText || message.yesCopy || 'AbpIdentity::Yes' | abpLocalization }}</span>
</button>
</div>
</ng-template>
</p-toast>
`,
}) })
export class ConfirmationComponent { export class ConfirmationComponent {
confirm = Toaster.Status.confirm; confirm = Toaster.Status.confirm;
reject = Toaster.Status.reject; reject = Toaster.Status.reject;
dismiss = Toaster.Status.dismiss; dismiss = Toaster.Status.dismiss;
constructor(private confirmationService: ConfirmationService) {} visible = false;
data: Confirmation.DialogData;
get iconClass(): string {
switch (this.data.severity) {
case 'info':
return 'fa-info-circle';
case 'success':
return 'fa-check-circle';
case 'warning':
return 'fa-exclamation-triangle';
case 'error':
return 'fa-times-circle';
default:
return 'fa-question-circle';
}
}
constructor(
private confirmationService: ConfirmationService,
private localizationService: LocalizationService,
) {
this.confirmationService.confirmation$.subscribe(confirmation => {
this.data = confirmation;
this.visible = !!confirmation;
});
}
close(status: Toaster.Status) { close(status: Toaster.Status) {
this.confirmationService.clear(status); this.confirmationService.clear(status);

25
npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.scss

@ -0,0 +1,25 @@
.modal {
&.show {
display: block !important;
}
&-backdrop {
background-color: rgba(0, 0, 0, 0.6);
}
&::-webkit-scrollbar {
width: 7px;
}
&::-webkit-scrollbar-track {
background: #ddd;
}
&::-webkit-scrollbar-thumb {
background: #8a8686;
}
&-dialog {
z-index: 1050;
}
}

46
npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts

@ -24,6 +24,7 @@ export type ModalSize = 'sm' | 'md' | 'lg' | 'xl';
selector: 'abp-modal', selector: 'abp-modal',
templateUrl: './modal.component.html', templateUrl: './modal.component.html',
animations: [fadeAnimation], animations: [fadeAnimation],
styleUrls: ['./modal.component.scss'],
}) })
export class ModalComponent implements OnDestroy { export class ModalComponent implements OnDestroy {
@Input() @Input()
@ -100,6 +101,20 @@ export class ModalComponent implements OnDestroy {
destroy$ = new Subject<void>(); destroy$ = new Subject<void>();
get isFormDirty(): boolean {
let node: HTMLDivElement;
if (!this.modalContent) {
node = document.getElementById('modal-container') as HTMLDivElement;
}
const nodes = getFlatNodes(
((node || this.modalContent.nativeElement).querySelector('#abp-modal-body') as HTMLElement)
.childNodes,
);
return hasNgDirty(nodes);
}
constructor(private renderer: Renderer2, private confirmationService: ConfirmationService) {} constructor(private renderer: Renderer2, private confirmationService: ConfirmationService) {}
ngOnDestroy(): void { ngOnDestroy(): void {
@ -109,21 +124,15 @@ export class ModalComponent implements OnDestroy {
close() { close() {
if (this.busy) return; if (this.busy) return;
let node: HTMLDivElement; if (this.isFormDirty) {
if (!this.modalContent) {
node = document.getElementById('modal-container') as HTMLDivElement;
}
const nodes = getFlatNodes(
((node || this.modalContent.nativeElement).querySelector('#abp-modal-body') as HTMLElement).childNodes,
);
if (hasNgDirty(nodes)) {
if (this.isConfirmationOpen) return; if (this.isConfirmationOpen) return;
this.isConfirmationOpen = true; this.isConfirmationOpen = true;
this.confirmationService this.confirmationService
.warn('AbpAccount::AreYouSureYouWantToCancelEditingWarningMessage', 'AbpAccount::AreYouSure') .warn(
'AbpAccount::AreYouSureYouWantToCancelEditingWarningMessage',
'AbpAccount::AreYouSure',
)
.subscribe((status: Toaster.Status) => { .subscribe((status: Toaster.Status) => {
this.isConfirmationOpen = false; this.isConfirmationOpen = false;
if (status === Toaster.Status.confirm) { if (status === Toaster.Status.confirm) {
@ -146,6 +155,16 @@ export class ModalComponent implements OnDestroy {
this.close(); this.close();
}); });
fromEvent(window, 'beforeunload')
.pipe(takeUntil(this.destroy$))
.subscribe(event => {
if (this.isFormDirty) {
event.returnValue = true;
} else {
delete event.returnValue;
}
});
setTimeout(() => { setTimeout(() => {
if (!this.abpClose) return; if (!this.abpClose) return;
fromEvent(this.abpClose.nativeElement, 'click') fromEvent(this.abpClose.nativeElement, 'click')
@ -162,7 +181,10 @@ export class ModalComponent implements OnDestroy {
function getFlatNodes(nodes: NodeList): HTMLElement[] { function getFlatNodes(nodes: NodeList): HTMLElement[] {
return Array.from(nodes).reduce( return Array.from(nodes).reduce(
(acc, val) => [...acc, ...(val.childNodes && val.childNodes.length ? getFlatNodes(val.childNodes) : [val])], (acc, val) => [
...acc,
...(val.childNodes && val.childNodes.length ? getFlatNodes(val.childNodes) : [val]),
],
[], [],
); );
} }

11
npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.html

@ -0,0 +1,11 @@
<div
class="toast-container"
[style.top]="top || 'auto'"
[style.right]="right || 'auto'"
[style.bottom]="bottom || 'auto'"
[style.left]="left || 'auto'"
[style.display]="toasts.length ? 'flex' : 'none'"
[@toastInOut]="toasts.length"
>
<abp-toast [toast]="toast" *ngFor="let toast of toasts; trackBy: trackByFunc"></abp-toast>
</div>

13
npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.scss

@ -0,0 +1,13 @@
.toast-container {
position: fixed;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
min-width: 350px;
min-height: 80px;
z-index: 1900;
&.new-on-top {
flex-direction: column-reverse;
}
}

46
npm/ng-packs/packages/theme-shared/src/lib/components/toast-container/toast-container.component.ts

@ -0,0 +1,46 @@
import { Component, Input, OnInit } from '@angular/core';
import { Toaster } from '../../models/toaster';
import { toastInOut } from '../../animations/toast.animations';
import { ToasterService } from '../../services/toaster.service';
@Component({
selector: 'abp-toast-container',
templateUrl: './toast-container.component.html',
styleUrls: ['./toast-container.component.scss'],
animations: [toastInOut],
})
export class ToastContainerComponent implements OnInit {
toasts = [] as Toaster.Toast[];
@Input()
top: string;
@Input()
right = '30px';
@Input()
bottom = '30px';
@Input()
left: string;
@Input()
toastKey: string;
constructor(private toastService: ToasterService) {}
ngOnInit() {
this.toastService.toasts$.subscribe(toasts => {
this.toasts = this.toastKey
? toasts.filter(t => {
return t.options && t.options.containerKey !== this.toastKey;
})
: toasts;
});
}
trackByFunc(index, toast) {
if (!toast) return null;
return toast.options.id;
}
}

16
npm/ng-packs/packages/theme-shared/src/lib/components/toast/toast.component.html

@ -0,0 +1,16 @@
<div class="toast" [ngClass]="severityClass" (click)="tap()">
<div class="toast-icon">
<i class="fa icon" [ngClass]="iconClass"></i>
</div>
<div class="toast-content">
<button class="close-button" (click)="close()" *ngIf="toast.options.closable">
<i class="fa fa-times"></i>
</button>
<div class="toast-title">
{{ toast.title | abpLocalization: toast.options?.titleLocalizationParams }}
</div>
<div class="toast-message">
{{ toast.message | abpLocalization: toast.options?.messageLocalizationParams }}
</div>
</div>
</div>

80
npm/ng-packs/packages/theme-shared/src/lib/components/toast/toast.component.scss

@ -0,0 +1,80 @@
@mixin fillColor($background, $color) {
border: 2px solid $background;
background-color: $background;
color: $color;
box-shadow: 0 0 10px -5px rgba(#000, 0.4);
&:hover {
border: 2px solid darken($background, 5);
background-color: darken($background, 5);
box-shadow: 0 0 15px -5px rgba(#000, 0.4);
}
}
.toast {
display: grid;
grid-template-columns: 50px 1fr;
gap: 10px;
margin: 5px 0;
padding: 10px;
border-radius: 0px;
width: 350px;
user-select: none;
box-shadow: 0 0 10px -5px rgba(#000, 0.4);
z-index: 9999;
@include fillColor(#f0f0f0, #000);
opacity: 1;
&.toast-success {
@include fillColor(#51a351, #fff);
}
&.toast-info {
@include fillColor(#2f96b4, #fff);
}
&.toast-warning {
@include fillColor(#f89406, #fff);
}
&.toast-error {
@include fillColor(#bd362f, #fff);
}
.toast-icon {
display: flex;
align-items: center;
justify-content: center;
.icon {
font-size: 36px;
}
}
.toast-content {
position: relative;
.close-button {
position: absolute;
top: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
padding: 5px 10px 5px 5px;
width: 25px;
height: 25px;
border: none;
border-radius: 50%;
background: transparent;
&:focus {
outline: none;
}
.close-icon {
width: 16px;
height: 16px;
stroke: #000;
}
}
.toast-title {
margin: 0;
padding: 0;
font-size: 1rem;
font-weight: 600;
}
.toast-message {
}
}
}

74
npm/ng-packs/packages/theme-shared/src/lib/components/toast/toast.component.ts

@ -1,26 +1,56 @@
import { Component } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { Toaster } from '../../models/toaster';
import { ToasterService } from '../../services/toaster.service';
import { LocalizationService } from '@abp/ng.core';
import snq from 'snq';
@Component({ @Component({
selector: 'abp-toast', selector: 'abp-toast',
// tslint:disable-next-line: component-max-inline-declarations templateUrl: './toast.component.html',
template: ` styleUrls: ['./toast.component.scss'],
<p-toast position="bottom-right" key="abpToast" styleClass="abp-toast" [baseZIndex]="1000">
<ng-template let-message pTemplate="message">
<span
class="ui-toast-icon pi"
[ngClass]="{
'pi-info-circle': message.severity === 'info',
'pi-exclamation-triangle': message.severity === 'warn',
'pi-times': message.severity === 'error',
'pi-check': message.severity === 'success'
}"
></span>
<div class="ui-toast-message-text-content">
<div class="ui-toast-summary">{{ message.summary | abpLocalization: message.titleLocalizationParams }}</div>
<div class="ui-toast-detail">{{ message.detail | abpLocalization: message.messageLocalizationParams }}</div>
</div>
</ng-template>
</p-toast>
`,
}) })
export class ToastComponent {} export class ToastComponent implements OnInit {
@Input()
toast: Toaster.Toast;
get severityClass(): string {
if (!this.toast || !this.toast.severity) return '';
return `toast-${this.toast.severity}`;
}
get iconClass(): string {
switch (this.toast.severity) {
case 'success':
return 'fa-check-circle';
case 'info':
return 'fa-info-circle';
case 'warning':
return 'fa-exclamation-triangle';
case 'error':
return 'fa-times-circle';
default:
return 'fa-exclamation-circle';
}
}
constructor(
private toastService: ToasterService,
private localizationService: LocalizationService,
) {}
ngOnInit() {
if (snq(() => this.toast.options.sticky)) return;
const timeout = snq(() => this.toast.options.life) || 5000;
setTimeout(() => {
this.close();
}, timeout);
}
close() {
this.toastService.remove(this.toast.options.id);
}
tap() {
if (this.toast.options && this.toast.options.tapToDismiss) this.close();
}
}

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

@ -41,30 +41,6 @@ export default `
background: #8a8686; background: #8a8686;
} }
.modal.show {
display: block !important;
}
.modal-backdrop {
background-color: rgba(0, 0, 0, 0.6);
}
.modal::-webkit-scrollbar {
width: 7px;
}
.modal::-webkit-scrollbar-track {
background: #ddd;
}
.modal::-webkit-scrollbar-thumb {
background: #8a8686;
}
.modal-dialog {
z-index: 1050;
}
.abp-ellipsis-inline { .abp-ellipsis-inline {
display: inline-block; display: inline-block;
overflow: hidden; overflow: hidden;
@ -78,112 +54,6 @@ export default `
white-space: nowrap; white-space: nowrap;
} }
.abp-toast .ui-toast-message {
box-sizing: border-box;
border: 2px solid transparent;
border-radius: 4px;
color: #1b1d29;
}
.abp-toast .ui-toast-message-content {
padding: 10px;
}
.abp-toast .ui-toast-message-content .ui-toast-icon {
top: 0;
left: 0;
padding: 10px;
}
.abp-toast .ui-toast-summary {
margin: 0;
font-weight: 700;
}
body abp-toast .ui-toast .ui-toast-message.ui-toast-message-error {
border: 2px solid #ba1659;
background-color: #f4f4f7;
}
body abp-toast .ui-toast .ui-toast-message.ui-toast-message-error .ui-toast-message-content .ui-toast-icon {
color: #ba1659;
}
body abp-toast .ui-toast .ui-toast-message.ui-toast-message-warn {
border: 2px solid #ed5d98;
background-color: #f4f4f7;
}
body abp-toast .ui-toast .ui-toast-message.ui-toast-message-warn .ui-toast-message-content .ui-toast-icon {
color: #ed5d98;
}
body abp-toast .ui-toast .ui-toast-message.ui-toast-message-success {
border: 2px solid #1c9174;
background-color: #f4f4f7;
}
body abp-toast .ui-toast .ui-toast-message.ui-toast-message-success .ui-toast-message-content .ui-toast-icon {
color: #1c9174;
}
body abp-toast .ui-toast .ui-toast-message.ui-toast-message-info {
border: 2px solid #fccb31;
background-color: #f4f4f7;
}
body abp-toast .ui-toast .ui-toast-message.ui-toast-message-info .ui-toast-message-content .ui-toast-icon {
color: #fccb31;
}
.abp-confirm .ui-toast-message {
box-sizing: border-box;
padding: 0px;
border:0 none;
border-radius: 4px;
background-color: transparent !important;
font-family: "Poppins", sans-serif;
text-align: center;
}
.abp-confirm .ui-toast-message-content {
padding: 0px;
}
.abp-confirm .abp-confirm-icon {
margin: 32px 50px 5px !important;
color: #f8bb86 !important;
font-size: 52px !important;
}
.abp-confirm .ui-toast-close-icon {
display: none !important;
}
.abp-confirm .abp-confirm-summary {
display: block !important;
margin-bottom: 13px !important;
padding: 13px 16px 0px !important;
font-weight: 600 !important;
font-size: 18px !important;
}
.abp-confirm .abp-confirm-body {
display: inline-block !important;
padding: 0px 10px !important;
}
.abp-confirm .abp-confirm-footer {
display: block;
margin-top: 30px;
padding: 16px;
text-align: right;
}
.abp-confirm .abp-confirm-footer .btn {
margin-left: 10px !important;
}
.ui-widget-overlay { .ui-widget-overlay {
z-index: 1000; z-index: 1000;
} }

16
npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts

@ -1,11 +1,23 @@
import { Toaster } from './toaster';
import { Config } from '@abp/ng.core'; import { Config } from '@abp/ng.core';
export namespace Confirmation { export namespace Confirmation {
export interface Options extends Toaster.Options { export interface Options {
id?: any;
closable?: boolean;
messageLocalizationParams?: string[];
titleLocalizationParams?: string[];
hideCancelBtn?: boolean; hideCancelBtn?: boolean;
hideYesBtn?: boolean; hideYesBtn?: boolean;
cancelText?: Config.LocalizationParam; cancelText?: Config.LocalizationParam;
yesText?: Config.LocalizationParam; yesText?: Config.LocalizationParam;
} }
export interface DialogData {
message: Config.LocalizationParam;
title?: Config.LocalizationParam;
severity?: Severity;
options?: Partial<Options>;
}
export type Severity = 'neutral' | 'success' | 'info' | 'warning' | 'error';
} }

22
npm/ng-packs/packages/theme-shared/src/lib/models/toaster.ts

@ -1,17 +1,27 @@
import { Config } from '@abp/ng.core';
export namespace Toaster { export namespace Toaster {
export interface Options { export interface ToastOptions {
id?: any;
closable?: boolean;
life?: number; life?: number;
sticky?: boolean; sticky?: boolean;
data?: any; closable?: boolean;
tapToDismiss?: boolean;
messageLocalizationParams?: string[]; messageLocalizationParams?: string[];
titleLocalizationParams?: string[]; titleLocalizationParams?: string[];
id: any;
containerKey?: string;
}
export interface Toast {
message: Config.LocalizationParam;
title?: Config.LocalizationParam;
severity?: string;
options?: ToastOptions;
} }
export type Severity = 'success' | 'info' | 'warn' | 'error'; export type Severity = 'neutral' | 'success' | 'info' | 'warning' | 'error';
export const enum Status { export enum Status {
confirm = 'confirm', confirm = 'confirm',
reject = 'reject', reject = 'reject',
dismiss = 'dismiss', dismiss = 'dismiss',

66
npm/ng-packs/packages/theme-shared/src/lib/services/confirmation.service.ts

@ -1,43 +1,73 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { AbstractToaster } from '../abstracts/toaster';
import { Confirmation } from '../models/confirmation'; import { Confirmation } from '../models/confirmation';
import { MessageService } from 'primeng/components/common/messageservice'; import { fromEvent, Observable, Subject, ReplaySubject } from 'rxjs';
import { fromEvent, Observable, Subject } from 'rxjs';
import { takeUntil, debounceTime, filter } from 'rxjs/operators'; import { takeUntil, debounceTime, filter } from 'rxjs/operators';
import { Toaster } from '../models/toaster'; import { Toaster } from '../models/toaster';
import { Config } from '@abp/ng.core';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class ConfirmationService extends AbstractToaster<Confirmation.Options> { export class ConfirmationService {
key = 'abpConfirmation'; status$: Subject<Toaster.Status>;
confirmation$ = new ReplaySubject<Confirmation.DialogData>(1);
sticky = true; info(
message: Config.LocalizationParam,
title: Config.LocalizationParam,
options?: Partial<Confirmation.Options>,
): Observable<Toaster.Status> {
return this.show(message, title, 'info', options);
}
destroy$ = new Subject(); success(
message: Config.LocalizationParam,
title: Config.LocalizationParam,
options?: Partial<Confirmation.Options>,
): Observable<Toaster.Status> {
return this.show(message, title, 'success', options);
}
constructor(protected messageService: MessageService) { warn(
super(messageService); message: Config.LocalizationParam,
title: Config.LocalizationParam,
options?: Partial<Confirmation.Options>,
): Observable<Toaster.Status> {
return this.show(message, title, 'warning', options);
}
error(
message: Config.LocalizationParam,
title: Config.LocalizationParam,
options?: Partial<Confirmation.Options>,
): Observable<Toaster.Status> {
return this.show(message, title, 'error', options);
} }
show( show(
message: string, message: Config.LocalizationParam,
title: string, title: Config.LocalizationParam,
severity: Toaster.Severity, severity?: Toaster.Severity,
options?: Confirmation.Options, options?: Partial<Confirmation.Options>,
): Observable<Toaster.Status> { ): Observable<Toaster.Status> {
this.confirmation$.next({
message,
title: title || 'AbpUi:AreYouSure',
severity: severity || 'neutral',
options,
});
this.status$ = new Subject();
this.listenToEscape(); this.listenToEscape();
return super.show(message, title, severity, options); return this.status$;
} }
clear(status?: Toaster.Status) { clear(status?: Toaster.Status) {
super.clear(status); this.confirmation$.next();
this.status$.next(status || Toaster.Status.dismiss);
this.destroy$.next();
} }
listenToEscape() { listenToEscape() {
fromEvent(document, 'keyup') fromEvent(document, 'keyup')
.pipe( .pipe(
takeUntil(this.destroy$), takeUntil(this.status$),
debounceTime(150), debounceTime(150),
filter((key: KeyboardEvent) => key && key.key === 'Escape'), filter((key: KeyboardEvent) => key && key.key === 'Escape'),
) )

119
npm/ng-packs/packages/theme-shared/src/lib/services/toaster.service.ts

@ -1,15 +1,116 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { AbstractToaster } from '../abstracts/toaster'; import { Toaster } from '../models';
import { Message } from 'primeng/components/common/message'; import { ReplaySubject } from 'rxjs';
import { MessageService } from 'primeng/components/common/messageservice'; import { Config } from '@abp/ng.core';
import snq from 'snq';
@Injectable({ providedIn: 'root' }) @Injectable({
export class ToasterService extends AbstractToaster { providedIn: 'root',
constructor(protected messageService: MessageService) { })
super(messageService); export class ToasterService {
toasts$ = new ReplaySubject<Toaster.Toast[]>(1);
private lastId = -1;
private toasts = [] as Toaster.Toast[];
/**
* Creates an info toast with given parameters.
* @param message Content of the toast
* @param title Title of the toast
* @param options Spesific style or structural options for individual toast
*/
info(
message: Config.LocalizationParam,
title?: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
) {
return this.show(message, title, 'info', options);
}
/**
* Creates a success toast with given parameters.
* @param message Content of the toast
* @param title Title of the toast
* @param options Spesific style or structural options for individual toast
*/
success(
message: Config.LocalizationParam,
title?: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
) {
return this.show(message, title, 'success', options);
}
/**
* Creates a warning toast with given parameters.
* @param message Content of the toast
* @param title Title of the toast
* @param options Spesific style or structural options for individual toast
*/
warn(
message: Config.LocalizationParam,
title?: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
) {
return this.show(message, title, 'warning', options);
}
/**
* Creates an error toast with given parameters.
* @param message Content of the toast
* @param title Title of the toast
* @param options Spesific style or structural options for individual toast
*/
error(
message: Config.LocalizationParam,
title?: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
) {
return this.show(message, title, 'error', options);
}
/**
* Creates a toast with given parameters.
* @param message Content of the toast
* @param title Title of the toast
* @param severity Sets color of the toast. "success", "warning" etc.
* @param options Spesific style or structural options for individual toast
*/
show(
message: Config.LocalizationParam,
title: Config.LocalizationParam = null,
severity: Toaster.Severity = 'neutral',
options = {} as Partial<Toaster.ToastOptions>,
) {
const id = ++this.lastId;
this.toasts.push({
message,
title,
severity,
options: { closable: true, id, ...options },
});
this.toasts$.next(this.toasts);
return id;
}
/**
* Removes the toast with given id.
* @param id ID of the toast to be removed.
*/
remove(id: number) {
this.toasts = this.toasts.filter(toast => snq(() => toast.options.id) !== id);
this.toasts$.next(this.toasts);
} }
addAll(messages: Message[]): void { /**
this.messageService.addAll(messages.map(message => ({ key: this.key, ...message }))); * Removes all open toasts at once.
*/
clear(key?: string) {
this.toasts = !key
? []
: this.toasts.filter(toast => snq(() => toast.options.containerKey) !== key);
this.toasts$.next(this.toasts);
} }
} }

45
npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts

@ -33,45 +33,44 @@ describe('ConfirmationService', () => {
service = spectator.get(ConfirmationService); service = spectator.get(ConfirmationService);
}); });
it('should display a confirmation popup', () => { test('should display a confirmation popup', () => {
service.info('test', 'title'); service.info('test', 'title');
spectator.detectChanges(); spectator.detectChanges();
expect(spectator.query('p-toast')).toBeTruthy(); expect(spectator.query('div.confirmation .title')).toHaveText('title');
expect(spectator.query('p-toastitem')).toBeTruthy(); expect(spectator.query('div.confirmation .message')).toHaveText('test');
expect(spectator.query('div.abp-confirm-summary')).toHaveText('title');
expect(spectator.query('div.abp-confirm-body')).toHaveText('test');
}); });
it('should close with ESC key', done => { test('should close with ESC key', done => {
service.info('test', 'title'); service.info('test', 'title').subscribe(() => {
spectator.detectChanges(); setTimeout(() => {
spectator.detectComponentChanges();
expect(spectator.query('div.confirmation')).toBeFalsy();
done();
}, 0);
});
expect(spectator.query('p-toastitem')).toBeTruthy(); spectator.detectChanges();
expect(spectator.query('div.confirmation')).toBeTruthy();
spectator.dispatchKeyboardEvent('div.confirmation', 'keyup', 'Escape');
});
spectator.dispatchKeyboardEvent('abp-confirmation', 'keyup', 'Escape'); test('should close when click cancel button', done => {
service.destroy$.subscribe(() => { service.info('test', 'title', { yesText: 'Sure', cancelText: 'Exit' }).subscribe(() => {
// expect(spectator.query('p-toastitem')).toBeFalsy();
spectator.detectComponentChanges(); spectator.detectComponentChanges();
expect(spectator.query('p-toastitem')).toBeFalsy(); setTimeout(() => {
done(); expect(spectator.query('div.confirmation')).toBeFalsy();
done();
}, 0);
}); });
});
it('should close when click cancel button', done => {
service.info('test', 'title', { yesText: 'Sure', cancelText: 'Exit' });
spectator.detectChanges(); spectator.detectChanges();
expect(spectator.query('p-toastitem')).toBeTruthy(); expect(spectator.query('div.confirmation')).toBeTruthy();
expect(spectator.query('button#cancel')).toHaveText('Exit'); expect(spectator.query('button#cancel')).toHaveText('Exit');
expect(spectator.query('button#confirm')).toHaveText('Sure'); expect(spectator.query('button#confirm')).toHaveText('Sure');
service.status$.subscribe(() => {
spectator.detectComponentChanges();
expect(spectator.query('p-toastitem')).toBeFalsy();
done();
});
spectator.click('button#cancel'); spectator.click('button#cancel');
}); });
}); });

12
npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts

@ -73,10 +73,10 @@ describe('ErrorHandler', () => {
it('should display the confirmation when not found error occurs', () => { it('should display the confirmation when not found error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 404 }))); store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 404 })));
spectator.detectChanges(); spectator.detectChanges();
expect(spectator.query('.abp-confirm-summary')).toHaveText( expect(spectator.query('.confirmation .title')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError404.title, DEFAULT_ERROR_MESSAGES.defaultError404.title,
); );
expect(spectator.query('.abp-confirm-body')).toHaveText( expect(spectator.query('.confirmation .message')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError404.details, DEFAULT_ERROR_MESSAGES.defaultError404.details,
); );
}); });
@ -84,10 +84,10 @@ describe('ErrorHandler', () => {
it('should display the confirmation when default error occurs', () => { it('should display the confirmation when default error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 412 }))); store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 412 })));
spectator.detectChanges(); spectator.detectChanges();
expect(spectator.query('.abp-confirm-summary')).toHaveText( expect(spectator.query('.confirmation .title')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError.title, DEFAULT_ERROR_MESSAGES.defaultError.title,
); );
expect(spectator.query('.abp-confirm-body')).toHaveText( expect(spectator.query('.confirmation .message')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError.details, DEFAULT_ERROR_MESSAGES.defaultError.details,
); );
}); });
@ -128,8 +128,8 @@ describe('ErrorHandler', () => {
); );
spectator.detectChanges(); spectator.detectChanges();
expect(spectator.query('.abp-confirm-summary')).toHaveText('test message'); expect(spectator.query('.title')).toHaveText('test message');
expect(spectator.query('.abp-confirm-body')).toHaveText('test detail'); expect(spectator.query('.confirmation .message')).toHaveText('test detail');
}); });
}); });

10
npm/ng-packs/packages/theme-shared/src/lib/tests/modal.component.spec.ts

@ -5,14 +5,18 @@ import { MessageService } from 'primeng/components/common/messageservice';
import { ToastModule } from 'primeng/toast'; import { ToastModule } from 'primeng/toast';
import { timer } from 'rxjs'; import { timer } from 'rxjs';
import { ButtonComponent, ConfirmationComponent, ModalComponent } from '../components'; import { ButtonComponent, ConfirmationComponent, ModalComponent } from '../components';
import { RouterTestingModule } from '@angular/router/testing';
describe('ModalComponent', () => { describe('ModalComponent', () => {
let spectator: SpectatorHost<ModalComponent, { visible: boolean; busy: boolean; ngDirty: boolean }>; let spectator: SpectatorHost<
ModalComponent,
{ visible: boolean; busy: boolean; ngDirty: boolean }
>;
let appearFn; let appearFn;
let disappearFn; let disappearFn;
const createHost = createHostFactory({ const createHost = createHostFactory({
component: ModalComponent, component: ModalComponent,
imports: [ToastModule], imports: [ToastModule, RouterTestingModule],
declarations: [ConfirmationComponent, LocalizationPipe, ButtonComponent], declarations: [ConfirmationComponent, LocalizationPipe, ButtonComponent],
providers: [MessageService], providers: [MessageService],
mocks: [Store], mocks: [Store],
@ -82,7 +86,7 @@ describe('ModalComponent', () => {
spectator.click('#abp-modal-close-button'); spectator.click('#abp-modal-close-button');
expect(disappearFn).not.toHaveBeenCalled(); expect(disappearFn).not.toHaveBeenCalled();
expect(spectator.query('p-toast')).toBeTruthy(); expect(spectator.query('div.confirmation')).toBeTruthy();
spectator.click('button#cancel'); spectator.click('button#cancel');
expect(spectator.query('div.modal')).toBeTruthy(); expect(spectator.query('div.modal')).toBeTruthy();

49
npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts

@ -11,7 +11,7 @@ import { OAuthService } from 'angular-oauth2-oidc';
@Component({ @Component({
selector: 'abp-dummy', selector: 'abp-dummy',
template: ` template: `
<abp-toast></abp-toast> <abp-toast-container></abp-toast-container>
`, `,
}) })
class DummyComponent { class DummyComponent {
@ -33,58 +33,57 @@ describe('ToasterService', () => {
service = spectator.get(ToasterService); service = spectator.get(ToasterService);
}); });
it('should display an error toast', () => { test('should display an error toast', () => {
service.error('test', 'title'); service.error('test', 'title');
spectator.detectChanges(); spectator.detectChanges();
expect(spectator.query('p-toast')).toBeTruthy(); expect(spectator.query('div.toast')).toBeTruthy();
expect(spectator.query('p-toastitem')).toBeTruthy(); expect(spectator.query('.toast-icon i')).toHaveClass('fa-times-circle');
expect(spectator.query('span.ui-toast-icon')).toHaveClass('pi-times'); expect(spectator.query('div.toast-title')).toHaveText('title');
expect(spectator.query('div.ui-toast-summary')).toHaveText('title'); expect(spectator.query('div.toast-message')).toHaveText('test');
expect(spectator.query('div.ui-toast-detail')).toHaveText('test');
}); });
it('should display a warning toast', () => { test('should display a warning toast', () => {
service.warn('test', 'title'); service.warn('test', 'title');
spectator.detectChanges(); spectator.detectChanges();
expect(spectator.query('span.ui-toast-icon')).toHaveClass('pi-exclamation-triangle'); expect(spectator.query('.toast-icon i')).toHaveClass('fa-exclamation-triangle');
}); });
it('should display a success toast', () => { test('should display a success toast', () => {
service.success('test', 'title'); service.success('test', 'title');
spectator.detectChanges(); spectator.detectChanges();
expect(spectator.query('span.ui-toast-icon')).toHaveClass('pi-check'); expect(spectator.query('.toast-icon i')).toHaveClass('fa-check-circle');
}); });
it('should display an info toast', () => { test('should display an info toast', () => {
service.info('test', 'title'); service.info('test', 'title');
spectator.detectChanges(); spectator.detectChanges();
expect(spectator.query('span.ui-toast-icon')).toHaveClass('pi-info-circle'); expect(spectator.query('.toast-icon i')).toHaveClass('fa-info-circle');
}); });
it('should display multiple toasts', () => { test('should display multiple toasts', () => {
service.addAll([ service.info('detail1', 'summary1');
{ summary: 'summary1', detail: 'detail1' }, service.info('detail2', 'summary2');
{ summary: 'summary2', detail: 'detail2' },
]);
spectator.detectChanges(); spectator.detectChanges();
expect( expect(spectator.queryAll('div.toast-title').map(node => node.textContent.trim())).toEqual([
spectator.queryAll('div.ui-toast-summary').map(node => node.textContent.trim()), 'summary1',
).toEqual(['summary1', 'summary2']); 'summary2',
expect(spectator.queryAll('div.ui-toast-detail').map(node => node.textContent.trim())).toEqual([ ]);
expect(spectator.queryAll('div.toast-message').map(node => node.textContent.trim())).toEqual([
'detail1', 'detail1',
'detail2', 'detail2',
]); ]);
}); });
it('should remove the opened toast', () => { test('should remove the opened toasts', () => {
service.info('test', 'title'); service.info('test', 'title');
spectator.detectChanges(); spectator.detectChanges();
expect(spectator.query('p-toastitem')).toBeTruthy(); expect(spectator.query('div.toast')).toBeTruthy();
service.clear(); service.clear();
spectator.detectChanges(); spectator.detectChanges();
expect(spectator.query('p-toastitem')).toBeFalsy(); expect(spectator.query('p-div.toast')).toBeFalsy();
}); });
}); });

3
npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts

@ -14,6 +14,7 @@ import { LoaderBarComponent } from './components/loader-bar/loader-bar.component
import { ModalComponent } from './components/modal/modal.component'; import { ModalComponent } from './components/modal/modal.component';
import { SortOrderIconComponent } from './components/sort-order-icon/sort-order-icon.component'; import { SortOrderIconComponent } from './components/sort-order-icon/sort-order-icon.component';
import { TableEmptyMessageComponent } from './components/table-empty-message/table-empty-message.component'; import { TableEmptyMessageComponent } from './components/table-empty-message/table-empty-message.component';
import { ToastContainerComponent } from './components/toast-container/toast-container.component';
import { TableComponent } from './components/table/table.component'; import { TableComponent } from './components/table/table.component';
import { ToastComponent } from './components/toast/toast.component'; import { ToastComponent } from './components/toast/toast.component';
import styles from './constants/styles'; import styles from './constants/styles';
@ -53,6 +54,7 @@ export function appendScript(injector: Injector) {
TableComponent, TableComponent,
TableEmptyMessageComponent, TableEmptyMessageComponent,
ToastComponent, ToastComponent,
ToastContainerComponent,
SortOrderIconComponent, SortOrderIconComponent,
LoadingDirective, LoadingDirective,
TableSortDirective, TableSortDirective,
@ -69,6 +71,7 @@ export function appendScript(injector: Injector) {
TableComponent, TableComponent,
TableEmptyMessageComponent, TableEmptyMessageComponent,
ToastComponent, ToastComponent,
ToastContainerComponent,
SortOrderIconComponent, SortOrderIconComponent,
LoadingDirective, LoadingDirective,
TableSortDirective, TableSortDirective,

1
nupkg/common.ps1

@ -40,6 +40,7 @@ $projects = (
"framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic", "framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic",
"framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared", "framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared",
"framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets", "framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets",
"framework/src/Volo.Abp.AspNetCore.Serilog",
"framework/src/Volo.Abp.AspNetCore.TestBase", "framework/src/Volo.Abp.AspNetCore.TestBase",
"framework/src/Volo.Abp.Auditing", "framework/src/Volo.Abp.Auditing",
"framework/src/Volo.Abp.Authorization", "framework/src/Volo.Abp.Authorization",

2
samples/BookStore-Angular-MongoDb/aspnet-core/src/Acme.BookStore.HttpApi.Host/Acme.BookStore.HttpApi.Host.csproj

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" /> <PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" />

2
samples/BookStore-Modular/application/src/Acme.BookStore.Web/Acme.BookStore.Web.csproj

@ -32,7 +32,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" /> <PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" />

2
samples/BookStore-Modular/modules/book-management/host/Acme.BookStore.BookManagement.HttpApi.Host/Acme.BookStore.BookManagement.HttpApi.Host.csproj

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" /> <PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />

2
samples/BookStore-Modular/modules/book-management/host/Acme.BookStore.BookManagement.IdentityServer/Acme.BookStore.BookManagement.IdentityServer.csproj

@ -8,7 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" /> <PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />

2
samples/BookStore-Modular/modules/book-management/host/Acme.BookStore.BookManagement.Web.Host/Acme.BookStore.BookManagement.Web.Host.csproj

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="3.1.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="3.1.0" />

2
samples/BookStore-Modular/modules/book-management/host/Acme.BookStore.BookManagement.Web.Unified/Acme.BookStore.BookManagement.Web.Unified.csproj

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.0" />

4
samples/BookStore/src/Acme.BookStore.Application/BookStoreApplicationModule.cs

@ -1,4 +1,5 @@
using Volo.Abp.AutoMapper; using Volo.Abp.Account;
using Volo.Abp.AutoMapper;
using Volo.Abp.FeatureManagement; using Volo.Abp.FeatureManagement;
using Volo.Abp.Identity; using Volo.Abp.Identity;
using Volo.Abp.Modularity; using Volo.Abp.Modularity;
@ -10,6 +11,7 @@ namespace Acme.BookStore
[DependsOn( [DependsOn(
typeof(BookStoreDomainModule), typeof(BookStoreDomainModule),
typeof(BookStoreApplicationContractsModule), typeof(BookStoreApplicationContractsModule),
typeof(AbpAccountApplicationModule),
typeof(AbpIdentityApplicationModule), typeof(AbpIdentityApplicationModule),
typeof(AbpPermissionManagementApplicationModule), typeof(AbpPermissionManagementApplicationModule),
typeof(AbpTenantManagementApplicationModule), typeof(AbpTenantManagementApplicationModule),

2
samples/BookStore/src/Acme.BookStore.Web/Acme.BookStore.Web.csproj

@ -23,7 +23,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.0" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.0" />

2
samples/DashboardDemo/src/DashboardDemo.Web/DashboardDemo.Web.csproj

@ -31,7 +31,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" /> <PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />

2
samples/MicroserviceDemo/applications/AuthServer.Host/AuthServer.Host.csproj

@ -12,7 +12,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Elasticsearch" Version="6.5.0" /> <PackageReference Include="Serilog.Sinks.Elasticsearch" Version="6.5.0" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="3.1.0" /> <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="3.1.0" />

2
samples/MicroserviceDemo/applications/BackendAdminApp.Host/BackendAdminApp.Host.csproj

@ -12,7 +12,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Elasticsearch" Version="6.5.0" /> <PackageReference Include="Serilog.Sinks.Elasticsearch" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" />

2
samples/MicroserviceDemo/applications/PublicWebSite.Host/PublicWebSite.Host.csproj

@ -12,7 +12,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Elasticsearch" Version="6.5.0" /> <PackageReference Include="Serilog.Sinks.Elasticsearch" Version="6.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="3.1.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="3.1.0" />

2
samples/MicroserviceDemo/gateways/BackendAdminAppGateway.Host/BackendAdminAppGateway.Host.csproj

@ -12,7 +12,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Elasticsearch" Version="6.5.0" /> <PackageReference Include="Serilog.Sinks.Elasticsearch" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" />

2
samples/MicroserviceDemo/gateways/InternalGateway.Host/InternalGateway.Host.csproj

@ -12,7 +12,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Elasticsearch" Version="6.5.0" /> <PackageReference Include="Serilog.Sinks.Elasticsearch" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" />

2
samples/MicroserviceDemo/gateways/PublicWebSiteGateway.Host/PublicWebSiteGateway.Host.csproj

@ -12,7 +12,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Elasticsearch" Version="6.5.0" /> <PackageReference Include="Serilog.Sinks.Elasticsearch" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" />

2
samples/MicroserviceDemo/microservices/BloggingService.Host/BloggingService.Host.csproj

@ -12,7 +12,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Elasticsearch" Version="6.5.0" /> <PackageReference Include="Serilog.Sinks.Elasticsearch" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" />

2
samples/MicroserviceDemo/microservices/IdentityService.Host/IdentityService.Host.csproj

@ -12,7 +12,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.Elasticsearch" Version="6.5.0" /> <PackageReference Include="Serilog.Sinks.Elasticsearch" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc4" />

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save