Browse Source

Merge branch 'dev' into olicooper/quartz-startup-delay

pull/3454/head
olicooper 6 years ago
committed by GitHub
parent
commit
10d79548cd
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      build/common.ps1
  2. 51
      docs/en/Application-Services.md
  3. 43
      docs/en/Background-Jobs-Hangfire.md
  4. 178
      docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md
  5. 113
      docs/en/How-To/Customize-Login-Page-MVC.md
  6. 84
      docs/en/How-To/Customize-SignIn-Manager.md
  7. 9
      docs/en/How-To/Index.md
  8. 56
      docs/en/UI/Angular/Content-Security-Strategy.md
  9. 41
      docs/en/UI/Angular/Cross-Origin-Strategy.md
  10. 2
      docs/en/UI/Angular/Custom-Setting-Page.md
  11. 58
      docs/en/UI/Angular/Dom-Strategy.md
  12. 205
      docs/en/UI/Angular/Lazy-Load-Service.md
  13. 77
      docs/en/UI/Angular/Loading-Strategy.md
  14. 8
      docs/en/docs-nav.json
  15. 47
      docs/zh-Hans/Application-Services.md
  16. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Popover/AbpPopoverTagHelper.cs
  17. 60
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Popover/AbpPopoverTagHelperService.cs
  18. 5
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperResourceService.cs
  19. 11
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/TuiEditor/TuiEditorScriptContributor.cs
  20. 23
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs
  21. 5
      framework/src/Volo.Abp.Core/Microsoft/Extensions/Configuration/ConfigurationHelper.cs
  22. 12
      framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs
  23. 15
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs
  24. 22
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/UrlBuilder.cs
  25. 8
      framework/src/Volo.Abp.Quartz/Volo/Abp/Quartz/AbpQuartzModule.cs
  26. 31
      framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Popovers.cshtml
  27. 5
      framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/IRegularTestController.cs
  28. 22
      framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestController.cs
  29. 46
      framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestControllerClientProxy_Tests.cs
  30. 19
      modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs
  31. 31
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs
  32. 2
      modules/audit-logging/src/Volo.Abp.AuditLogging.Domain/Volo/Abp/AuditLogging/IAuditLogRepository.cs
  33. 5
      modules/audit-logging/src/Volo.Abp.AuditLogging.EntityFrameworkCore/Volo/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogRepository.cs
  34. 7
      modules/audit-logging/src/Volo.Abp.AuditLogging.MongoDB/Volo/Abp/AuditLogging/MongoDB/MongoAuditLogRepository.cs
  35. 2
      modules/audit-logging/test/Volo.Abp.AuditLogging.TestBase/Volo/Abp/AuditLogging/AuditLogRepository_Tests.cs
  36. 10
      npm/ng-packs/angular.json
  37. 20
      npm/ng-packs/apps/dev-app/src/app/app.component.ts
  38. 10
      npm/ng-packs/apps/dev-app/tsconfig.dev.json
  39. 2
      npm/ng-packs/apps/dev-app/tsconfig.prod.json
  40. 1
      npm/ng-packs/package.json
  41. 12
      npm/ng-packs/packages/account-config/tsconfig.lib.json
  42. 2
      npm/ng-packs/packages/account-config/tsconfig.spec.json
  43. 2
      npm/ng-packs/packages/account/tsconfig.lib.json
  44. 2
      npm/ng-packs/packages/account/tsconfig.spec.json
  45. 42
      npm/ng-packs/packages/core/src/lib/services/lazy-load.service.ts
  46. 32
      npm/ng-packs/packages/core/src/lib/strategies/content-security.strategy.ts
  47. 17
      npm/ng-packs/packages/core/src/lib/strategies/cross-origin.strategy.ts
  48. 28
      npm/ng-packs/packages/core/src/lib/strategies/dom.strategy.ts
  49. 4
      npm/ng-packs/packages/core/src/lib/strategies/index.ts
  50. 88
      npm/ng-packs/packages/core/src/lib/strategies/loading.strategy.ts
  51. 41
      npm/ng-packs/packages/core/src/lib/tests/content-security.strategy.spec.ts
  52. 38
      npm/ng-packs/packages/core/src/lib/tests/cross-origin.strategy.spec.ts
  53. 49
      npm/ng-packs/packages/core/src/lib/tests/dom.strategy.spec.ts
  54. 113
      npm/ng-packs/packages/core/src/lib/tests/lazy-load-utils.spec.ts
  55. 74
      npm/ng-packs/packages/core/src/lib/tests/lazy-load.service.spec.ts
  56. 102
      npm/ng-packs/packages/core/src/lib/tests/loading.strategy.spec.ts
  57. 1
      npm/ng-packs/packages/core/src/lib/utils/index.ts
  58. 51
      npm/ng-packs/packages/core/src/lib/utils/lazy-load-utils.ts
  59. 4
      npm/ng-packs/packages/core/src/public-api.ts
  60. 2
      npm/ng-packs/packages/core/tsconfig.lib.json
  61. 2
      npm/ng-packs/packages/core/tsconfig.spec.json
  62. 12
      npm/ng-packs/packages/feature-management/tsconfig.lib.json
  63. 2
      npm/ng-packs/packages/feature-management/tsconfig.spec.json
  64. 12
      npm/ng-packs/packages/identity-config/tsconfig.lib.json
  65. 2
      npm/ng-packs/packages/identity-config/tsconfig.spec.json
  66. 6
      npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.ts
  67. 2
      npm/ng-packs/packages/identity/tsconfig.lib.json
  68. 2
      npm/ng-packs/packages/identity/tsconfig.spec.json
  69. 2
      npm/ng-packs/packages/permission-management/tsconfig.lib.json
  70. 2
      npm/ng-packs/packages/permission-management/tsconfig.spec.json
  71. 12
      npm/ng-packs/packages/setting-management-config/tsconfig.lib.json
  72. 2
      npm/ng-packs/packages/setting-management-config/tsconfig.spec.json
  73. 12
      npm/ng-packs/packages/setting-management/tsconfig.lib.json
  74. 2
      npm/ng-packs/packages/setting-management/tsconfig.spec.json
  75. 12
      npm/ng-packs/packages/tenant-management-config/tsconfig.lib.json
  76. 2
      npm/ng-packs/packages/tenant-management-config/tsconfig.spec.json
  77. 6
      npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.ts
  78. 12
      npm/ng-packs/packages/tenant-management/tsconfig.lib.json
  79. 2
      npm/ng-packs/packages/tenant-management/tsconfig.spec.json
  80. 2
      npm/ng-packs/packages/theme-basic/tsconfig.lib.json
  81. 2
      npm/ng-packs/packages/theme-basic/tsconfig.spec.json
  82. 6
      npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts
  83. 4
      npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts
  84. 2
      npm/ng-packs/packages/theme-shared/tsconfig.lib.json
  85. 2
      npm/ng-packs/packages/theme-shared/tsconfig.spec.json
  86. 30
      npm/ng-packs/tsconfig.json
  87. 22
      npm/ng-packs/tsconfig.prod.json
  88. 2
      npm/packs/tui-editor/abp.resourcemapping.js
  89. 16
      samples/BookStore-Angular-MongoDb/aspnet-core/src/Acme.BookStore.Domain/Users/AppUser.cs
  90. 16
      samples/BookStore-Modular/application/src/Acme.BookStore.Domain/Users/AppUser.cs
  91. 7
      samples/BookStore-Modular/application/src/Acme.BookStore.EntityFrameworkCore.DbMigrations/EntityFrameworkCore/BookStoreMigrationsDbContext.cs
  92. 2
      samples/BookStore-Modular/application/src/Acme.BookStore.EntityFrameworkCore.DbMigrations/EntityFrameworkCore/BookStoreMigrationsDbContextFactory.cs
  93. 8
      samples/BookStore-Modular/application/src/Acme.BookStore.EntityFrameworkCore/EntityFrameworkCore/BookStoreDbContext.cs
  94. 8
      samples/BookStore-Modular/application/src/Acme.BookStore.EntityFrameworkCore/EntityFrameworkCore/BookStoreDbContextModelCreatingExtensions.cs
  95. 37
      samples/BookStore-Modular/application/src/Acme.BookStore.EntityFrameworkCore/EntityFrameworkCore/BookStoreEfCoreEntityExtensionMappings.cs
  96. 5
      samples/BookStore-Modular/application/src/Acme.BookStore.EntityFrameworkCore/EntityFrameworkCore/BookStoreEntityFrameworkCoreModule.cs
  97. 36
      samples/BookStore-Modular/modules/book-management/src/Acme.BookStore.BookManagement.EntityFrameworkCore/EntityFrameworkCore/BookManagementEfCoreEntityExtensionMappings.cs
  98. 5
      samples/BookStore-Modular/modules/book-management/src/Acme.BookStore.BookManagement.EntityFrameworkCore/EntityFrameworkCore/BookManagementEntityFrameworkCoreModule.cs
  99. 16
      samples/BookStore/src/Acme.BookStore.Domain/Users/AppUser.cs
  100. 7
      samples/BookStore/src/Acme.BookStore.EntityFrameworkCore.DbMigrations/EntityFrameworkCore/BookStoreMigrationsDbContext.cs

1
build/common.ps1

@ -28,6 +28,7 @@ $solutionPaths = (
"../samples/BookStore-Modular/modules/book-management",
"../samples/BookStore-Modular/application",
"../samples/DashboardDemo",
"../samples/EfCoreMigrationDemo",
"../samples/MicroserviceDemo",
"../samples/RabbitMqEventBus",
"../abp_io/AbpIoLocalization"

51
docs/en/Application-Services.md

@ -132,6 +132,8 @@ The `CreateAsync` method above manually creates a `Book` entity from given `Crea
However, in many cases, it's very practical to use **auto object mapping** to set properties of an object from a similar object. ABP provides an [object to object mapping](Object-To-Object-Mapping.md) infrastructure to make this even easier.
Object to object mapping provides abstractions and it is implemented by the [AutoMapper](https://automapper.org/) library by default.
Let's create another method to get a book. First, define the method in the `IBookAppService` interface:
````csharp
@ -146,7 +148,6 @@ public interface IBookAppService : IApplicationService
`BookDto` is a simple [DTO](Data-Transfer-Objects.md) class defined as below:
````csharp
[AbpAutoMapFrom(typeof(Book))] //Defines the mapping
public class BookDto
{
public Guid Id { get; set; }
@ -159,21 +160,38 @@ public class BookDto
}
````
* `BookDto` defines `[AbpAutoMapFrom(typeof(Book))]` attribute to create the object mapping from `Book` to `BookDto`.
AutoMapper requires to create a mapping [profile class](https://docs.automapper.org/en/stable/Configuration.html#profile-instances). Example:
Then you can implement the `GetAsync` method as shown below:
````csharp
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<Book, BookDto>();
}
}
````
You should then register profiles using the `AbpAutoMapperOptions`:
````csharp
public async Task<BookDto> GetAsync(Guid id)
[DependsOn(typeof(AbpAutoMapperModule))]
public class MyModule : AbpModule
{
var book = await _bookRepository.GetAsync(id);
return book.MapTo<BookDto>();
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpAutoMapperOptions>(options =>
{
//Add all mappings defined in the assembly of the MyModule class
options.AddMaps<MyModule>();
});
}
}
````
`MapTo` extension method converts `Book` object to `BookDto` object by copying all properties with the same naming.
`AddMaps` registers all profile classes defined in the assembly of the given class, typically your module class. It also registers for the [attribute mapping](https://docs.automapper.org/en/stable/Attribute-mapping.html).
An alternative to the `MapTo` is using the `IObjectMapper` service:
Then you can implement the `GetAsync` method as shown below:
````csharp
public async Task<BookDto> GetAsync(Guid id)
@ -183,8 +201,6 @@ public async Task<BookDto> GetAsync(Guid id)
}
````
While the second syntax is a bit harder to write, it better works if you write unit tests.
See the [object to object mapping document](Object-To-Object-Mapping.md) for more.
## Validation
@ -250,7 +266,6 @@ public interface ICrudAppService<
DTO classes used in this example are `BookDto` and `CreateUpdateBookDto`:
````csharp
[AbpAutoMapFrom(typeof(Book))]
public class BookDto : AuditedEntityDto<Guid>
{
public string Name { get; set; }
@ -260,7 +275,6 @@ public class BookDto : AuditedEntityDto<Guid>
public float Price { get; set; }
}
[AbpAutoMapTo(typeof(Book))]
public class CreateUpdateBookDto
{
[Required]
@ -275,6 +289,19 @@ public class CreateUpdateBookDto
}
````
[Profile](https://docs.automapper.org/en/stable/Configuration.html#profile-instances) class of DTO class.
```csharp
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<Book, BookDto>();
CreateMap<CreateUpdateBookDto, Book>();
}
}
```
* `CreateUpdateBookDto` is shared by create and update operations, but you could use separated DTO classes as well.
And finally, the `BookAppService` implementation is very simple:

43
docs/en/Background-Jobs-Hangfire.md

@ -39,5 +39,46 @@ public class YourModule : AbpModule
````
## Configuration
You can install any storage for Hangfire. The most common one is SQL Server (see the [Hangfire.SqlServer](https://www.nuget.org/packages/Hangfire.SqlServer) NuGet package).
TODO...
After you have installed these NuGet packages, you need to configure your project to use Hangfire.
1.First, we change the `Module` class (example: `<YourProjectName>HttpApiHostModule`) to add Hangfire configuration of the storage and connection string in the `ConfigureServices` method:
````csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
var hostingEnvironment = context.Services.GetHostingEnvironment();
//... other configarations
ConfigureHangfire(context, configuration);
}
private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.AddHangfire(config =>
{
config.UseSqlServerStorage(configuration.GetConnectionString("Default"));
});
}
````
2. We need to add `UseHangfireServer` call in the `OnApplicationInitialization` method in `Module` class
If you want to use hangfire's dashboard, you can add it, too: by `UseHangfireDashboard`
````csharp
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
// ... others
app.UseHangfireServer();
app.UseHangfireDashboard();
}
````

178
docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md

@ -0,0 +1,178 @@
# How to Use the Azure Active Directory Authentication for MVC / Razor Page Applications
## Introduction
This post demonstrates how to integrate AzureAD to an ABP application that enables users to sign in using OAuth 2.0 with credentials from Azure Active Directory.
Adding Azure Active Directory is pretty straightforward in ABP framework. Couple of configurations needs to be done correctly.
There will be two samples of connections for better coverage;
- **AddAzureAD** (Microsoft.AspNetCore.Authentication.AzureAD.UI package)
- **AddOpenIdConnect** (Default Microsoft.AspNetCore.Authentication.OpenIdConnect package)
## Sample Code
https://github.com/abpframework/abp-samples/tree/master/aspnet-core/BookStore-AzureAD
## Setup
Update your `appsettings.json` in your **.Web** application and add the following section filled with your AzureAD application settings.
````xml
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "<your-tenant-id",
"ClientId": "<your-client-id>",
"Domain": "domain.onmicrosoft.com",
"CallbackPath": "/signin-azuread-oidc"
}
````
## AddAzureAD
#### **Update your `appsettings.json`**
Install `Microsoft.AspNetCore.Authentication.AzureAD.UI` package to your **.Web** application.
In your **.Web** application, add the following section filled with your AzureAD application settings. Modify `ConfigureAuthentication` method of your **BookStoreWebModule** with the following:
````xml
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier);
context.Services.AddAuthentication()
.AddIdentityServerAuthentication(options =>
{
options.Authority = configuration["AuthServer:Authority"];
options.RequireHttpsMetadata = false;
options.ApiName = "Acme.BookStore";
})
.AddAzureAD(options => configuration.Bind("AzureAd", options));
context.Services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Authority = options.Authority + "/v2.0/";
options.ClientId = configuration["AzureAd:ClientId"];
options.CallbackPath = configuration["AzureAd:CallbackPath"];
options.ResponseType = OpenIdConnectResponseType.IdToken;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters.ValidateIssuer = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.SignInScheme = IdentityConstants.ExternalScheme;
options.Scope.Add("email");
});
}
````
## AddOpenIdConnect
Modify `ConfigureAuthentication` method of your **BookStoreWebModule** with the following:
````xml
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier);
context.Services.AddAuthentication()
.AddIdentityServerAuthentication(options =>
{
options.Authority = configuration["AuthServer:Authority"];
options.RequireHttpsMetadata = false;
options.ApiName = "BookStore";
})
.AddOpenIdConnect("AzureOpenId", "AzureAD", options =>
{
options.Authority = "https://login.microsoftonline.com/" + configuration["AzureAd:TenantId"];
options.ClientId = configuration["AzureAd:ClientId"];
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.CallbackPath = configuration["AzureAd:CallbackPath"];
options.RequireHttpsMetadata = false;
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
});
}
````
# FAQ
* Help! `GetExternalLoginInfoAsync` returns `null`!
* There can be 2 reasons for this;
1. You are trying to authenticate against wrong scheme. Check if you set **SignInScheme** to `IdentityConstants.ExternalScheme`:
````xml
options.SignInScheme = IdentityConstants.ExternalScheme;
````
2. Your `ClaimTypes.NameIdentifier` is `null`. Check if you added claim mapping:
````xml
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier);
````
* Help! I keep getting ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** error!
* If you set your **CallbackPath** in appsettings as:
````xml
"AzureAd": {
...
"CallbackPath": "/signin-azuread-oidc"
}
````
your **Redirect URI** of your application in azure portal must be with <u>domain</u> like `https://localhost:44320/signin-azuread-oidc`, not only `/signin-azuread-oidc`.
* Help! I am getting ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** error!
* This occurs when you use Azure Authority **v2.0 endpoint** without requesting `email` scope. [Abp checks unique email to create user](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). Simply add
````xml
options.Scope.Add("email");
````
to your openid configuration.
* How can I **debug/watch** which claims I get before they get mapped?
* You can add a simple event under openid configuration to debug before mapping like:
````xml
options.Events.OnTokenValidated = (async context =>
{
var claimsFromOidcProvider = context.Principal.Claims.ToList();
await Task.CompletedTask;
});
````
* I want to debug further, how can I implement my custom **SignInManager**?
* You can check [Customizing SignInManager in Abp Framework](link here) post.
* I want to add extra properties to user while they are being created. How can I do that?
* You can check [Customizing Login Page in Abp Framework]() post.
* Why can't I see **External Register Page** after I sign in from external provider for the first time?
* ABP framework automatically registers your user with supported email claim from your external authentication provider. You can change this behavior by [Customizing Login Page in Abp Framework](will be link here).

113
docs/en/How-To/Customize-Login-Page-MVC.md

@ -0,0 +1,113 @@
# How to Customize the Login Page for MVC / Razor Page Applications
When you create a new application using the [application startup template](../Startup-Templates/Application.md), source code of the login page will not be inside your solution, so you can not directly change it. The login page comes from the [Account Module](../Modules/Account) that is used a [NuGet package](https://www.nuget.org/packages/Volo.Abp.Account.Web) reference.
This document explains how to customize the login page for your own application.
## Create a Login PageModel
Create a new class inheriting from the [LoginModel](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs) of the Account module.
````csharp
public class CustomLoginModel : LoginModel
{
public CustomLoginModel(
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemeProvider,
Microsoft.Extensions.Options.IOptions<Volo.Abp.Account.Web.AbpAccountOptions> accountOptions)
: base(schemeProvider, accountOptions)
{
}
}
````
> Naming convention is important here. If your class name doesn't end with `LoginModel`, you need to manually replace the `LoginModel` using the [dependency injection](../Dependency-Injection.md) system.
Then you can override any method you need and add new methods and properties needed by the UI.
## Overriding the Login Page UI
Create folder named **Account** under **Pages** directory and create a **Login.cshtml** under this folder. It will automatically override the `Login.cshtml` file defined in the Account Module thanks to the [Virtual File System](../Virtual-File-System.md).
A good way to customize a page is to copy its source code. [Click here](https://github.com/abpframework/abp/blob/dev/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml) for the source code of the login page. At the time this document has been written, the source code was like below:
````xml
@page
@using Volo.Abp.Account.Settings
@using Volo.Abp.Settings
@model Acme.BookStore.Web.Pages.Account.CustomLoginModel
@inherits Volo.Abp.Account.Web.Pages.Account.AccountPage
@inject Volo.Abp.Settings.ISettingProvider SettingProvider
@if (Model.EnableLocalLogin)
{
<div class="card mt-3 shadow-sm rounded">
<div class="card-body p-5">
<h4>@L["Login"]</h4>
@if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled))
{
<strong>
@L["AreYouANewUser"]
<a href="@Url.Page("./Register", new {returnUrl = Model.ReturnUrl, returnUrlHash = Model.ReturnUrlHash})" class="text-decoration-none">@L["Register"]</a>
</strong>
}
<form method="post" class="mt-4">
<input asp-for="ReturnUrl" />
<input asp-for="ReturnUrlHash" />
<div class="form-group">
<label asp-for="LoginInput.UserNameOrEmailAddress"></label>
<input asp-for="LoginInput.UserNameOrEmailAddress" class="form-control" />
<span asp-validation-for="LoginInput.UserNameOrEmailAddress" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="LoginInput.Password"></label>
<input asp-for="LoginInput.Password" class="form-control" />
<span asp-validation-for="LoginInput.Password" class="text-danger"></span>
</div>
<div class="form-check">
<label asp-for="LoginInput.RememberMe" class="form-check-label">
<input asp-for="LoginInput.RememberMe" class="form-check-input" />
@Html.DisplayNameFor(m => m.LoginInput.RememberMe)
</label>
</div>
<abp-button type="submit" button-type="Primary" name="Action" value="Login" class="btn-block btn-lg mt-3">@L["Login"]</abp-button>
</form>
</div>
<div class="card-footer text-center border-0">
<abp-button type="button" button-type="Link" name="Action" value="Cancel" class="px-2 py-0">@L["Cancel"]</abp-button> @* TODO: Only show if identity server is used *@
</div>
</div>
}
@if (Model.VisibleExternalProviders.Any())
{
<div class="col-md-6">
<h4>@L["UseAnotherServiceToLogIn"]</h4>
<form asp-page="./Login" asp-page-handler="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" asp-route-returnUrlHash="@Model.ReturnUrlHash" method="post">
<input asp-for="ReturnUrl" />
<input asp-for="ReturnUrlHash" />
@foreach (var provider in Model.VisibleExternalProviders)
{
<button type="submit" class="btn btn-primary" name="provider" value="@provider.AuthenticationScheme" title="@L["GivenTenantIsNotAvailable", provider.DisplayName]">@provider.DisplayName</button>
}
</form>
</div>
}
@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any())
{
<div class="alert alert-warning">
<strong>@L["InvalidLoginRequest"]</strong>
@L["ThereAreNoLoginSchemesConfiguredForThisClient"]
</div>
}
````
Just changed the `@model` to `Acme.BookStore.Web.Pages.Account.CustomLoginModel` to use the customized `PageModel` class. You can change it however your application needs.
## The Source Code
You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization).
## See Also
* [ASP.NET Core (MVC / Razor Pages) User Interface Customization Guide](../UI/AspNetCore/Customization-User-Interface).

84
docs/en/How-To/Customize-SignIn-Manager.md

@ -0,0 +1,84 @@
# Customize the SignInManager
## Introduction
ABP Framework uses Microsoft Identity underneath hence supports customization as much as Microsoft Identity does.
## Sample Code
https://github.com/abpframework/abp-samples/blob/master/aspnet-core/BookStore-AzureAD/src/Acme.BookStore.Web/CustomSignInManager.cs
## Creating CustomSignInManager
To create your own custom SignIn Manager, you need to inherit `SignInManager<Volo.Abp.Identity.IdentityUser>`.
````xml
public class CustomSignInManager : SignInManager<Volo.Abp.Identity.IdentityUser>
{
public CustomSigninManager(
UserManager<Volo.Abp.Identity.IdentityUser> userManager,
IHttpContextAccessor contextAccessor,
IUserClaimsPrincipalFactory<Volo.Abp.Identity.IdentityUser> claimsFactory,
IOptions<IdentityOptions> optionsAccessor,
ILogger<SignInManager<Volo.Abp.Identity.IdentityUser>> logger,
IAuthenticationSchemeProvider schemes,
IUserConfirmation<Volo.Abp.Identity.IdentityUser> confirmation)
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
{
}
}
````
## Overriding Methods
Afterwards you can override a method like `GetExternalLoginInfoAsync`:
````xml
public override async Task<ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null)
{
var auth = await Context.AuthenticateAsync(IdentityConstants.ExternalScheme);
var items = auth?.Properties?.Items;
if (auth?.Principal == null || items == null || !items.ContainsKey("LoginProvider"))
{
return null;
}
if (expectedXsrf != null)
{
if (!items.ContainsKey("XsrfId"))
{
return null;
}
var userId = items[XsrfKey] as string;
if (userId != expectedXsrf)
{
return null;
}
var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
var provider = items[LoginProviderKey] as string;
if (providerKey == null || provider == null)
{
return null;
}
var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName ?? provider;
return new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName)
{
AuthenticationTokens = auth.Properties.GetTokens()
};
}
````
## Registering to DI
You need to register your Custom SignIn Manager to DI to activate it. Inside the `.Web` project, locate the `ApplicationNameWebModule` and add the following under `ConfigureServices` method:
````xml
context.Services
.GetObject<IdentityBuilder>()
.AddSignInManager<CustomSigninManager>();
````

9
docs/en/How-To/Index.md

@ -0,0 +1,9 @@
# "How To" Guides
This section contains "how to" guides for some specific questions frequently asked. While some of them are common development tasks and not directly related to the ABP Framework, we think it is useful to have some concrete examples those directly work with your ABP based applications.
## Authentication
* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md)
* [How to Use the Azure Active Directory Authentication for MVC / Razor Page Applications](Azure-Active-Directory-Authentication-MVC.md)
* [Customize the SignInManager](Customize-SignIn-Manager.md) (as an example of customization)

56
docs/en/UI/Angular/Content-Security-Strategy.md

@ -0,0 +1,56 @@
# ContentSecurityStrategy
`ContentSecurityStrategy` is an abstract class exposed by @abp/ng.core package. It helps you mark inline scripts or styles as safe in terms of [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy).
## API
### constructor(public nonce?: string)
- `nonce` enables whitelisting inline script or styles in order to avoid using `unsafe-inline` in [script-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#Unsafe_inline_script) and [style-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src#Unsafe_inline_styles) directives.
### applyCSP(element: HTMLScriptElement | HTMLStyleElement): void
This method maps the aforementioned properties to the given `element`.
## LooseContentSecurityPolicy
`LooseContentSecurityPolicy` is a class that extends `ContentSecurityStrategy`. It requires `nonce` and marks given `<script>` or `<style>` tag with it.
## StrictContentSecurityPolicy
`StrictContentSecurityPolicy` is a class that extends `ContentSecurityStrategy`. It does not mark inline scripts and styles as safe. You can consider it as a noop alternative.
## Predefined Content Security Strategies
Predefined content security strategies are accessible via `CONTENT_SECURITY_STRATEGY` constant.
### Loose(nonce: string)
`nonce` will be set.
### Strict()
Nothing will be done.
## What's Next?
TODO: Place new InsertionStrategy link here.

41
docs/en/UI/Angular/Cross-Origin-Strategy.md

@ -0,0 +1,41 @@
# CrossOriginStrategy
`CrossOriginStrategy` is a class exposed by @abp/ng.core package. Its instances define how a source referenced by an element will be retrieved by the browser and are consumed by other classes such as `LoadingStrategy`.
## API
### constructor(public crossorigin: 'anonymous' | 'use-credentials', public integrity?: string)
- `crossorigin` is mapped to [the HTML attribute with the same name](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin).
- `integrity` is a hash for validating a remote resource. Its use is explained [here](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity).
### setCrossOrigin(element: HTMLElement): void
This method maps the aforementioned properties to the given `element`.
## Predefined Cross-Origin Strategies
Predefined cross-origin strategies are accessible via `CROSS_ORIGIN_STRATEGY` constant.
### Anonymous(integrity?: string)
`crossorigin` will be set as `"anonymous"` and `integrity` is optional.
### UseCredentials(integrity?: string)
`crossorigin` will be set as `"use-credentials"` and `integrity` is optional.
## What's Next?
- [LoadingStrategy](./Loading-Strategy.md)

2
docs/en/UI/Angular/Custom-Setting-Page.md

@ -43,4 +43,4 @@ Navigate to `/setting-management` route to see the changes:
## What's Next?
- [TrackByService](./Track-By-Service.md)
- [Lazy Loading Scripts & Styles](./Lazy-Load-Service.md)

58
docs/en/UI/Angular/Dom-Strategy.md

@ -0,0 +1,58 @@
# DomStrategy
`DomStrategy` is a class exposed by @abp/ng.core package. Its instances define how an element will be attached to the DOM and are consumed by other classes such as `LoadingStrategy`.
## API
### constructor(public target?: HTMLElement, public position?: InsertPosition)
- `target` is an HTMLElement (_default: document.head_).
- `position` defines where the created element will be placed. All possible values of `position` can be found [here](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement) (_default: 'beforeend'_).
### insertElement(element: HTMLElement): void
This method inserts given `element` to `target` based on the `position`.
## Predefined Dom Strategies
Predefined dom strategies are accessible via `DOM_STRATEGY` constant.
### AppendToBody()
`insertElement` will place the given `element` at the end of `<body>`.
### AppendToHead()
`insertElement` will place the given `element` at the end of `<head>`.
### PrependToHead()
`insertElement` will place the given `element` at the beginning of `<head>`.
### AfterElement(target: HTMLElement)
`insertElement` will place the given `element` after (as a sibling to) the `target`.
### BeforeElement(target: HTMLElement)
`insertElement` will place the given `element` before (as a sibling to) the `target`.
## What's Next?
- [LoadingStrategy](./Loading-Strategy.md)
TODO: Place new InsertionStrategy link here.

205
docs/en/UI/Angular/Lazy-Load-Service.md

@ -0,0 +1,205 @@
# How to Lazy Load Scripts and Styles
You can use the `LazyLoadService` in @abp/ng.core package in order to lazy load scripts and styles in an easy and explicit way.
## Getting Started
You do not have to provide the `LazyLoadService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services.
```js
import { LazyLoadService } from '@abp/ng.core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private lazyLoadService: LazyLoadService) {}
}
```
## Usage
You can use the `load` method of `LazyLoadService` to create a `<script>` or `<link>` element in the DOM at the desired position and force the browser to download the target resource.
### How to Load Scripts
The first parameter of `load` method expects a `LoadingStrategy`. If you pass a `ScriptLoadingStrategy` instance, the `LazyLoadService` will create a `<script>` element with given `src` and place it in the designated DOM position.
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
@Component({
template: `
<some-component *ngIf="libraryLoaded$ | async"></some-component>
`
})
class DemoComponent {
libraryLoaded$ = this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/some-library.js'),
);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
The `load` method returns an observable to which you can subscibe in your component or with an `async` pipe. In the example above, the `NgIf` directive will render `<some-component>` only **if the script gets successfully loaded or is already loaded before**.
> You can subscribe multiple times in your template with `async` pipe. The styles will only be loaded once.
Please refer to [LoadingStrategy](./Loading-Strategy.md) to see all available loading strategies and how you can build your own loading strategy.
### How to Load Styles
If you pass a `StyleLoadingStrategy` instance as the first parameter of `load` method, the `LazyLoadService` will create a `<link>` element with given `href` and place it in the designated DOM position.
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
@Component({
template: `
<some-component *ngIf="stylesLoaded$ | async"></some-component>
`
})
class DemoComponent {
stylesLoaded$ = this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousStyleToHead('/assets/some-styles.css'),
);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
The `load` method returns an observable to which you can subscibe in your component or with an `AsyncPipe`. In the example above, the `NgIf` directive will render `<some-component>` only **if the style gets successfully loaded or is already loaded before**.
> You can subscribe multiple times in your template with `async` pipe. The styles will only be loaded once.
Please refer to [LoadingStrategy](./Loading-Strategy.md) to see all available loading strategies and how you can build your own loading strategy.
### Advanced Usage
You have quite a bit of **freedom to define how your lazy load will work**. Here is an example:
```js
const domStrategy = DOM_STRATEGY.PrependToHead();
const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous(
'sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh',
);
const loadingStrategy = new StyleLoadingStrategy(
'https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css',
domStrategy,
crossOriginStrategy,
);
this.lazyLoad.load(loadingStrategy, 1, 2000);
```
This code will create a `<link>` element with given url and integrity hash, insert it to to top of the `<head>` element, and retry once after 2 seconds if first try fails.
A common usecase is **loading multiple scripts and/or styles before using a feature**:
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
import { frokJoin } from 'rxjs';
@Component({
template: `
<some-component *ngIf="scriptsAndStylesLoaded$ | async"></some-component>
`
})
class DemoComponent {
private stylesLoaded$ = forkJoin(
this.lazyLoad.load(
LOADING_STRATEGY.PrependAnonymousStyleToHead('/assets/library-dark-theme.css'),
),
this.lazyLoad.load(
LOADING_STRATEGY.PrependAnonymousStyleToHead('/assets/library.css'),
),
);
private scriptsLoaded$ = forkJoin(
this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/library.js'),
),
this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/other-library.css'),
),
);
scriptsAndStylesLoaded$ = forkJoin(this.scriptsLoaded$, this.stylesLoaded$);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
RxJS `forkJoin` will load all scripts and styles in parallel and emit only when all of them are loaded. So, when `<some-component>` is placed, all required dependencies will be available.
> Noticed we have prepended styles to the document head? This is sometimes necessary, because your application styles may be overriding some of the library styles. In such a case, you must be careful about the order of prepended styles. They will be placed one-by-one and, **when prepending, the last one placed will be on top**.
Another frequent usecase is **loading dependent scripts in order**:
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
import { concat } from 'rxjs';
@Component({
template: `
<some-component *ngIf="scriptsLoaded$ | async"></some-component>
`
})
class DemoComponent {
scriptsLoaded$ = concat(
this.lazyLoad.load(
LOADING_STRATEGY.PrependAnonymousScriptToHead('/assets/library.js'),
),
this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/script-that-requires-library.js'),
),
);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
In this example, the second file needs the first one to be loaded beforehand. RxJS `concat` function will let you load all scripts one-by-one in the given order and emit only when all of them are loaded.
## API
### loaded: Set<string>
All previously loaded paths are available via this property. It is a simple [JavaScript Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set).
### load(strategy: LoadingStrategy, retryTimes?: number, retryDelay?: number): Observable<Event>
- `strategy` parameter is the primary focus here and is explained above.
- `retryTimes` defines how many times the loading will be tried again before fail (_default: 2_).
- `retryDelay` defines how much delay there will be between retries (_default: 1000_).
## What's Next?
- [TrackByService](./Track-By-Service.md)

77
docs/en/UI/Angular/Loading-Strategy.md

@ -0,0 +1,77 @@
# LoadingStrategy
`LoadingStrategy` is an abstract class exposed by @abp/ng.core package. There are two loading strategies extending it: `ScriptLoadingStrategy` and `StyleLoadingStrategy`. Implementing the same methods and properties, both of these strategies help you define how your lazy loading will work.
## API
### constructor(public path: string, protected domStrategy?: DomStrategy, protected crossOriginStrategy?: CrossOriginStrategy)
- `path` is set to `<script>` elements as `src` and `<link>` elements as `href` attribute.
- `domStrategy` is the `DomStrategy` that will be used when inserting the created element. (_default: AppendToHead_)
- `crossOriginStrategy` is the `CrossOriginStrategy` that will be used on the created element before inserting it. (_default: Anonymous_)
Please refer to [DomStrategy](./Dom-Strategy.md) and [CrossOriginStrategy](./Cross-Origin-Strategy.md) documentation for their usage.
### createElement(): HTMLScriptElement | HTMLLinkElement
This method creates and returns a `<script>` or `<link>` element with `path` set as `src` or `href`.
### createStream(): Observable<Event>
This method creates and returns an observable stream that emits on success and throws on error.
## ScriptLoadingStrategy
`ScriptLoadingStrategy` is a class that extends `LoadingStrategy`. It lets you **lazy load a script**.
## StyleLoadingStrategy
`StyleLoadingStrategy` is a class that extends `LoadingStrategy`. It lets you **lazy load a style**.
## Predefined Loading Strategies
Predefined content security strategies are accessible via `LOADING_STRATEGY` constant.
### AppendAnonymousScriptToHead(src: string, integrity?: string)
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **end** of `<head>` tag in the document.
### PrependAnonymousScriptToHead(src: string, integrity?: string)
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **beginning** of `<head>` tag in the document.
### AppendAnonymousScriptToBody(src: string, integrity?: string)
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<script>` element and places it at the **end** of `<body>` tag in the document.
### AppendAnonymousStyleToHead(href: string, integrity?: string)
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<style>` element and places it at the **end** of `<head>` tag in the document.
### PrependAnonymousStyleToHead(href: string, integrity?: string)
Sets given paremeters and `crossorigin="anonymous"` as attributes of created `<style>` element and places it at the **beginning** of `<head>` tag in the document.
## What's Next?
- [LazyLoadService](./Lazy-Load-Service.md)

8
docs/en/docs-nav.json

@ -75,6 +75,10 @@
}
]
},
{
"text": "\"How to\" Guides",
"path": "How-To/Index.md"
},
{
"text": "Migrating from the ASP.NET Boilerplate",
"path": "AspNet-Boilerplate-Migration-Guide.md"
@ -341,6 +345,10 @@
"text": "Custom Setting Page",
"path": "UI/Angular/Custom-Setting-Page.md"
},
{
"text": "Lazy Loading Scripts & Styles",
"path": "UI/Angular/Lazy-Load-Service.md"
},
{
"text": "TrackByService",
"path": "UI/Angular/Track-By-Service.md"

47
docs/zh-Hans/Application-Services.md

@ -146,7 +146,6 @@ public interface IBookAppService : IApplicationService
`BookDto`是一个简单的[DTO](Data-Transfer-Objects.md)类, 定义如下:
````csharp
[AbpAutoMapFrom(typeof(Book))] //Defines the mapping
public class BookDto
{
public Guid Id { get; set; }
@ -159,7 +158,36 @@ public class BookDto
}
````
* `BookDto`定义了`[AbpAutoMapFrom(typeof(Book))]`属性来从创建对象映射Book到BookDto.
我们创建一个Automapper的[Profile](https://docs.automapper.org/en/stable/Configuration.html#profile-instances)类. 例如:
```csharp
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<Book, BookDto>();
}
}
```
然后使用`AbpAutoMapperOptions`注册配置文件:
````csharp
[DependsOn(typeof(AbpAutoMapperModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpAutoMapperOptions>(options =>
{
//Add all mappings defined in the assembly of the MyModule class
options.AddMaps<MyModule>();
});
}
}
````
`AddMaps` 注册给定类的程序集中所有的配置类,通常使用模块类. 它还会注册 [attribute 映射](https://docs.automapper.org/en/stable/Attribute-mapping.html). 更多信息请参考[对象到对象映射](Object-To-Object-Mapping.md)文档
然后你可以实现`GetAsync`方法. 如下所示:
@ -249,7 +277,6 @@ public interface ICrudAppService<
示例中使用的DTO类是`BookDto`和`CreateUpdateBookDto`:
````csharp
[AbpAutoMapFrom(typeof(Book))]
public class BookDto : AuditedEntityDto<Guid>
{
public string Name { get; set; }
@ -259,7 +286,6 @@ public class BookDto : AuditedEntityDto<Guid>
public float Price { get; set; }
}
[AbpAutoMapTo(typeof(Book))]
public class CreateUpdateBookDto
{
[Required]
@ -274,6 +300,19 @@ public class CreateUpdateBookDto
}
````
DTO类的[Profile](https://docs.automapper.org/en/stable/Configuration.html#profile-instances)类.
```csharp
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<Book, BookDto>();
CreateMap<CreateUpdateBookDto, Book>();
}
}
```
* `CreateUpdateBookDto`由创建和更新操作共享,但你也可以使用单独的DTO类.
最后`BookAppService`实现非常简单:

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Popover/AbpPopoverTagHelper.cs

@ -18,6 +18,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Popover
public bool? Dismissible { get; set; }
public bool? Hoverable { get; set; }
public string AbpPopover { get; set; }
public string AbpPopoverRight { get; set; }

60
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Popover/AbpPopoverTagHelperService.cs

@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Popover
@ -13,34 +14,67 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Popover
SetDataPlacement(context, output);
SetPopoverData(context, output);
SetDataTriggerIfDismissible(context, output);
SetDataTriggerIfHoverable(context, output);
}
else
{
SetDisabled(context, output);
}
}
}
protected virtual void SetDisabled(TagHelperContext context, TagHelperOutput output)
{
var triggerAsHtml = TagHelper.Dismissible ?? false ? "datatrigger=\"focus\" " : "";
var triggerAsHtml = TagHelper.Dismissible ?? false ? "data-trigger=\"focus\" " : "";
if (TagHelper.Hoverable ?? false)
{
if (triggerAsHtml.Contains("focus"))
{
triggerAsHtml = triggerAsHtml.Replace("focus", "focus hover");
}
else
{
triggerAsHtml = "data-trigger=\"hover\" ";
}
}
var dataPlacementAsHtml = "data-placement=\"" + GetDirectory().ToString().ToLowerInvariant() + "\" ";
// data-placement="default" with data-trigger="focus" causes Cannot read property 'indexOf' of undefined at computeAutoPlacement(bootstrap.bundle.js?_v=637146714627330435:2185) error
if (IsDismissibleOrHoverable() && GetDirectory() == PopoverDirectory.Default)
{
//dataPlacementAsHtml = string.Empty; //bootstrap default placement is right, abp's is top.
dataPlacementAsHtml = dataPlacementAsHtml.Replace("default", "top");
}
var titleAttribute = output.Attributes.FirstOrDefault(at => at.Name == "title");
var titleAsHtml = titleAttribute == null ? "" : "title=\"" + titleAttribute.Value + "\" ";
var preElementHtml = "<span class=\"d-inline-block\" " + titleAsHtml + triggerAsHtml + dataPlacementAsHtml + "data-toggle=\"popover\" data-content=\"" + GetDataContent() + "\">";
var preElementHtml = "<span tabindex=\"0\" class=\"d-inline-block\" " + titleAsHtml + triggerAsHtml + dataPlacementAsHtml + "data-toggle=\"popover\" data-content=\"" + GetDataContent() + "\">";
var postElementHtml = "</span>";
output.PreElement.SetHtmlContent(preElementHtml);
output.PostElement.SetHtmlContent(postElementHtml);
output.Attributes.Add("style", "pointer-events: none;");
}
}
protected virtual void SetDataTriggerIfDismissible(TagHelperContext context, TagHelperOutput output)
{
if (TagHelper.Dismissible ?? false)
{
output.Attributes.Add("data-trigger", "focus");
}
}
protected virtual void SetDataTriggerIfHoverable(TagHelperContext context, TagHelperOutput output)
{
if (TagHelper.Hoverable ?? false)
{
//If already has focus data trigger
if (output.Attributes.TryGetAttribute("data-trigger", out _))
{
output.Attributes.SetAttribute(new TagHelperAttribute("data-trigger", "focus hover"));
}
else
{
output.Attributes.Add("data-trigger", "hover");
}
}
}
protected virtual void SetDataToggle(TagHelperContext context, TagHelperOutput output)
@ -101,5 +135,17 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Popover
return PopoverDirectory.Default;
}
protected virtual bool IsDismissibleOrHoverable()
{
if (TagHelper.Dismissible ?? false)
{
return true;
}
if (TagHelper.Hoverable ?? false)
{
return true;
}
return false;
}
}
}

5
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperResourceService.cs

@ -63,9 +63,10 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling.TagHelpers
foreach (var bundleFile in bundleFiles)
{
var file = WebContentFileProvider.GetFileInfo(bundleFile);
if (file == null)
if (file == null || !file.Exists)
{
throw new AbpException($"Could not find the bundle file from {nameof(IWebContentFileProvider)}");
throw new AbpException($"Could not find the bundle file '{bundleFile}' from {nameof(IWebContentFileProvider)}");
}
AddHtmlTag(context, output, bundleFile + "?_v=" + file.LastModified.UtcTicks);

11
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/TuiEditor/TuiEditorScriptContributor.cs

@ -17,7 +17,16 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.TuiEditor
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("/libs/to-mark/to-mark.min.js");
context.Files.AddIfNotContains("/libs/tui-code-snippet/tui-code-snippet.min.js");
if (context.FileProvider.GetFileInfo("/libs/tui-code-snippet/tui-code-snippet.min.js").Exists)
{
context.Files.AddIfNotContains("/libs/tui-code-snippet/tui-code-snippet.min.js");
}
else
{
context.Files.AddIfNotContains("/libs/tui-code-snippet/tui-code-snippet.js");
}
context.Files.AddIfNotContains("/libs/squire-rte/squire.js");
context.Files.AddIfNotContains("/libs/tui-editor/tui-editor-Editor.min.js");
}

23
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@ -51,9 +52,14 @@ namespace Volo.Abp.Cli.ProjectModification
{
var fileDirectory = Path.GetDirectoryName(file).EnsureEndsWith(Path.DirectorySeparatorChar);
if (IsAngularProject(fileDirectory))
{
CreateNpmrcFile(Path.GetDirectoryName(file));
}
RunYarn(fileDirectory);
if (IsAngularProject(fileDirectory) == false)
if (!IsAngularProject(fileDirectory))
{
Thread.Sleep(500);
RunGulp(fileDirectory);
@ -62,6 +68,21 @@ namespace Volo.Abp.Cli.ProjectModification
}
}
private void CreateNpmrcFile(string directoryName)
{
var fileName = Path.Combine(directoryName, ".npmrc");
if (File.Exists(fileName))
{
return;
}
using var fs = File.Create(fileName);
var content = new UTF8Encoding(true).GetBytes("@abp:registry:https://www.myget.org/F/abp-nightly/npm");
fs.Write(content, 0, content.Length);
}
private bool IsAngularProject(string fileDirectory)
{
return File.Exists(Path.Combine(fileDirectory, "angular.json"));

5
framework/src/Volo.Abp.Core/Microsoft/Extensions/Configuration/ConfigurationHelper.cs

@ -6,7 +6,8 @@ namespace Microsoft.Extensions.Configuration
public static class ConfigurationHelper
{
public static IConfigurationRoot BuildConfiguration(
AbpConfigurationBuilderOptions options = null)
AbpConfigurationBuilderOptions options = null,
Action<IConfigurationBuilder> builderAction = null)
{
options = options ?? new AbpConfigurationBuilderOptions();
@ -43,6 +44,8 @@ namespace Microsoft.Extensions.Configuration
builder = builder.AddCommandLine(options.CommandLineArgs);
}
builderAction?.Invoke(builder);
return builder.Build();
}
}

12
framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs

@ -1,7 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
@ -15,7 +14,7 @@ namespace Volo.Abp.Reflection
{
return false;
}
var type = obj.GetType();
if (!type.GetTypeInfo().IsGenericType)
{
@ -37,9 +36,7 @@ namespace Volo.Abp.Reflection
return true;
}
if (includeNullables &&
type.IsGenericType &&
type.GetGenericTypeDefinition() == typeof(Nullable<>))
if (includeNullables && IsNullable(type))
{
return IsPrimitiveExtendedInternal(type.GenericTypeArguments[0], includeEnums);
}
@ -47,6 +44,11 @@ namespace Volo.Abp.Reflection
return false;
}
public static bool IsNullable(Type type)
{
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
}
public static Type GetFirstGenericArgumentIfNullable(this Type t)
{
if (t.GetGenericArguments().Length > 0 && t.GetGenericTypeDefinition() == typeof(Nullable<>))

15
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs

@ -56,7 +56,7 @@ namespace Volo.Abp.Http.Client.DynamicProxying
IJsonSerializer jsonSerializer,
IRemoteServiceHttpClientAuthenticator clientAuthenticator,
ICancellationTokenProvider cancellationTokenProvider,
ICorrelationIdProvider correlationIdProvider,
ICorrelationIdProvider correlationIdProvider,
IOptions<AbpCorrelationIdOptions> correlationIdOptions,
ICurrentTenant currentTenant)
{
@ -110,7 +110,14 @@ namespace Volo.Abp.Http.Client.DynamicProxying
//TODO: Think on that
if (TypeHelper.IsPrimitiveExtended(typeof(T), true))
{
return (T)Convert.ChangeType(responseAsString, typeof(T));
if (typeof(DateTime).IsAssignableFrom(typeof(T)))
{
return (T)(object)DateTime.Parse(responseAsString.Trim('\"'), CultureInfo.InvariantCulture);
}
else
{
return (T)Convert.ChangeType(responseAsString, typeof(T));
}
}
return JsonSerializer.Deserialize<T>(responseAsString);
@ -151,8 +158,8 @@ namespace Volo.Abp.Http.Client.DynamicProxying
}
return await response.Content.ReadAsStringAsync();
}
}
private ApiVersionInfo GetApiVersionInfo(ActionApiDescriptionModel action)
{
var apiVersion = FindBestApiVersion(action);

22
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/UrlBuilder.cs

@ -1,9 +1,12 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using JetBrains.Annotations;
using Volo.Abp.Http.Modeling;
using Volo.Abp.Http.ProxyScripting.Generators;
using Volo.Abp.Reflection;
namespace Volo.Abp.Http.Client.DynamicProxying
{
@ -88,10 +91,25 @@ namespace Volo.Abp.Http.Client.DynamicProxying
}
}
private static void AddQueryStringParameter(StringBuilder urlBuilder, bool isFirstParam, string name, object value)
private static void AddQueryStringParameter(
StringBuilder urlBuilder,
bool isFirstParam,
string name,
[NotNull] object value)
{
urlBuilder.Append(isFirstParam ? "?" : "&");
urlBuilder.Append(name + "=" + System.Net.WebUtility.UrlEncode(value.ToString()));
urlBuilder.Append(name + "=" + System.Net.WebUtility.UrlEncode(ConvertValueToString(value)));
}
private static string ConvertValueToString([NotNull] object value)
{
if (value is DateTime dateTimeValue)
{
return dateTimeValue.ToUniversalTime().ToString("u");
}
return value.ToString();
}
}
}

8
framework/src/Volo.Abp.Quartz/Volo/Abp/Quartz/AbpQuartzModule.cs

@ -25,21 +25,21 @@ namespace Volo.Abp.Quartz
_scheduler = context.ServiceProvider.GetService<IScheduler>();
_scheduler.JobFactory = context.ServiceProvider.GetService<IJobFactory>();
if (options.StartDelay.Ticks > 0)
{
_scheduler.StartDelayed(options.StartDelay);
AsyncHelper.RunSync(() => _scheduler.StartDelayed(options.StartDelay));
}
else
{
_scheduler.Start();
AsyncHelper.RunSync(() => _scheduler.Start());
}
}
public override void OnApplicationShutdown(ApplicationShutdownContext context)
{
//TODO: ABP may provide two methods for application shutdown: OnPreApplicationShutdown & OnApplicationShutdown
_scheduler.Shutdown();
AsyncHelper.RunSync(() => _scheduler.Shutdown());
}
}
}

31
framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Popovers.cshtml

@ -31,20 +31,23 @@
<h4>Example</h4>
<div class="demo-with-code">
<div class="demo-area">
<abp-button abp-popover="Hi, i'm popover content!">
Popover Default
</abp-button>
<abp-button abp-popover-top="Hi, i'm popover content!" title="Popover Title">
Popover With Title
</abp-button>
<abp-button abp-popover-right="Hi, i'm popover content!" title="Popover Title" dismissible="true">
Dismissible Popover
</abp-button>
<abp-button abp-popover-left="Hi, i'm popover content!" title="Popover Title" disabled="true">
Disabled Popover
</abp-button>
<div class="demo-area">
<abp-button abp-popover="Hi, i'm popover content!">
Popover Default
</abp-button>
<abp-button abp-popover-top="Hi, i'm hoverable popover content!" title="Popover Title" hoverable="true">
Popover With Title
</abp-button>
<abp-button abp-popover-right="Hi, i'm dismissible popover content!" title="Popover Title" dismissible="true">
Dismissible Popover
</abp-button>
<abp-button abp-popover-left="Hi, i'm disabled popover content!" title="Popover Title" disabled="true">
Disabled Popover
</abp-button>
<abp-button abp-popover="Hi, i'm disabled dismissible hoverable popover content!" title="Popover Title" disabled="true" dismissible="true" hoverable="true">
Disabled Popover
</abp-button>
</div>
<div class="code-area">
<abp-tabs>

5
framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/IRegularTestController.cs

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
namespace Volo.Abp.Http.DynamicProxying
{
@ -8,6 +9,8 @@ namespace Volo.Abp.Http.DynamicProxying
Task GetException1Async();
Task<DateTime> GetWithDateTimeParameterAsync(DateTime dateTime1);
Task<string> PostValueWithHeaderAndQueryStringAsync(string headerValue, string qsValue);
Task<string> PostValueWithBodyAsync(string bodyValue);

22
framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestController.cs

@ -1,4 +1,6 @@
using System.Threading.Tasks;
using System;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Application.Services;
using Volo.Abp.AspNetCore.Mvc;
@ -25,6 +27,14 @@ namespace Volo.Abp.Http.DynamicProxying
throw new UserFriendlyException("This is an error message!");
}
[HttpGet]
[Route("get-with-datetime-parameter")]
public Task<DateTime> GetWithDateTimeParameterAsync(DateTime dateTime1)
{
var culture = CultureInfo.CurrentCulture;
return Task.FromResult(dateTime1);
}
[HttpPost]
[Route("post-with-header-and-qs")]
public Task<string> PostValueWithHeaderAndQueryStringAsync([FromHeader] string headerValue, [FromQuery] string qsValue)
@ -48,7 +58,7 @@ namespace Volo.Abp.Http.DynamicProxying
[HttpPost]
[Route("post-object-with-query")]
public Task<Car> PostObjectWithQueryAsync( Car bodyValue)
public Task<Car> PostObjectWithQueryAsync(Car bodyValue)
{
return Task.FromResult(bodyValue);
}
@ -59,7 +69,7 @@ namespace Volo.Abp.Http.DynamicProxying
{
return Task.FromResult(bodyValue);
}
[HttpGet]
[Route("post-object-and-id-with-url/{id}")]
public Task<Car> GetObjectandIdAsync(int id, [FromBody] Car bodyValue)
@ -67,7 +77,7 @@ namespace Volo.Abp.Http.DynamicProxying
bodyValue.Year = id;
return Task.FromResult(bodyValue);
}
[HttpGet]
[Route("post-object-and-id-with-url-and-query/{id}")]
public Task<Car> GetObjectAndIdWithQueryAsync(int id, Car bodyValue)
@ -116,7 +126,11 @@ namespace Volo.Abp.Http.DynamicProxying
{
[FromQuery]
public int Year { get; set; }
[FromQuery]
public string Model { get; set; }
[FromQuery]
public DateTime FirstReleaseDate { get; set; }
}
}

46
framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestControllerClientProxy_Tests.cs

@ -1,7 +1,9 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.Http.Client;
using Volo.Abp.Localization;
using Xunit;
namespace Volo.Abp.Http.DynamicProxying
@ -30,6 +32,25 @@ namespace Volo.Abp.Http.DynamicProxying
exception.Error.Message.ShouldBe("This is an error message!");
}
[Fact]
public async Task GetWithDateTimeParameterAsync()
{
var dateTime1 = new DateTime(2020, 04, 19, 19, 05, 01);
var result = await _controller.GetWithDateTimeParameterAsync(dateTime1);
result.ShouldBe(dateTime1);
}
[Fact]
public async Task GetWithDateTimeParameterAsync_With_Different_Culture()
{
using (CultureHelper.Use("es"))
{
var dateTime1 = new DateTime(2020, 04, 19, 19, 05, 01);
var result = await _controller.GetWithDateTimeParameterAsync(dateTime1);
result.ShouldBe(dateTime1);
}
}
[Fact]
public async Task PostValueWithHeaderAndQueryStringAsync()
{
@ -54,7 +75,7 @@ namespace Volo.Abp.Http.DynamicProxying
[Fact]
public async Task PostObjectWithBodyAsync()
{
var result = await _controller.PostObjectWithBodyAsync(new Car { Year = 1976, Model = "Ford" });
var result = await _controller.PostObjectWithBodyAsync(new Car { Year = 1976, Model = "Ford", FirstReleaseDate = new DateTime(1976, 02, 22, 15, 0, 6, 22) });
result.Year.ShouldBe(1976);
result.Model.ShouldBe("Ford");
}
@ -62,15 +83,26 @@ namespace Volo.Abp.Http.DynamicProxying
[Fact]
public async Task PostObjectWithQueryAsync()
{
var result = await _controller.PostObjectWithQueryAsync(new Car { Year = 1976, Model = "Ford" });
var result = await _controller.PostObjectWithQueryAsync(new Car { Year = 1976, Model = "Ford", FirstReleaseDate = new DateTime(1976, 02, 22, 15, 0, 6, 22) });
result.Year.ShouldBe(1976);
result.Model.ShouldBe("Ford");
}
[Fact]
public async Task PostObjectWithQueryAsync_With_Different_Culture()
{
using (CultureHelper.Use("tr"))
{
var result = await _controller.PostObjectWithQueryAsync(new Car { Year = 1976, Model = "Ford", FirstReleaseDate = new DateTime(1976, 02, 22, 15, 0, 6, 22) });
result.Year.ShouldBe(1976);
result.Model.ShouldBe("Ford");
}
}
[Fact]
public async Task GetObjectWithUrlAsync()
{
var result = await _controller.GetObjectWithUrlAsync(new Car { Year = 1976, Model = "Ford" });
var result = await _controller.GetObjectWithUrlAsync(new Car { Year = 1976, Model = "Ford", FirstReleaseDate = new DateTime(1976, 02, 22, 15, 0, 6, 22) });
result.Year.ShouldBe(1976);
result.Model.ShouldBe("Ford");
}
@ -78,7 +110,7 @@ namespace Volo.Abp.Http.DynamicProxying
[Fact]
public async Task GetObjectandIdAsync()
{
var result = await _controller.GetObjectandIdAsync(42, new Car { Year = 1976, Model = "Ford" });
var result = await _controller.GetObjectandIdAsync(42, new Car { Year = 1976, Model = "Ford", FirstReleaseDate = new DateTime(1976, 02, 22, 15, 0, 6, 22) });
result.Year.ShouldBe(42);
result.Model.ShouldBe("Ford");
}
@ -86,7 +118,7 @@ namespace Volo.Abp.Http.DynamicProxying
[Fact]
public async Task GetObjectAndIdWithQueryAsync()
{
var result = await _controller.GetObjectAndIdWithQueryAsync(42, new Car { Year = 1976, Model = "Ford" });
var result = await _controller.GetObjectAndIdWithQueryAsync(42, new Car { Year = 1976, Model = "Ford", FirstReleaseDate = new DateTime(1976, 02, 22, 15, 0, 6, 22) });
result.Year.ShouldBe(42);
result.Model.ShouldBe("Ford");
}
@ -97,7 +129,7 @@ namespace Volo.Abp.Http.DynamicProxying
var result = await _controller.PatchValueWithBodyAsync("mybody");
result.ShouldBe("mybody");
}
[Fact]
public async Task PutValueWithHeaderAndQueryStringAsync()
{

19
modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs

@ -68,18 +68,11 @@ namespace Volo.Abp.Account.Web.Pages.Account
return Page();
}
var schemes = await SchemeProvider.GetAllSchemesAsync();
var providers = schemes
.Where(x => x.DisplayName != null || x.Name.Equals(AccountOptions.WindowsAuthenticationSchemeName, StringComparison.OrdinalIgnoreCase))
.Select(x => new ExternalProviderModel
{
DisplayName = x.DisplayName,
AuthenticationScheme = x.Name
})
.ToList();
var providers = await GetExternalProviders();
ExternalProviders = providers.ToList();
EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin);
if (context?.ClientId != null)
{
var client = await ClientStore.FindEnabledClientByIdAsync(context.ClientId);
@ -94,8 +87,6 @@ namespace Volo.Abp.Account.Web.Pages.Account
}
}
ExternalProviders = providers.ToArray();
if (IsExternalLoginOnly)
{
return await base.OnPostExternalLogin(providers.First().AuthenticationScheme);
@ -124,6 +115,10 @@ namespace Volo.Abp.Account.Web.Pages.Account
ValidateModel();
ExternalProviders = await GetExternalProviders();
EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin);
await ReplaceEmailToUsernameOfInputIfNeeds();
var result = await SignInManager.PasswordSignInAsync(

31
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs

@ -63,21 +63,10 @@ namespace Volo.Abp.Account.Web.Pages.Account
{
LoginInput = new LoginInputModel();
var schemes = await SchemeProvider.GetAllSchemesAsync();
var providers = schemes
.Where(x => x.DisplayName != null || x.Name.Equals(AccountOptions.WindowsAuthenticationSchemeName, StringComparison.OrdinalIgnoreCase))
.Select(x => new ExternalProviderModel
{
DisplayName = x.DisplayName,
AuthenticationScheme = x.Name
})
.ToList();
ExternalProviders = await GetExternalProviders();
EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin);
ExternalProviders = providers.ToArray();
if (IsExternalLoginOnly)
{
//return await ExternalLogin(vm.ExternalLoginScheme, returnUrl);
@ -94,6 +83,10 @@ namespace Volo.Abp.Account.Web.Pages.Account
ValidateModel();
ExternalProviders = await GetExternalProviders();
EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin);
await ReplaceEmailToUsernameOfInputIfNeeds();
var result = await SignInManager.PasswordSignInAsync(
@ -140,6 +133,20 @@ namespace Volo.Abp.Account.Web.Pages.Account
return RedirectSafely(ReturnUrl, ReturnUrlHash);
}
protected virtual async Task<List<ExternalProviderModel>> GetExternalProviders()
{
var schemes = await SchemeProvider.GetAllSchemesAsync();
return schemes
.Where(x => x.DisplayName != null || x.Name.Equals(AccountOptions.WindowsAuthenticationSchemeName, StringComparison.OrdinalIgnoreCase))
.Select(x => new ExternalProviderModel
{
DisplayName = x.DisplayName,
AuthenticationScheme = x.Name
})
.ToList();
}
[UnitOfWork]
public virtual async Task<IActionResult> OnPostExternalLogin(string provider)
{

2
modules/audit-logging/src/Volo.Abp.AuditLogging.Domain/Volo/Abp/AuditLogging/IAuditLogRepository.cs

@ -46,7 +46,7 @@ namespace Volo.Abp.AuditLogging
DateTime startDate,
DateTime endDate);
Task<EntityChange> GetEntityChange(Guid auditLogId, Guid entityChangeId, bool includeDetails = true);
Task<EntityChange> GetEntityChange(Guid entityChangeId);
Task<List<EntityChange>> GetEntityChangeListAsync(
string sorting = null,

5
modules/audit-logging/src/Volo.Abp.AuditLogging.EntityFrameworkCore/Volo/Abp/AuditLogging/EntityFrameworkCore/EfCoreAuditLogRepository.cs

@ -141,10 +141,9 @@ namespace Volo.Abp.AuditLogging.EntityFrameworkCore
return GetQueryable().IncludeDetails();
}
public Task<EntityChange> GetEntityChange(Guid auditLogId, Guid entityChangeId, bool includeDetails = true)
public Task<EntityChange> GetEntityChange(Guid entityChangeId)
{
return DbContext.Set<EntityChange>().AsNoTracking().IncludeDetails(includeDetails)
.Where(x => x.Id == entityChangeId && x.AuditLogId == auditLogId).FirstAsync();
return DbContext.Set<EntityChange>().AsNoTracking().IncludeDetails().Where(x => x.Id == entityChangeId).FirstAsync();
}
public virtual async Task<List<EntityChange>> GetEntityChangeListAsync(

7
modules/audit-logging/src/Volo.Abp.AuditLogging.MongoDB/Volo/Abp/AuditLogging/MongoDB/MongoAuditLogRepository.cs

@ -140,11 +140,12 @@ namespace Volo.Abp.AuditLogging.MongoDB
return result.ToDictionary(element => element.Day.ClearTime(), element => element.avgExecutionTime);
}
public virtual async Task<EntityChange> GetEntityChange(Guid auditLogId, Guid entityChangeId, bool includeDetails = true)
public virtual async Task<EntityChange> GetEntityChange(Guid entityChangeId)
{
return (await GetMongoQueryable()
.Where(x => x.Id == auditLogId && x.EntityChanges.Any(y => y.Id == entityChangeId)).FirstAsync())
.EntityChanges.First(x => x.Id == entityChangeId);
.Where(x => x.EntityChanges.Any(y => y.Id == entityChangeId))
.FirstAsync()
).EntityChanges.First(x => x.Id == entityChangeId);
}
public virtual async Task<List<EntityChange>> GetEntityChangeListAsync(

2
modules/audit-logging/test/Volo.Abp.AuditLogging.TestBase/Volo/Abp/AuditLogging/AuditLogRepository_Tests.cs

@ -538,7 +538,7 @@ namespace Volo.Abp.AuditLogging
var entityChanges = await AuditLogRepository.GetEntityChangeListAsync();
var entityChange =
await AuditLogRepository.GetEntityChange(entityChanges.First().AuditLogId, entityChanges.First().Id);
await AuditLogRepository.GetEntityChange(entityChanges.First().Id);
entityChange.ChangeTime.ShouldBe(entityChanges.First().ChangeTime);
}

10
npm/ng-packs/angular.json

@ -32,7 +32,6 @@
}
}
},
"theme-shared": {
"projectType": "library",
"root": "packages/theme-shared",
@ -436,7 +435,7 @@
"index": "apps/dev-app/src/index.html",
"main": "apps/dev-app/src/main.ts",
"polyfills": "apps/dev-app/src/polyfills.ts",
"tsConfig": "apps/dev-app/tsconfig.app.json",
"tsConfig": "apps/dev-app/tsconfig.prod.json",
"aot": false,
"assets": ["apps/dev-app/src/favicon.ico", "apps/dev-app/src/assets"],
"extractCss": true,
@ -487,6 +486,7 @@
]
},
"internal": {
"tsConfig": "apps/dev-app/tsconfig.dev.json",
"fileReplacements": [
{
"replace": "apps/dev-app/src/environments/environment.ts",
@ -519,7 +519,11 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": ["apps/dev-app/tsconfig.app.json", "apps/dev-app/tsconfig.spec.json"],
"tsConfig": [
"apps/dev-app/tsconfig.dev.json",
"apps/dev-app/tsconfig.prod.json",
"apps/dev-app/tsconfig.spec.json"
],
"exclude": ["**/node_modules/**"]
}
}

20
npm/ng-packs/apps/dev-app/src/app/app.component.ts

@ -1,5 +1,6 @@
import { LazyLoadService } from '@abp/ng.core';
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { forkJoin } from 'rxjs';
@Component({
selector: 'app-root',
@ -12,14 +13,13 @@ export class AppComponent implements OnInit {
constructor(private lazyLoadService: LazyLoadService) {}
ngOnInit() {
this.lazyLoadService
.load(
['fontawesome-all.min.css', 'fontawesome-v4-shims.min.css'],
'style',
null,
'head',
'afterbegin',
)
.subscribe();
forkJoin(
this.lazyLoadService.load(
LOADING_STRATEGY.PrependAnonymousStyleToHead('fontawesome-v4-shims.min.css'),
),
this.lazyLoadService.load(
LOADING_STRATEGY.PrependAnonymousStyleToHead('fontawesome-all.min.css'),
),
).subscribe();
}
}

10
npm/ng-packs/apps/dev-app/tsconfig.dev.json

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/app",
"types": []
},
"files": ["src/main.ts", "src/polyfills.ts"],
"include": ["src/**/*.ts"],
"exclude": ["src/test.ts", "src/**/*.spec.ts"]
}

2
npm/ng-packs/apps/dev-app/tsconfig.app.json → npm/ng-packs/apps/dev-app/tsconfig.prod.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"outDir": "../../out-tsc/app",
"types": [],

1
npm/ng-packs/package.json

@ -9,6 +9,7 @@
"symlink": "symlink",
"start": "ng serve dev-app",
"start:internal": "ng serve dev-app --configuration=internal",
"build": "ng build",
"test": "ng test --watchAll --runInBand",
"commit": "git-cz",
"lint": "ng lint",

12
npm/ng-packs/packages/account-config/tsconfig.lib.json

@ -1,15 +1,12 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",
"declaration": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2018"
]
"lib": ["dom", "es2018"]
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
@ -19,8 +16,5 @@
"strictInjectionParameters": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
"exclude": ["src/test.ts", "**/*.spec.ts"]
}

2
npm/ng-packs/packages/account-config/tsconfig.spec.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"emitDecoratorMetadata": true,
"esModuleInterop": true,

2
npm/ng-packs/packages/account/tsconfig.lib.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",

2
npm/ng-packs/packages/account/tsconfig.spec.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"emitDecoratorMetadata": true,
"esModuleInterop": true,

42
npm/ng-packs/packages/core/src/lib/services/lazy-load.service.ts

@ -1,20 +1,56 @@
import { Injectable } from '@angular/core';
import { Observable, ReplaySubject, throwError } from 'rxjs';
import { concat, Observable, of, ReplaySubject, throwError } from 'rxjs';
import { delay, retryWhen, shareReplay, take, tap } from 'rxjs/operators';
import { LoadingStrategy } from '../strategies';
import { uuid } from '../utils';
@Injectable({
providedIn: 'root',
})
export class LazyLoadService {
readonly loaded = new Set();
loadedLibraries: { [url: string]: ReplaySubject<void> } = {};
load(strategy: LoadingStrategy, retryTimes?: number, retryDelay?: number): Observable<Event>;
load(
urlOrUrls: string | string[],
type: 'script' | 'style',
content: string = '',
content?: string,
targetQuery?: string,
position?: InsertPosition,
): Observable<void>;
load(
strategyOrUrl: LoadingStrategy | string | string[],
retryTimesOrType?: number | 'script' | 'style',
retryDelayOrContent?: number | string,
targetQuery: string = 'body',
position: InsertPosition = 'beforeend',
): Observable<void> {
): Observable<Event | void> {
if (strategyOrUrl instanceof LoadingStrategy) {
const strategy = strategyOrUrl;
const retryTimes = typeof retryTimesOrType === 'number' ? retryTimesOrType : 2;
const retryDelay = typeof retryDelayOrContent === 'number' ? retryDelayOrContent : 1000;
if (this.loaded.has(strategy.path)) return of(new CustomEvent('load'));
return strategy.createStream().pipe(
retryWhen(error$ =>
concat(
error$.pipe(delay(retryDelay), take(retryTimes)),
throwError(new CustomEvent('error')),
),
),
tap(() => this.loaded.add(strategy.path)),
delay(100),
shareReplay({ bufferSize: 1, refCount: true }),
);
}
let urlOrUrls = strategyOrUrl;
const content = (retryDelayOrContent as string) || '';
const type = retryTimesOrType as 'script' | 'style';
if (!urlOrUrls && !content) {
return throwError('Should pass url or content');
} else if (!urlOrUrls && content) {

32
npm/ng-packs/packages/core/src/lib/strategies/content-security.strategy.ts

@ -0,0 +1,32 @@
export abstract class ContentSecurityStrategy {
constructor(public nonce?: string) {}
abstract applyCSP(element: HTMLScriptElement | HTMLStyleElement): void;
}
export class LooseContentSecurityStrategy extends ContentSecurityStrategy {
constructor(nonce: string) {
super(nonce);
}
applyCSP(element: HTMLScriptElement | HTMLStyleElement) {
element.setAttribute('nonce', this.nonce);
}
}
export class StrictContentSecurityStrategy extends ContentSecurityStrategy {
constructor() {
super();
}
applyCSP(_: HTMLScriptElement | HTMLStyleElement) {}
}
export const CONTENT_SECURITY_STRATEGY = {
Loose(nonce: string) {
return new LooseContentSecurityStrategy(nonce);
},
Strict() {
return new StrictContentSecurityStrategy();
},
};

17
npm/ng-packs/packages/core/src/lib/strategies/cross-origin.strategy.ts

@ -0,0 +1,17 @@
export class CrossOriginStrategy {
constructor(public crossorigin: 'anonymous' | 'use-credentials', public integrity?: string) {}
setCrossOrigin<T extends HTMLElement>(element: T) {
if (this.integrity) element.setAttribute('integrity', this.integrity);
element.setAttribute('crossorigin', this.crossorigin);
}
}
export const CROSS_ORIGIN_STRATEGY = {
Anonymous(integrity?: string) {
return new CrossOriginStrategy('anonymous', integrity);
},
UseCredentials(integrity?: string) {
return new CrossOriginStrategy('use-credentials', integrity);
},
};

28
npm/ng-packs/packages/core/src/lib/strategies/dom.strategy.ts

@ -0,0 +1,28 @@
export class DomStrategy {
constructor(
public target: HTMLElement = document.head,
public position: InsertPosition = 'beforeend',
) {}
insertElement<T extends HTMLElement>(element: T) {
this.target.insertAdjacentElement(this.position, element);
}
}
export const DOM_STRATEGY = {
AfterElement(element: HTMLElement) {
return new DomStrategy(element, 'afterend');
},
AppendToBody() {
return new DomStrategy(document.body, 'beforeend');
},
AppendToHead() {
return new DomStrategy(document.head, 'beforeend');
},
BeforeElement(element: HTMLElement) {
return new DomStrategy(element, 'beforebegin');
},
PrependToHead() {
return new DomStrategy(document.head, 'afterbegin');
},
};

4
npm/ng-packs/packages/core/src/lib/strategies/index.ts

@ -0,0 +1,4 @@
export * from './content-security.strategy';
export * from './cross-origin.strategy';
export * from './dom.strategy';
export * from './loading.strategy';

88
npm/ng-packs/packages/core/src/lib/strategies/loading.strategy.ts

@ -0,0 +1,88 @@
import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { fromLazyLoad } from '../utils';
import { CrossOriginStrategy, CROSS_ORIGIN_STRATEGY } from './cross-origin.strategy';
import { DomStrategy, DOM_STRATEGY } from './dom.strategy';
export abstract class LoadingStrategy<T extends HTMLScriptElement | HTMLLinkElement = any> {
constructor(
public path: string,
protected domStrategy: DomStrategy = DOM_STRATEGY.AppendToHead(),
protected crossOriginStrategy: CrossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous(),
) {}
abstract createElement(): T;
createStream<E extends Event>(): Observable<E> {
return of(null).pipe(
switchMap(() =>
fromLazyLoad<E>(this.createElement(), this.domStrategy, this.crossOriginStrategy),
),
);
}
}
export class ScriptLoadingStrategy extends LoadingStrategy<HTMLScriptElement> {
constructor(src: string, domStrategy?: DomStrategy, crossOriginStrategy?: CrossOriginStrategy) {
super(src, domStrategy, crossOriginStrategy);
}
createElement(): HTMLScriptElement {
const element = document.createElement('script');
element.src = this.path;
return element;
}
}
export class StyleLoadingStrategy extends LoadingStrategy<HTMLLinkElement> {
constructor(href: string, domStrategy?: DomStrategy, crossOriginStrategy?: CrossOriginStrategy) {
super(href, domStrategy, crossOriginStrategy);
}
createElement(): HTMLLinkElement {
const element = document.createElement('link');
element.rel = 'stylesheet';
element.href = this.path;
return element;
}
}
export const LOADING_STRATEGY = {
AppendAnonymousScriptToBody(src: string, integrity?: string) {
return new ScriptLoadingStrategy(
src,
DOM_STRATEGY.AppendToBody(),
CROSS_ORIGIN_STRATEGY.Anonymous(integrity),
);
},
AppendAnonymousScriptToHead(src: string, integrity?: string) {
return new ScriptLoadingStrategy(
src,
DOM_STRATEGY.AppendToHead(),
CROSS_ORIGIN_STRATEGY.Anonymous(integrity),
);
},
AppendAnonymousStyleToHead(src: string, integrity?: string) {
return new StyleLoadingStrategy(
src,
DOM_STRATEGY.AppendToHead(),
CROSS_ORIGIN_STRATEGY.Anonymous(integrity),
);
},
PrependAnonymousScriptToHead(src: string, integrity?: string) {
return new ScriptLoadingStrategy(
src,
DOM_STRATEGY.PrependToHead(),
CROSS_ORIGIN_STRATEGY.Anonymous(integrity),
);
},
PrependAnonymousStyleToHead(src: string, integrity?: string) {
return new StyleLoadingStrategy(
src,
DOM_STRATEGY.PrependToHead(),
CROSS_ORIGIN_STRATEGY.Anonymous(integrity),
);
},
};

41
npm/ng-packs/packages/core/src/lib/tests/content-security.strategy.spec.ts

@ -0,0 +1,41 @@
import {
CONTENT_SECURITY_STRATEGY,
LooseContentSecurityStrategy,
StrictContentSecurityStrategy,
} from '../strategies';
import { uuid } from '../utils';
describe('LooseContentSecurityStrategy', () => {
describe('#applyCSP', () => {
it('should set nonce attribute', () => {
const nonce = uuid();
const strategy = new LooseContentSecurityStrategy(nonce);
const element = document.createElement('link');
strategy.applyCSP(element);
expect(element.getAttribute('nonce')).toBe(nonce);
});
});
});
describe('StrictContentSecurityStrategy', () => {
describe('#applyCSP', () => {
it('should not set nonce attribute', () => {
const strategy = new StrictContentSecurityStrategy();
const element = document.createElement('link');
strategy.applyCSP(element);
expect(element.getAttribute('nonce')).toBeNull();
});
});
});
describe('CONTENT_SECURITY_STRATEGY', () => {
test.each`
name | Strategy | nonce
${'Loose'} | ${LooseContentSecurityStrategy} | ${uuid()}
${'Strict'} | ${StrictContentSecurityStrategy} | ${undefined}
`('should successfully map $name to $Strategy.name', ({ name, Strategy, nonce }) => {
expect(CONTENT_SECURITY_STRATEGY[name](nonce)).toEqual(new Strategy(nonce));
});
});

38
npm/ng-packs/packages/core/src/lib/tests/cross-origin.strategy.spec.ts

@ -0,0 +1,38 @@
import { CrossOriginStrategy, CROSS_ORIGIN_STRATEGY } from '../strategies';
import { uuid } from '../utils';
describe('CrossOriginStrategy', () => {
describe('#setCrossOrigin', () => {
it('should set crossorigin attribute', () => {
const strategy = new CrossOriginStrategy('use-credentials');
const element = document.createElement('link');
strategy.setCrossOrigin(element);
expect(element.crossOrigin).toBe('use-credentials');
});
it('should set integrity attribute when given', () => {
const integrity = uuid();
const strategy = new CrossOriginStrategy('anonymous', integrity);
const element = document.createElement('link');
strategy.setCrossOrigin(element);
expect(element.crossOrigin).toBe('anonymous');
expect(element.getAttribute('integrity')).toBe(integrity);
});
});
});
describe('CROSS_ORIGIN_STRATEGY', () => {
test.each`
name | integrity | crossOrigin
${'Anonymous'} | ${undefined} | ${'anonymous'}
${'Anonymous'} | ${uuid()} | ${'anonymous'}
${'UseCredentials'} | ${undefined} | ${'use-credentials'}
${'UseCredentials'} | ${uuid()} | ${'use-credentials'}
`('should successfully map $name to CrossOriginStrategy', ({ name, integrity, crossOrigin }) => {
expect(CROSS_ORIGIN_STRATEGY[name](integrity)).toEqual(
new CrossOriginStrategy(crossOrigin, integrity),
);
});
});

49
npm/ng-packs/packages/core/src/lib/tests/dom.strategy.spec.ts

@ -0,0 +1,49 @@
import { DomStrategy, DOM_STRATEGY } from '../strategies';
describe('DomStrategy', () => {
describe('#insertElement', () => {
it('should append element to head by default', () => {
const strategy = new DomStrategy();
const element = document.createElement('script');
strategy.insertElement(element);
expect(document.head.lastChild).toBe(element);
});
it('should append element to body when body is given as target', () => {
const strategy = new DomStrategy(document.body);
const element = document.createElement('script');
strategy.insertElement(element);
expect(document.body.lastChild).toBe(element);
});
it('should prepend to head when position is given as "afterbegin"', () => {
const strategy = new DomStrategy(undefined, 'afterbegin');
const element = document.createElement('script');
strategy.insertElement(element);
expect(document.head.firstChild).toBe(element);
});
});
});
describe('DOM_STRATEGY', () => {
const div = document.createElement('DIV');
beforeEach(() => {
document.body.innerHTML = '';
document.body.appendChild(div);
});
test.each`
name | target | position
${'AfterElement'} | ${div} | ${'afterend'}
${'AppendToBody'} | ${document.body} | ${'beforeend'}
${'AppendToHead'} | ${document.head} | ${'beforeend'}
${'BeforeElement'} | ${div} | ${'beforebegin'}
${'PrependToHead'} | ${document.head} | ${'afterbegin'}
`('should successfully map $name to CrossOriginStrategy', ({ name, target, position }) => {
expect(DOM_STRATEGY[name](target)).toEqual(new DomStrategy(target, position));
});
});

113
npm/ng-packs/packages/core/src/lib/tests/lazy-load-utils.spec.ts

@ -0,0 +1,113 @@
import { DomStrategy, DOM_STRATEGY } from '../strategies';
import { CrossOriginStrategy, CROSS_ORIGIN_STRATEGY } from '../strategies/cross-origin.strategy';
import { uuid } from '../utils';
import { fromLazyLoad } from '../utils/lazy-load-utils';
describe('Lazy Load Utils', () => {
describe('#fromLazyLoad', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('should append to head by default', () => {
const element = document.createElement('link');
const spy = jest.spyOn(document.head, 'insertAdjacentElement');
fromLazyLoad(element);
expect(spy).toHaveBeenCalledWith('beforeend', element);
});
it('should allow setting a dom strategy', () => {
const element = document.createElement('link');
const spy = jest.spyOn(document.head, 'insertAdjacentElement');
fromLazyLoad(element, DOM_STRATEGY.PrependToHead());
expect(spy).toHaveBeenCalledWith('afterbegin', element);
});
it('should set crossorigin to "anonymous" by default', () => {
const element = document.createElement('link');
fromLazyLoad(element);
expect(element.crossOrigin).toBe('anonymous');
});
it('should not set integrity by default', () => {
const element = document.createElement('link');
fromLazyLoad(element);
expect(element.getAttribute('integrity')).toBeNull();
});
it('should allow setting a cross-origin strategy', () => {
const element = document.createElement('link');
const integrity = uuid();
fromLazyLoad(element, undefined, CROSS_ORIGIN_STRATEGY.UseCredentials(integrity));
expect(element.crossOrigin).toBe('use-credentials');
expect(element.getAttribute('integrity')).toBe(integrity);
});
it('should emit error event on fail and clear callbacks', done => {
const error = new CustomEvent('error');
const parentNode = { removeChild: jest.fn() };
const element = ({ parentNode } as any) as HTMLLinkElement;
fromLazyLoad(
element,
{
insertElement(el: HTMLLinkElement) {
expect(el).toBe(element);
setTimeout(() => {
el.onerror(error);
}, 0);
},
} as DomStrategy,
{
setCrossOrigin(_: HTMLLinkElement) {},
} as CrossOriginStrategy,
).subscribe({
error: value => {
expect(value).toBe(error);
expect(parentNode.removeChild).toHaveBeenCalledWith(element);
expect(element.onerror).toBeNull();
done();
},
});
});
it('should emit load event on success and clear callbacks', done => {
const success = new CustomEvent('load');
const parentNode = { removeChild: jest.fn() };
const element = ({ parentNode } as any) as HTMLLinkElement;
fromLazyLoad(
element,
{
insertElement(el: HTMLLinkElement) {
expect(el).toBe(element);
setTimeout(() => {
el.onload(success);
}, 0);
},
} as DomStrategy,
{
setCrossOrigin(_: HTMLLinkElement) {},
} as CrossOriginStrategy,
).subscribe({
next: value => {
expect(value).toBe(success);
expect(parentNode.removeChild).not.toHaveBeenCalled();
expect(element.onload).toBeNull();
done();
},
});
});
});
});

74
npm/ng-packs/packages/core/src/lib/tests/lazy-load.service.spec.ts

@ -1,9 +1,65 @@
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { of, throwError } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators';
import { LazyLoadService } from '../services/lazy-load.service';
import { catchError } from 'rxjs/operators';
import { of } from 'rxjs';
import { ScriptLoadingStrategy } from '../strategies';
describe('LazyLoadService', () => {
describe('#load', () => {
const service = new LazyLoadService();
const strategy = new ScriptLoadingStrategy('http://example.com/');
afterEach(() => {
jest.clearAllMocks();
});
it('should emit an error event if not loaded', done => {
const counter = jest.fn();
jest.spyOn(strategy, 'createStream').mockReturnValueOnce(
of(null).pipe(
switchMap(() => {
counter();
return throwError('THIS WILL NOT BE THE FINAL ERROR');
}),
),
);
service.load(strategy, 5, 0).subscribe({
error: errorEvent => {
expect(errorEvent).toEqual(new CustomEvent('error'));
expect(counter).toHaveBeenCalledTimes(6);
expect(service.loaded.has(strategy.path)).toBe(false);
done();
},
});
});
it('should emit a load event if loaded', done => {
const loadEvent = new CustomEvent('load');
jest.spyOn(strategy, 'createStream').mockReturnValue(of(loadEvent));
service.load(strategy).subscribe({
next: event => {
expect(event).toBe(loadEvent);
expect(service.loaded.has(strategy.path)).toBe(true);
done();
},
});
});
it('should emit a custom load event if loaded if resource is loaded before', done => {
const loadEvent = new CustomEvent('load');
service.loaded.add(strategy.path);
service.load(strategy).subscribe(event => {
expect(event).toEqual(loadEvent);
done();
});
});
});
});
describe('LazyLoadService (Deprecated)', () => {
let spectator: SpectatorService<LazyLoadService>;
let service: LazyLoadService;
const scriptElement = document.createElement('script');
@ -25,15 +81,17 @@ describe('LazyLoadService', () => {
spy.mockReturnValue(scriptElement);
service.load('https://abp.io', 'script', 'test').subscribe(res => {
expect(document.querySelector('script[src="https://abp.io"][type="text/javascript"]').textContent).toMatch(
'test',
);
expect(
document.querySelector('script[src="https://abp.io"][type="text/javascript"]').textContent,
).toMatch('test');
});
scriptElement.onload(null);
service.load('https://abp.io', 'script', 'test').subscribe(res => {
expect(document.querySelectorAll('script[src="https://abp.io"][type="text/javascript"]')).toHaveLength(1);
expect(
document.querySelectorAll('script[src="https://abp.io"][type="text/javascript"]'),
).toHaveLength(1);
done();
});
});
@ -59,7 +117,9 @@ describe('LazyLoadService', () => {
test('should load an link element', done => {
service.load('https://abp.io', 'style').subscribe(res => {
expect(document.querySelector('link[type="text/css"][rel="stylesheet"][href="https://abp.io"]')).toBeTruthy();
expect(
document.querySelector('link[type="text/css"][rel="stylesheet"][href="https://abp.io"]'),
).toBeTruthy();
done();
});

102
npm/ng-packs/packages/core/src/lib/tests/loading.strategy.spec.ts

@ -0,0 +1,102 @@
import {
CROSS_ORIGIN_STRATEGY,
DOM_STRATEGY,
LOADING_STRATEGY,
ScriptLoadingStrategy,
StyleLoadingStrategy,
} from '../strategies';
const path = 'http://example.com/';
describe('ScriptLoadingStrategy', () => {
describe('#createElement', () => {
it('should return a script element with src attribute', () => {
const strategy = new ScriptLoadingStrategy(path);
const element = strategy.createElement();
expect(element.tagName).toBe('SCRIPT');
expect(element.src).toBe(path);
});
});
describe('#createStream', () => {
it('should use given dom and cross-origin strategies', done => {
const domStrategy = DOM_STRATEGY.PrependToHead();
const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.UseCredentials();
domStrategy.insertElement = jest.fn((el: HTMLScriptElement) => {
setTimeout(() => {
el.onload(
new CustomEvent('success', {
detail: {
crossOrigin: el.crossOrigin,
},
}),
);
}, 0);
}) as any;
const strategy = new ScriptLoadingStrategy(path, domStrategy, crossOriginStrategy);
strategy.createStream<CustomEvent>().subscribe(event => {
expect(event.detail.crossOrigin).toBe('use-credentials');
done();
});
});
});
});
describe('StyleLoadingStrategy', () => {
describe('#createElement', () => {
it('should return a style element with href and rel attributes', () => {
const strategy = new StyleLoadingStrategy(path);
const element = strategy.createElement();
expect(element.tagName).toBe('LINK');
expect(element.href).toBe(path);
expect(element.rel).toBe('stylesheet');
});
});
describe('#createStream', () => {
it('should use given dom and cross-origin strategies', done => {
const domStrategy = DOM_STRATEGY.PrependToHead();
const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.UseCredentials();
domStrategy.insertElement = jest.fn((el: HTMLLinkElement) => {
setTimeout(() => {
el.onload(
new CustomEvent('success', {
detail: {
crossOrigin: el.crossOrigin,
},
}),
);
}, 0);
}) as any;
const strategy = new StyleLoadingStrategy(path, domStrategy, crossOriginStrategy);
strategy.createStream<CustomEvent>().subscribe(event => {
expect(event.detail.crossOrigin).toBe('use-credentials');
done();
});
});
});
});
describe('LOADING_STRATEGY', () => {
test.each`
name | Strategy | domStrategy
${'AppendAnonymousScriptToBody'} | ${ScriptLoadingStrategy} | ${'AppendToBody'}
${'AppendAnonymousScriptToHead'} | ${ScriptLoadingStrategy} | ${'AppendToHead'}
${'AppendAnonymousStyleToHead'} | ${StyleLoadingStrategy} | ${'AppendToHead'}
${'PrependAnonymousScriptToHead'} | ${ScriptLoadingStrategy} | ${'PrependToHead'}
${'PrependAnonymousStyleToHead'} | ${StyleLoadingStrategy} | ${'PrependToHead'}
`(
'should successfully map $name to $Strategy.name with $domStrategy dom strategy',
({ name, Strategy, domStrategy }) => {
expect(LOADING_STRATEGY[name](path)).toEqual(new Strategy(path, DOM_STRATEGY[domStrategy]()));
},
);
});

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

@ -1,5 +1,6 @@
export * from './common-utils';
export * from './generator-utils';
export * from './initial-utils';
export * from './lazy-load-utils';
export * from './route-utils';
export * from './rxjs-utils';

51
npm/ng-packs/packages/core/src/lib/utils/lazy-load-utils.ts

@ -0,0 +1,51 @@
import { Observable, Observer } from 'rxjs';
import { CrossOriginStrategy, CROSS_ORIGIN_STRATEGY } from '../strategies/cross-origin.strategy';
import { DomStrategy, DOM_STRATEGY } from '../strategies/dom.strategy';
export function fromLazyLoad<T extends Event>(
element: HTMLScriptElement | HTMLLinkElement,
domStrategy: DomStrategy = DOM_STRATEGY.AppendToHead(),
crossOriginStrategy: CrossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous(),
): Observable<T> {
crossOriginStrategy.setCrossOrigin(element);
domStrategy.insertElement(element);
return new Observable((observer: Observer<T>) => {
element.onload = (event: T) => {
clearCallbacks(element);
observer.next(event);
observer.complete();
};
const handleError = createErrorHandler(observer, element);
element.onerror = handleError;
element.onabort = handleError;
element.onemptied = handleError;
element.onstalled = handleError;
element.onsuspend = handleError;
return () => {
clearCallbacks(element);
observer.complete();
};
});
}
function createErrorHandler(observer: Observer<Event>, element: HTMLElement) {
/* tslint:disable-next-line:only-arrow-functions */
return function(event: Event | string) {
clearCallbacks(element);
element.parentNode.removeChild(element);
observer.error(event);
};
}
function clearCallbacks(element: HTMLElement) {
element.onload = null;
element.onerror = null;
element.onabort = null;
element.onemptied = null;
element.onstalled = null;
element.onsuspend = null;
}

4
npm/ng-packs/packages/core/src/public-api.ts

@ -7,6 +7,7 @@ export * from './lib/abstracts';
export * from './lib/actions';
export * from './lib/components';
export * from './lib/constants';
export * from './lib/core.module';
export * from './lib/directives';
export * from './lib/enums';
export * from './lib/guards';
@ -16,7 +17,6 @@ export * from './lib/pipes';
export * from './lib/plugins';
export * from './lib/services';
export * from './lib/states';
export * from './lib/strategies';
export * from './lib/tokens';
export * from './lib/utils';
export * from './lib/core.module';

2
npm/ng-packs/packages/core/tsconfig.lib.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",

2
npm/ng-packs/packages/core/tsconfig.spec.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"emitDecoratorMetadata": true,
"esModuleInterop": true,

12
npm/ng-packs/packages/feature-management/tsconfig.lib.json

@ -1,15 +1,12 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",
"declaration": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2018"
]
"lib": ["dom", "es2018"]
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
@ -19,8 +16,5 @@
"strictInjectionParameters": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
"exclude": ["src/test.ts", "**/*.spec.ts"]
}

2
npm/ng-packs/packages/feature-management/tsconfig.spec.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"emitDecoratorMetadata": true,
"esModuleInterop": true,

12
npm/ng-packs/packages/identity-config/tsconfig.lib.json

@ -1,15 +1,12 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",
"declaration": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2018"
]
"lib": ["dom", "es2018"]
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
@ -19,8 +16,5 @@
"strictInjectionParameters": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
"exclude": ["src/test.ts", "**/*.spec.ts"]
}

2
npm/ng-packs/packages/identity-config/tsconfig.spec.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"emitDecoratorMetadata": true,
"esModuleInterop": true,

6
npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.ts

@ -1,5 +1,5 @@
import { ABP } from '@abp/ng.core';
import { ConfirmationService, Toaster } from '@abp/ng.theme.shared';
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared';
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Select, Store } from '@ngxs/store';
@ -116,8 +116,8 @@ export class RolesComponent implements OnInit {
.warn('AbpIdentity::RoleDeletionConfirmationMessage', 'AbpIdentity::AreYouSure', {
messageLocalizationParams: [name],
})
.subscribe((status: Toaster.Status) => {
if (status === Toaster.Status.confirm) {
.subscribe((status: Confirmation.Status) => {
if (status === Confirmation.Status.confirm) {
this.store.dispatch(new DeleteRole(id)).subscribe(() => this.get());
}
});

2
npm/ng-packs/packages/identity/tsconfig.lib.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",

2
npm/ng-packs/packages/identity/tsconfig.spec.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"emitDecoratorMetadata": true,
"esModuleInterop": true,

2
npm/ng-packs/packages/permission-management/tsconfig.lib.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",

2
npm/ng-packs/packages/permission-management/tsconfig.spec.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"emitDecoratorMetadata": true,
"esModuleInterop": true,

12
npm/ng-packs/packages/setting-management-config/tsconfig.lib.json

@ -1,15 +1,12 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",
"declaration": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2018"
]
"lib": ["dom", "es2018"]
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
@ -19,8 +16,5 @@
"strictInjectionParameters": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
"exclude": ["src/test.ts", "**/*.spec.ts"]
}

2
npm/ng-packs/packages/setting-management-config/tsconfig.spec.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"emitDecoratorMetadata": true,
"esModuleInterop": true,

12
npm/ng-packs/packages/setting-management/tsconfig.lib.json

@ -1,15 +1,12 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",
"declaration": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2018"
]
"lib": ["dom", "es2018"]
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
@ -19,8 +16,5 @@
"strictInjectionParameters": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
"exclude": ["src/test.ts", "**/*.spec.ts"]
}

2
npm/ng-packs/packages/setting-management/tsconfig.spec.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"emitDecoratorMetadata": true,
"esModuleInterop": true,

12
npm/ng-packs/packages/tenant-management-config/tsconfig.lib.json

@ -1,15 +1,12 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",
"declaration": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2018"
]
"lib": ["dom", "es2018"]
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
@ -19,8 +16,5 @@
"strictInjectionParameters": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
"exclude": ["src/test.ts", "**/*.spec.ts"]
}

2
npm/ng-packs/packages/tenant-management-config/tsconfig.spec.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"emitDecoratorMetadata": true,
"esModuleInterop": true,

6
npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.ts

@ -1,5 +1,5 @@
import { ABP } from '@abp/ng.core';
import { ConfirmationService, Toaster } from '@abp/ng.theme.shared';
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared';
import { Component, OnInit, TemplateRef, ViewChild, ChangeDetectorRef } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Select, Store } from '@ngxs/store';
@ -252,8 +252,8 @@ export class TenantsComponent implements OnInit {
messageLocalizationParams: [name],
},
)
.subscribe((status: Toaster.Status) => {
if (status === Toaster.Status.confirm) {
.subscribe((status: Confirmation.Status) => {
if (status === Confirmation.Status.confirm) {
this.store.dispatch(new DeleteTenant(id)).subscribe(() => this.get());
}
});

12
npm/ng-packs/packages/tenant-management/tsconfig.lib.json

@ -1,15 +1,12 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",
"declaration": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2018"
],
"lib": ["dom", "es2018"],
"paths": {}
},
"angularCompilerOptions": {
@ -20,8 +17,5 @@
"strictInjectionParameters": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
"exclude": ["src/test.ts", "**/*.spec.ts"]
}

2
npm/ng-packs/packages/tenant-management/tsconfig.spec.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"emitDecoratorMetadata": true,
"esModuleInterop": true,

2
npm/ng-packs/packages/theme-basic/tsconfig.lib.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",

2
npm/ng-packs/packages/theme-basic/tsconfig.spec.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"emitDecoratorMetadata": true,
"esModuleInterop": true,

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

@ -14,7 +14,7 @@ import {
import { fromEvent, Subject } from 'rxjs';
import { debounceTime, filter, takeUntil } from 'rxjs/operators';
import { fadeAnimation } from '../../animations/modal.animations';
import { Toaster } from '../../models/toaster';
import { Confirmation } from '../../models/confirmation';
import { ConfirmationService } from '../../services/confirmation.service';
import { ButtonComponent } from '../button/button.component';
@ -133,9 +133,9 @@ export class ModalComponent implements OnDestroy {
'AbpAccount::AreYouSureYouWantToCancelEditingWarningMessage',
'AbpAccount::AreYouSure',
)
.subscribe((status: Toaster.Status) => {
.subscribe((status: Confirmation.Status) => {
this.isConfirmationOpen = false;
if (status === Toaster.Status.confirm) {
if (status === Confirmation.Status.confirm) {
this.visible = false;
}
});

4
npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts

@ -17,7 +17,7 @@ import { Observable, Subject } from 'rxjs';
import snq from 'snq';
import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component';
import { HttpErrorConfig, ErrorScreenErrorCodes } from '../models/common';
import { Toaster } from '../models/toaster';
import { Confirmation } from '../models/confirmation';
import { ConfirmationService } from '../services/confirmation.service';
export const DEFAULT_ERROR_MESSAGES = {
@ -185,7 +185,7 @@ export class ErrorHandler {
message?: Config.LocalizationParam,
title?: Config.LocalizationParam,
body?: any,
): Observable<Toaster.Status> {
): Observable<Confirmation.Status> {
if (body) {
if (body.details) {
message = body.details;

2
npm/ng-packs/packages/theme-shared/tsconfig.lib.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"target": "es2015",

2
npm/ng-packs/packages/theme-shared/tsconfig.spec.json

@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.prod.json",
"compilerOptions": {
"emitDecoratorMetadata": true,
"esModuleInterop": true,

30
npm/ng-packs/tsconfig.json

@ -13,7 +13,35 @@
"target": "es2015",
"typeRoots": ["node_modules/@types"],
"lib": ["es2018", "dom"],
"types": ["jest"]
"types": ["jest"],
"paths": {
"@abp/ng.core": ["packages/core/src/public-api.ts"],
"@abp/ng.core/*": ["packages/core/src/lib/*"],
"@abp/ng.theme.shared": ["packages/theme-shared/src/public-api.ts"],
"@abp/ng.theme.shared/*": ["packages/theme-shared/src/lib/*"],
"@abp/ng.theme.basic": ["packages/theme-basic/src/public-api.ts"],
"@abp/ng.theme.basic/*": ["packages/theme-basic/src/lib/*"],
"@abp/ng.account": ["packages/account/src/public-api.ts"],
"@abp/ng.account/*": ["packages/account/src/lib/*"],
"@abp/ng.account.config": ["packages/account-config/src/public-api.ts"],
"@abp/ng.account.config/*": ["packages/account-config/src/lib/*"],
"@abp/ng.identity": ["packages/identity/src/public-api.ts"],
"@abp/ng.identity/*": ["packages/identity/src/lib/*"],
"@abp/ng.identity.config": ["packages/identity-config/src/public-api.ts"],
"@abp/ng.identity.config/*": ["packages/identity-config/src/lib/*"],
"@abp/ng.tenant-management": ["packages/tenant-management/src/public-api.ts"],
"@abp/ng.tenant-management/*": ["packages/tenant-management/src/lib/*"],
"@abp/ng.tenant-management.config": ["packages/tenant-management-config/src/public-api.ts"],
"@abp/ng.tenant-management.config/*": ["packages/tenant-management-config/src/lib/*"],
"@abp/ng.setting-management": ["packages/setting-management/src/public-api.ts"],
"@abp/ng.setting-management/*": ["packages/setting-management/src/lib/*"],
"@abp/ng.setting-management.config": ["packages/setting-management-config/src/public-api.ts"],
"@abp/ng.setting-management.config/*": ["packages/setting-management-config/src/lib/*"],
"@abp/ng.permission-management": ["packages/permission-management/src/public-api.ts"],
"@abp/ng.permission-management/*": ["packages/permission-management/src/lib/*"],
"@abp/ng.feature-management": ["packages/feature-management/src/public-api.ts"],
"@abp/ng.feature-management/*": ["packages/feature-management/src/lib/*"]
}
},
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,

22
npm/ng-packs/tsconfig.prod.json

@ -0,0 +1,22 @@
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "esnext",
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"typeRoots": ["node_modules/@types"],
"lib": ["es2018", "dom"],
"types": ["jest"]
},
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true
}
}

2
npm/packs/tui-editor/abp.resourcemapping.js

@ -1,7 +1,7 @@
module.exports = {
mappings: {
"@node_modules/to-mark/dist/to-mark.min.js": "@libs/to-mark/",
"@node_modules/tui-code-snippet/dist/tui-code-snippet.min.js": "@libs/tui-code-snippet/",
"@node_modules/tui-code-snippet/dist/*.*": "@libs/tui-code-snippet/",
"@node_modules/squire-rte/build/squire.js": "@libs/squire-rte/",
"@node_modules/tui-editor/dist/*.*": "@libs/tui-editor/"
}

16
samples/BookStore-Angular-MongoDb/aspnet-core/src/Acme.BookStore.Domain/Users/AppUser.cs

@ -41,9 +41,19 @@ namespace Acme.BookStore.Users
#endregion
/* Add your own properties here. Example:
*
* public virtual string MyProperty { get; set; }
*/
*
* public string MyProperty { get; set; }
*
* If you add a property and using the EF Core, remember these;
*
* 1. Update BookStoreDbContext.OnModelCreating
* to configure the mapping for your new property
* 2. Update BookStoreEfCoreEntityExtensionMappings to extend the IdentityUser entity
* and add your new property to the migration.
* 3. Use the Add-Migration to add a new database migration.
* 4. Run the .DbMigrator project (or use the Update-Database command) to apply
* schema change to the database.
*/
private AppUser()
{

16
samples/BookStore-Modular/application/src/Acme.BookStore.Domain/Users/AppUser.cs

@ -41,9 +41,19 @@ namespace Acme.BookStore.Users
#endregion
/* Add your own properties here. Example:
*
* public virtual string MyProperty { get; set; }
*/
*
* public string MyProperty { get; set; }
*
* If you add a property and using the EF Core, remember these;
*
* 1. Update BookStoreDbContext.OnModelCreating
* to configure the mapping for your new property
* 2. Update BookStoreEfCoreEntityExtensionMappings to extend the IdentityUser entity
* and add your new property to the migration.
* 3. Use the Add-Migration to add a new database migration.
* 4. Run the .DbMigrator project (or use the Update-Database command) to apply
* schema change to the database.
*/
private AppUser()
{

7
samples/BookStore-Modular/application/src/Acme.BookStore.EntityFrameworkCore.DbMigrations/EntityFrameworkCore/BookStoreMigrationsDbContext.cs

@ -42,13 +42,6 @@ namespace Acme.BookStore.EntityFrameworkCore
builder.ConfigureTenantManagement();
builder.ConfigureBookManagement();
/* Configure customizations for entities from the modules included */
builder.Entity<IdentityUser>(b =>
{
b.ConfigureCustomUserProperties();
});
/* Configure your own tables/entities inside the ConfigureBookStore method */
builder.ConfigureBookStore();

2
samples/BookStore-Modular/application/src/Acme.BookStore.EntityFrameworkCore.DbMigrations/EntityFrameworkCore/BookStoreMigrationsDbContextFactory.cs

@ -11,6 +11,8 @@ namespace Acme.BookStore.EntityFrameworkCore
{
public BookStoreMigrationsDbContext CreateDbContext(string[] args)
{
BookStoreEfCoreEntityExtensionMappings.Configure();
var configuration = BuildConfiguration();
var builder = new DbContextOptionsBuilder<BookStoreMigrationsDbContext>()

8
samples/BookStore-Modular/application/src/Acme.BookStore.EntityFrameworkCore/EntityFrameworkCore/BookStoreDbContext.cs

@ -3,6 +3,7 @@ using Acme.BookStore.Users;
using Volo.Abp.Data;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.Modeling;
using Volo.Abp.Identity;
using Volo.Abp.Users.EntityFrameworkCore;
namespace Acme.BookStore.EntityFrameworkCore
@ -39,12 +40,13 @@ namespace Acme.BookStore.EntityFrameworkCore
builder.Entity<AppUser>(b =>
{
b.ToTable("AbpUsers"); //Sharing the same table "AbpUsers" with the IdentityUser
b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "Users"); //Sharing the same table "AbpUsers" with the IdentityUser
b.ConfigureByConvention();
b.ConfigureAbpUser();
//Moved customization to a method so we can share it with the BookStoreMigrationsDbContext class
b.ConfigureCustomUserProperties();
/* Configure mappings for your additional properties
* Also see the BookStoreEfCoreEntityExtensionMappings class
*/
});
/* Configure your own tables/entities inside the ConfigureBookStore method */

8
samples/BookStore-Modular/application/src/Acme.BookStore.EntityFrameworkCore/EntityFrameworkCore/BookStoreDbContextModelCreatingExtensions.cs

@ -1,7 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Volo.Abp;
using Volo.Abp.Users;
namespace Acme.BookStore.EntityFrameworkCore
{
@ -20,11 +18,5 @@ namespace Acme.BookStore.EntityFrameworkCore
// //...
//});
}
public static void ConfigureCustomUserProperties<TUser>(this EntityTypeBuilder<TUser> b)
where TUser: class, IUser
{
//b.Property<string>(nameof(AppUser.MyProperty))...
}
}
}

37
samples/BookStore-Modular/application/src/Acme.BookStore.EntityFrameworkCore/EntityFrameworkCore/BookStoreEfCoreEntityExtensionMappings.cs

@ -0,0 +1,37 @@
using Volo.Abp.Identity;
using Volo.Abp.ObjectExtending;
using Volo.Abp.Threading;
namespace Acme.BookStore.EntityFrameworkCore
{
public static class BookStoreEfCoreEntityExtensionMappings
{
private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner();
public static void Configure()
{
OneTimeRunner.Run(() =>
{
/* You can configure entity extension properties for the
* entities defined in the used modules.
*
* The properties defined here becomes table fields.
* If you want to use the ExtraProperties dictionary of the entity
* instead of creating a new field, then define the property in the
* MyProjectNameDomainObjectExtensions class.
*
* Example:
*
* ObjectExtensionManager.Instance
* .MapEfCoreProperty<IdentityUser, string>(
* "MyProperty",
* b => b.HasMaxLength(128)
* );
*
* See the documentation for more:
* https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Extending-Entities
*/
});
}
}
}

5
samples/BookStore-Modular/application/src/Acme.BookStore.EntityFrameworkCore/EntityFrameworkCore/BookStoreEntityFrameworkCoreModule.cs

@ -29,6 +29,11 @@ namespace Acme.BookStore.EntityFrameworkCore
)]
public class BookStoreEntityFrameworkCoreModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
BookStoreEfCoreEntityExtensionMappings.Configure();
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAbpDbContext<BookStoreDbContext>(options =>

36
samples/BookStore-Modular/modules/book-management/src/Acme.BookStore.BookManagement.EntityFrameworkCore/EntityFrameworkCore/BookManagementEfCoreEntityExtensionMappings.cs

@ -0,0 +1,36 @@
using Volo.Abp.ObjectExtending;
using Volo.Abp.Threading;
namespace Acme.BookStore.BookManagement.EntityFrameworkCore
{
public static class BookManagementEfCoreEntityExtensionMappings
{
private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner();
public static void Configure()
{
OneTimeRunner.Run(() =>
{
/* You can configure entity extension properties for the
* entities defined in the used modules.
*
* The properties defined here becomes table fields.
* If you want to use the ExtraProperties dictionary of the entity
* instead of creating a new field, then define the property in the
* MyProjectNameDomainObjectExtensions class.
*
* Example:
*
* ObjectExtensionManager.Instance
* .MapEfCoreProperty<IdentityUser, string>(
* "MyProperty",
* b => b.HasMaxLength(128)
* );
*
* See the documentation for more:
* https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Extending-Entities
*/
});
}
}
}

5
samples/BookStore-Modular/modules/book-management/src/Acme.BookStore.BookManagement.EntityFrameworkCore/EntityFrameworkCore/BookManagementEntityFrameworkCoreModule.cs

@ -10,6 +10,11 @@ namespace Acme.BookStore.BookManagement.EntityFrameworkCore
)]
public class BookManagementEntityFrameworkCoreModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
BookManagementEfCoreEntityExtensionMappings.Configure();
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAbpDbContext<BookManagementDbContext>(options =>

16
samples/BookStore/src/Acme.BookStore.Domain/Users/AppUser.cs

@ -41,9 +41,19 @@ namespace Acme.BookStore.Users
#endregion
/* Add your own properties here. Example:
*
* public virtual string MyProperty { get; set; }
*/
*
* public string MyProperty { get; set; }
*
* If you add a property and using the EF Core, remember these;
*
* 1. Update BookStoreDbContext.OnModelCreating
* to configure the mapping for your new property
* 2. Update BookStoreEfCoreEntityExtensionMappings to extend the IdentityUser entity
* and add your new property to the migration.
* 3. Use the Add-Migration to add a new database migration.
* 4. Run the .DbMigrator project (or use the Update-Database command) to apply
* schema change to the database.
*/
private AppUser()
{

7
samples/BookStore/src/Acme.BookStore.EntityFrameworkCore.DbMigrations/EntityFrameworkCore/BookStoreMigrationsDbContext.cs

@ -40,13 +40,6 @@ namespace Acme.BookStore.EntityFrameworkCore
builder.ConfigureFeatureManagement();
builder.ConfigureTenantManagement();
/* Configure customizations for entities from the modules included */
builder.Entity<IdentityUser>(b =>
{
b.ConfigureCustomUserProperties();
});
/* Configure your own tables/entities inside the ConfigureBookStore method */
builder.ConfigureBookStore();

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

Loading…
Cancel
Save