mirror of https://github.com/abpframework/abp.git
119 changed files with 1752 additions and 457 deletions
@ -1,3 +1,58 @@ |
|||
# 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) |
|||
@ -0,0 +1,3 @@ |
|||
# IdentityServer Module |
|||
|
|||
TODO |
|||
@ -0,0 +1,3 @@ |
|||
# FluentValidation 集成 |
|||
|
|||
TODO |
|||
@ -0,0 +1,5 @@ |
|||
# 日志 |
|||
|
|||
ABP框架没有实现任何日志基础设施. 它使用[ASP.NET Core日志系统](https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/logging). |
|||
|
|||
> .NET Core 的日志系统是独立于ASP.NET Core的,它可以在任何类型的应用程序中使用. |
|||
@ -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)了解更多信息. |
|||
@ -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>(); |
|||
} |
|||
} |
|||
} |
|||
@ -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" |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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 |
|||
{ |
|||
} |
|||
} |
|||
@ -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"; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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()); |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -1,3 +1,10 @@ |
|||
# 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. |
|||
@ -1,13 +1,15 @@ |
|||
# 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. |
|||
|
|||
 |
|||
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 |
|||
|
|||
* Retrieves documents from a Github repository. |
|||
* Supports Markdown document formatting. |
|||
* Supports versioning (integrated to Github releases). |
|||
* Supports multiple projects. |
|||
 |
|||
|
|||
|
Before Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 260 KiB |
@ -0,0 +1,8 @@ |
|||
namespace Volo.Abp.Identity |
|||
{ |
|||
public class IdentityRoleNameChangedEvent |
|||
{ |
|||
public IdentityRole IdentityRole { get; set; } |
|||
public string OldName { get; set; } |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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, |
|||
}), |
|||
]), |
|||
]); |
|||
@ -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> |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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 { |
|||
} |
|||
} |
|||
} |
|||
@ -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({ |
|||
selector: 'abp-toast', |
|||
// tslint:disable-next-line: component-max-inline-declarations
|
|||
template: ` |
|||
<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> |
|||
`,
|
|||
templateUrl: './toast.component.html', |
|||
styleUrls: ['./toast.component.scss'], |
|||
}) |
|||
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(); |
|||
} |
|||
} |
|||
|
|||
@ -1,11 +1,23 @@ |
|||
import { Toaster } from './toaster'; |
|||
import { Config } from '@abp/ng.core'; |
|||
|
|||
export namespace Confirmation { |
|||
export interface Options extends Toaster.Options { |
|||
export interface Options { |
|||
id?: any; |
|||
closable?: boolean; |
|||
messageLocalizationParams?: string[]; |
|||
titleLocalizationParams?: string[]; |
|||
hideCancelBtn?: boolean; |
|||
hideYesBtn?: boolean; |
|||
cancelText?: 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'; |
|||
} |
|||
|
|||
@ -1,15 +1,116 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import { AbstractToaster } from '../abstracts/toaster'; |
|||
import { Message } from 'primeng/components/common/message'; |
|||
import { MessageService } from 'primeng/components/common/messageservice'; |
|||
import { Toaster } from '../models'; |
|||
import { ReplaySubject } from 'rxjs'; |
|||
import { Config } from '@abp/ng.core'; |
|||
import snq from 'snq'; |
|||
|
|||
@Injectable({ providedIn: 'root' }) |
|||
export class ToasterService extends AbstractToaster { |
|||
constructor(protected messageService: MessageService) { |
|||
super(messageService); |
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
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); |
|||
} |
|||
} |
|||
|
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue