Browse Source

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

pull/2282/head
Arkat Erol 7 years ago
parent
commit
3a88eab5bd
  1. 4
      .github/labeler.yml
  2. 17
      .github/workflows/labeler.yml
  3. 1
      .gitignore
  4. 6
      abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/pt-BR.json
  5. 8
      docs/en/AspNetCore/Bundling-Minification.md
  6. 4
      docs/en/Best-Practices/Module-Architecture.md
  7. 4
      docs/en/Multi-Tenancy.md
  8. 42
      docs/en/Tutorials/Angular/Part-II.md
  9. 16
      docs/zh-Hans/Best-Practices/Module-Architecture.md
  10. 6
      docs/zh-Hans/Index.md
  11. 4
      docs/zh-Hans/Multi-Tenancy.md
  12. 28
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Toolbar/LanguageSwitch/Default.cshtml
  13. 9
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Toolbar/UserMenu/Default.cshtml
  14. 14
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml
  15. 3
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs
  16. 49
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinder.cs
  17. 47
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinderProvider.cs
  18. 24
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/UpdateCommand.cs
  19. 10
      framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposeServicesAttribute.cs
  20. 3
      framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposedServiceExplorer.cs
  21. 55
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs
  22. 17
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpDateTimeValueConverter.cs
  23. 2
      framework/src/Volo.Abp.MailKit/Volo.Abp.MailKit.csproj
  24. 61
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController.cs
  25. 100
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController_Tests.cs
  26. 9
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs
  27. 11
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs
  28. 9
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpDateTimeValueConverter_Tests.cs
  29. 24
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/PersonRepository.cs
  30. 20
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs
  31. 10
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/City.cs
  32. 30
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/District.cs
  33. 13
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/IPersonRepository.cs
  34. 6
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs
  35. 17
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/PersonView.cs
  36. 7
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs
  37. 73
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/AbpDateTimeValueConverter_Tests.cs
  38. 17
      modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs
  39. 7
      modules/account/src/Volo.Abp.Account.Web/Areas/Account/Controllers/LogoutController.cs
  40. 12
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs
  41. 4
      modules/account/src/Volo.Abp.Account.Web/Settings/AccountSettingDefinitionProvider.cs
  42. 2
      modules/account/src/Volo.Abp.Account.Web/Settings/AccountSettingNames.cs
  43. 2
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUser.cs
  44. 8
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/tr.json
  45. 3
      modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.js
  46. 3
      npm/ng-packs/.prettierrc
  47. 26
      npm/ng-packs/packages/account/src/lib/components/register/register.component.ts
  48. 1
      npm/ng-packs/packages/core/src/lib/core.module.ts
  49. 67
      npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts
  50. 15
      npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts
  51. 20
      npm/ng-packs/packages/core/src/lib/pipes/sort.pipe.ts
  52. 34
      npm/ng-packs/packages/core/src/lib/services/rest.service.ts
  53. 221
      npm/ng-packs/packages/core/src/lib/states/config.state.ts
  54. 138
      npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts
  55. 39
      npm/ng-packs/packages/core/src/lib/tests/permission.directive.spec.ts
  56. 18
      npm/ng-packs/packages/core/src/lib/tests/permission.guard.spec.ts
  57. 24
      npm/ng-packs/packages/core/src/lib/tests/sort.pipe.spec.ts
  58. 13
      npm/ng-packs/packages/core/src/lib/utils/date-extensions.ts
  59. 8
      npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.html
  60. 8
      npm/ng-packs/packages/identity/src/lib/components/users/users.component.html
  61. 10
      npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.html
  62. 8
      npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.ts
  63. 14
      npm/ng-packs/packages/theme-basic/src/lib/components/application-layout/application-layout.component.html
  64. 33
      npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.ts
  65. 2
      npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.html
  66. 1
      npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.scss
  67. 21
      npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.ts
  68. 58
      npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.html
  69. 4
      npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts
  70. 49
      npm/ng-packs/packages/theme-shared/src/lib/components/sort-order-icon/sort-order-icon.component.ts
  71. 0
      npm/ng-packs/packages/theme-shared/src/lib/constants/scripts.ts
  72. 16
      npm/ng-packs/packages/theme-shared/src/lib/constants/styles.ts
  73. 8
      npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts
  74. 15
      npm/ng-packs/packages/theme-shared/src/lib/tests/breadcrumb.component.spec.ts
  75. 2
      npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts
  76. 8
      npm/ng-packs/packages/theme-shared/src/lib/tests/error.component.spec.ts
  77. 14
      npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts
  78. 23
      npm/ng-packs/packages/theme-shared/src/lib/tests/modal.component.spec.ts
  79. 46
      npm/ng-packs/packages/theme-shared/src/lib/tests/sort-order-icon.component.spec.ts
  80. 12
      npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts
  81. 23
      npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts
  82. 2
      samples/BasicConsoleApplication/AbpConsoleDemo/AbpConsoleDemo.csproj
  83. 6
      samples/BookStore-Modular/modules/book-management/test/Acme.BookStore.BookManagement.TestBase/Acme.BookStore.BookManagement.TestBase.csproj
  84. 2
      samples/MicroserviceDemo/_run/01_AuthServer.ps1
  85. 2
      samples/MicroserviceDemo/_run/02_IdentityService.ps1
  86. 2
      samples/MicroserviceDemo/_run/03_BloggingService.ps1
  87. 2
      samples/MicroserviceDemo/_run/04_ProductService.ps1
  88. 2
      samples/MicroserviceDemo/_run/05_InternalGateway.ps1
  89. 2
      samples/MicroserviceDemo/_run/06_BackendAdminAppGateway.ps1
  90. 2
      samples/MicroserviceDemo/_run/07_PublicWebSiteGateway.ps1
  91. 2
      samples/MicroserviceDemo/_run/08_BackendAdminApp.ps1
  92. 2
      samples/MicroserviceDemo/_run/09_PublicWebSite.ps1
  93. 2
      samples/MicroserviceDemo/_run/10_ConsoleClientDemo.ps1
  94. 19
      samples/MicroserviceDemo/_run/README.md
  95. 9
      samples/MicroserviceDemo/_run/__Run_All_Application.ps1
  96. 1
      samples/MicroserviceDemo/_run/__Run_Browser.ps1
  97. 2
      samples/MicroserviceDemo/applications/AuthServer.Host/Properties/launchSettings.json
  98. 2
      samples/MicroserviceDemo/applications/BackendAdminApp.Host/Properties/launchSettings.json
  99. 2
      samples/MicroserviceDemo/applications/PublicWebSite.Host/Properties/launchSettings.json
  100. 29
      samples/MicroserviceDemo/gateways/BackendAdminAppGateway.Host/BackendAdminAppGatewayHostModule.cs

4
.github/labeler.yml

@ -0,0 +1,4 @@
ui-angular:
- npm/ng-packs/**/*
- templates/app/angular/**/*
- templates/module/angular/**/*

17
.github/workflows/labeler.yml

@ -0,0 +1,17 @@
name: "Pull Request Labeler"
on:
pull_request:
paths:
- npm/ng-packs/**/*
- templates/app/angular/**/*
- templates/module/angular/**/*
branches:
- master
- dev
jobs:
labeler:
runs-on: ubuntu-18.04
steps:
- uses: actions/labeler@v2
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

1
.gitignore

@ -289,4 +289,5 @@ samples/MicroserviceDemo/gateways/BackendAdminAppGateway.Host/Logs/logs.txt
samples/MicroserviceDemo/applications/PublicWebSite.Host/Logs/logs.txt samples/MicroserviceDemo/applications/PublicWebSite.Host/Logs/logs.txt
samples/MicroserviceDemo/gateways/PublicWebSiteGateway.Host/Logs/logs.txt samples/MicroserviceDemo/gateways/PublicWebSiteGateway.Host/Logs/logs.txt
samples/MicroserviceDemo/microservices/BloggingService.Host/Logs/logs.txt samples/MicroserviceDemo/microservices/BloggingService.Host/Logs/logs.txt
samples/MicroserviceDemo/applications/ConsoleClientDemo/Logs/logs.txt
modules/docs/app/Volo.DocsTestApp/Logs/logs.txt modules/docs/app/Volo.DocsTestApp/Logs/logs.txt

6
abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/pt-BR.json

@ -18,10 +18,10 @@
"AbpShortDescription": "ABP é uma arquitetura completa e uma forte infraestrutura para criar aplicativos da web modernos! Segue as melhores práticas e convenções para fornecer uma sólida experiência de desenvolvimento.", "AbpShortDescription": "ABP é uma arquitetura completa e uma forte infraestrutura para criar aplicativos da web modernos! Segue as melhores práticas e convenções para fornecer uma sólida experiência de desenvolvimento.",
"SourceCodeUpper": "CÓDIGO FONTE", "SourceCodeUpper": "CÓDIGO FONTE",
"LatestReleaseLogs": "Últimos registros de lançamento", "LatestReleaseLogs": "Últimos registros de lançamento",
"Infrastructure": "Infra-estrutura", "Infrastructure": "Infraestrutura",
"Architecture": "Arquitetura", "Architecture": "Arquitetura",
"Modular": "Modular", "Modular": "Modular",
"DontRepeatYourself": "Não se repita", "DontRepeatYourself": "Não repita a si mesmo",
"DeveloperFocused": "Desenvolvedor focado", "DeveloperFocused": "Desenvolvedor focado",
"FullStackApplicationInfrastructure": "Infraestrutura de aplicativos full stack.", "FullStackApplicationInfrastructure": "Infraestrutura de aplicativos full stack.",
"DomainDrivenDesign": "Design Orientado a Domínio", "DomainDrivenDesign": "Design Orientado a Domínio",
@ -30,7 +30,7 @@
"AuthorizationExplanation": "Autorização avançada com usuário, função e sistema de permissão refinado. Criada na biblioteca Microsoft Identity.", "AuthorizationExplanation": "Autorização avançada com usuário, função e sistema de permissão refinado. Criada na biblioteca Microsoft Identity.",
"MultiTenancy": "Múltiplos inquilinos", "MultiTenancy": "Múltiplos inquilinos",
"MultiTenancyExplanation": "Aplicativos SaaS facilitados! Multilocação integrada do banco de dados à interface do usuário.", "MultiTenancyExplanation": "Aplicativos SaaS facilitados! Multilocação integrada do banco de dados à interface do usuário.",
"CrossCuttingConcerns": "Preocupações transversais", "CrossCuttingConcerns": "Características transversais",
"CrossCuttingConcernsExplanation": "Infraestrutura completa para autorização, validação, tratamento de exceções, armazenamento em cache, log de auditoria, gerenciamento de transações e assim por diante.", "CrossCuttingConcernsExplanation": "Infraestrutura completa para autorização, validação, tratamento de exceções, armazenamento em cache, log de auditoria, gerenciamento de transações e assim por diante.",
"BuiltInBundlingMinification": "Pacote & Minificação Integrados", "BuiltInBundlingMinification": "Pacote & Minificação Integrados",
"BuiltInBundlingMinificationExplanation": "Pare de usar ferramentas externas para empacotamento e minificação. O ABP oferece uma maneira mais simples, dinâmica, poderosa, modular e integrada!", "BuiltInBundlingMinificationExplanation": "Pare de usar ferramentas externas para empacotamento e minificação. O ABP oferece uma maneira mais simples, dinâmica, poderosa, modular e integrada!",

8
docs/en/AspNetCore/Bundling-Minification.md

@ -114,7 +114,7 @@ public class MyWebModule : AbpModule
{ {
public override void ConfigureServices(ServiceConfigurationContext context) public override void ConfigureServices(ServiceConfigurationContext context)
{ {
Configure<BundlingOptions>(options => Configure<AbpBundlingOptions>(options =>
{ {
options options
.ScriptBundles .ScriptBundles
@ -151,7 +151,7 @@ public class MyWebExtensionModule : AbpModule
{ {
public override void ConfigureServices(ServiceConfigurationContext context) public override void ConfigureServices(ServiceConfigurationContext context)
{ {
Configure<BundlingOptions>(options => Configure<AbpBundlingOptions>(options =>
{ {
options options
.ScriptBundles .ScriptBundles
@ -189,7 +189,7 @@ public class MyExtensionGlobalStyleContributor : BundleContributor
Then you can use this contributor as like below: Then you can use this contributor as like below:
````C# ````C#
services.Configure<BundlingOptions>(options => services.Configure<AbpBundlingOptions>(options =>
{ {
options options
.ScriptBundles .ScriptBundles
@ -317,7 +317,7 @@ In some specific cases, it may be needed to create a **new** bundle **inherited*
Example: Example:
````c# ````c#
services.Configure<BundlingOptions>(options => services.Configure<AbpBundlingOptions>(options =>
{ {
options options
.StyleBundles .StyleBundles

4
docs/en/Best-Practices/Module-Architecture.md

@ -57,7 +57,7 @@ Next section describes the packages in more details.
* **Do** divide the domain layer into two projects: * **Do** divide the domain layer into two projects:
* **Domain.Shared** package, named as *CompanyName.ModuleName.Domain.Shared*, that contains constants, enums and other types those can be safely shared with the all layers of the module. This package can also be shared to 3rd-party clients. It can not contain entities, repositories, domain services or any other business objects. * **Domain.Shared** package, named as *CompanyName.ModuleName.Domain.Shared*, that contains constants, enums and other types those can be safely shared with the all layers of the module. This package can also be shared to 3rd-party clients. It can not contain entities, repositories, domain services or any other business objects.
* **Domain** package, named as *CompanyName.ModuleName.Domain*, that contains entities, repository interfaces, domain service interfaces and their implementations and other domain objects. * **Domain** package, named as *CompanyName.ModuleName.Domain*, that contains entities, repository interfaces, domain service interfaces and their implementations and other domain objects.
* Domain package depends on the **Domain.Share** package. * Domain package depends on the **Domain.Shared** package.
#### Application Layer #### Application Layer
@ -85,5 +85,5 @@ Next section describes the packages in more details.
#### Web Layer #### Web Layer
* Do create a **Web** package, named as *CompanyName.ModuleName.Web*, that contains pages, views, scripts, styles, images and other UI components. * **Do** create a **Web** package, named as *CompanyName.ModuleName.Web*, that contains pages, views, scripts, styles, images and other UI components.
* Web package only depends on the **HttpApi** package. * Web package only depends on the **HttpApi** package.

4
docs/en/Multi-Tenancy.md

@ -312,10 +312,10 @@ http://nginx.org/en/docs/http/ngx_http_core_module.html#ignore_invalid_headers
http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers
"__tenant" parameter name can be changed using AspNetCoreMultiTenancyOptions. Example: "__tenant" parameter name can be changed using AbpAspNetCoreMultiTenancyOptions. Example:
````C# ````C#
services.Configure<AspNetCoreMultiTenancyOptions>(options => services.Configure<AbpAspNetCoreMultiTenancyOptions>(options =>
{ {
options.TenantKey = "MyTenantKey"; options.TenantKey = "MyTenantKey";
}); });

42
docs/en/Tutorials/Angular/Part-II.md

@ -218,26 +218,6 @@ Open `book-list.component.html` and add the form in the body template of the mod
> We've used [NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview) in this component. > We've used [NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview) in this component.
Open the `book-list.component.ts` and then create an array, named `bookTypeArr`:
```js
//...
booksType = Books.BookType;
bookTypeArr = Object.keys(Books.BookType).filter(
bookType => typeof this.booksType[bookType] === 'number'
);
```
> It use the `booksType` field,please put the `bookTypeArr` defined below that line
The `bookTypeArr` contains the fields of the `BookType` enum. Resulting array is shown below:
```js
['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...]
```
This array was used in the previous form template (in the `ngFor` loop).
#### Datepicker Requirements #### Datepicker Requirements
You need to import `NgbDatepickerModule` to the `books.module.ts`: You need to import `NgbDatepickerModule` to the `books.module.ts`:
@ -269,6 +249,28 @@ export class BookListComponent implements OnInit {
> The `NgbDateAdapter` converts Datepicker value to `Date` type. See the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details. > The `NgbDateAdapter` converts Datepicker value to `Date` type. See the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details.
#### Create the Book Type Array
Open the `book-list.component.ts` and then create an array, named `bookTypeArr`:
```js
//...
booksType = Books.BookType;
bookTypeArr = Object.keys(Books.BookType).filter(
bookType => typeof this.booksType[bookType] === 'number'
);
```
The `bookTypeArr` contains the fields of the `BookType` enum. Resulting array is shown below:
```js
['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...]
```
This array was used in the previous form template (in the `ngFor` loop).
![new-book-form](images/bookstore-new-book-form.png) ![new-book-form](images/bookstore-new-book-form.png)
#### Saving the Book #### Saving the Book

16
docs/zh-Hans/Best-Practices/Module-Architecture.md

@ -57,33 +57,33 @@
* **推荐** 将领域层划分为两个项目: * **推荐** 将领域层划分为两个项目:
* **Domain.Shared** 包(项目) 命名为*CompanyName.ModuleName.Domain.Shared*,包含常量,枚举和其他类型, 它不能包含实体,存储库,域服务或任何其他业务对象. 可以安全地与模块中的所有层使用. 此包也可以与第三方客户端使用. * **Domain.Shared** 包(项目) 命名为*CompanyName.ModuleName.Domain.Shared*,包含常量,枚举和其他类型, 它不能包含实体,存储库,域服务或任何其他业务对象. 可以安全地与模块中的所有层使用. 此包也可以与第三方客户端使用.
* **Domain** 包(项目) 命名为*CompanyName.ModuleName.Domain*, 包含实体, 仓储接口,领域服务接口及其实现和其他领域对象. * **Domain** 包(项目) 命名为*CompanyName.ModuleName.Domain*, 包含实体, 仓储接口,领域服务接口及其实现和其他领域对象.
* Domain package 依赖于 **Domain.Share** package. * Domain 包依赖于 **Domain.Shared** 包.
#### 应用服务层 #### 应用服务层
* **推荐** 将应用服务层划分为两个项目: * **推荐** 将应用服务层划分为两个项目:
* **Application.Contracts** 包(项目) 命名为*CompanyName.ModuleName.Application.Contracts,包含应用服务接口和相关的数据传输对象(DTO). * **Application.Contracts** 包(项目) 命名为*CompanyName.ModuleName.Application.Contracts,包含应用服务接口和相关的数据传输对象(DTO).
* Application contract package 依赖于 **Domain.Shared** package. * Application contract 包依赖于 **Domain.Shared** 包.
* **Application** 包(项目)命名为*CompanyName.ModuleName.Application*,包含应用服务实现. * **Application** 包(项目)命名为*CompanyName.ModuleName.Application*,包含应用服务实现.
* Application package 依赖于 **Domain** 和 **Application.Contracts** packages. * Application 包依赖于 **Domain** 包和 **Application.Contracts** 包.
#### 基础设施层 #### 基础设施层
* **推荐** 为每个orm/数据库集成创建一个独立的集成包, 比如Entity Framework Core 和 MongoDB. * **推荐** 为每个orm/数据库集成创建一个独立的集成包, 比如Entity Framework Core 和 MongoDB.
* **推荐** 例如, 创建一个抽象Entity Framework Core集成的*CompanyName.ModuleName.EntityFrameworkCore* package. ORM 集成 package 依赖于 **Domain** package. * **推荐** 例如, 创建一个抽象Entity Framework Core集成的*CompanyName.ModuleName.EntityFrameworkCore* 包. ORM 集成包依赖于 **Domain** 包.
* **不推荐** 依赖于orm/数据库集成包中的其他层. * **不推荐** 依赖于orm/数据库集成包中的其他层.
* **推荐** 为每个主要的库创建一个独立的集成包, 在不影响其他包的情况下可以被另一个库替换. * **推荐** 为每个主要的库创建一个独立的集成包, 在不影响其他包的情况下可以被另一个库替换.
#### HTTP 层 #### HTTP 层
* **推荐** 创建命名为*CompanyName.ModuleName.HttpApi*的**HTTP API**包, 为模块开发REST风格的HTTP API. * **推荐** 创建命名为*CompanyName.ModuleName.HttpApi*的**HTTP API**包, 为模块开发REST风格的HTTP API.
* HTTP API package 只依赖于 **Application.Contracts** package. 不要依赖 Application package. * HTTP API 包只依赖于 **Application.Contracts** 包. 不要依赖 Application 包.
* **推荐** 为每个应用服务创建一个Controller (通常通过实现其接口). 这些控制器使用应用服务接口来委托操作. 它根据需要配置路由, HTTP方法和其他与Web相关的东西. * **推荐** 为每个应用服务创建一个Controller (通常通过实现其接口). 这些控制器使用应用服务接口来委托操作. 它根据需要配置路由, HTTP方法和其他与Web相关的东西.
* **推荐** 创建一个为HTTP API包提供客户端服务的**HTTP API Client**包, 它的命名为*companyname.modulename.httpapi*. 这些客户端服务将应用服务接口实现远程端点的客户端. * **推荐** 创建一个为HTTP API包提供客户端服务的**HTTP API Client**包, 它的命名为*Companyname.ModuleName.HttpApi.Client*. 这些客户端服务将应用服务接口实现远程端点的客户端.
* HTTP API Client package 仅依赖于 **Application.Contracts** package. * HTTP API Client 包仅依赖于 **Application.Contracts** 包.
* **推荐** 使用ABP框架提供的动态代理HTTP C#客户端的功能. * **推荐** 使用ABP框架提供的动态代理HTTP C#客户端的功能.
#### Web 层 #### Web 层
* **推荐** 创建命名为*CompanyName.ModuleName.Web*的 **Web**包. 包含页面,视图,脚本,样式,图像和其他UI组件. * **推荐** 创建命名为*CompanyName.ModuleName.Web*的 **Web**包. 包含页面,视图,脚本,样式,图像和其他UI组件.
* Web package 仅依赖于 **HttpApi** package. * Web 包仅依赖于 **HttpApi** 包.

6
docs/zh-Hans/Index.md

@ -6,12 +6,6 @@ ABP是一个**开源应用程序框架**,专注于基于ASP.NET Core的Web应用
浏览左侧导航菜单以深入了解文档. 浏览左侧导航菜单以深入了解文档.
## 项目状态
ABP是开源[ASP.NET Boilerplate](https://aspnetboilerplate.com/)框架的**下一代框架**, 它目前处于早期预览阶段,尚未准备好在生产中使用. 文档仍在进行中,远未完成.
对于短期和生产级应用程序, 建议使用[ASP.NET Boilerplate](https://aspnetboilerplate.com/)框架,该框架具有丰富的功能集,成熟,积极维护和最新.
## 入门 ## 入门
使用ABP开发新项目的最简单方法是使用启动模板: 使用ABP开发新项目的最简单方法是使用启动模板:

4
docs/zh-Hans/Multi-Tenancy.md

@ -312,10 +312,10 @@ Volo.Abp.AspNetCore.MultiTenancy 添加了下面这些租户解析器,从当前W
http://nginx.org/en/docs/http/ngx_http_core_module.html#ignore_invalid_headers http://nginx.org/en/docs/http/ngx_http_core_module.html#ignore_invalid_headers
http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers
可以使用AspNetCoreMultiTenancyOptions修改默认的参数名"__tenant".例如: 可以使用AbpAspNetCoreMultiTenancyOptions修改默认的参数名"__tenant".例如:
````C# ````C#
services.Configure<AspNetCoreMultiTenancyOptions>(options => services.Configure<AbpAspNetCoreMultiTenancyOptions>(options =>
{ {
options.TenantKey = "MyTenantKey"; options.TenantKey = "MyTenantKey";
}); });

28
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Toolbar/LanguageSwitch/Default.cshtml

@ -1,14 +1,18 @@
@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Themes.Basic.Components.Toolbar.LanguageSwitch @using System.Linq
@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Themes.Basic.Components.Toolbar.LanguageSwitch
@model LanguageSwitchViewComponentModel @model LanguageSwitchViewComponentModel
<div class="dropdown"> @if (Model.OtherLanguages.Any())
<a class="nav-link dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> {
@Model.CurrentLanguage.DisplayName <div class="dropdown">
</a> <a class="nav-link dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@Model.CurrentLanguage.DisplayName
</a>
<div class="dropdown-menu dropdown-menu-right border-0 shadow-sm" aria-labelledby="dropdownMenuLink"> <div class="dropdown-menu dropdown-menu-right border-0 shadow-sm" aria-labelledby="dropdownMenuLink">
@foreach (var language in Model.OtherLanguages) @foreach (var language in Model.OtherLanguages)
{ {
<a class="dropdown-item" href="/Abp/Languages/Switch?culture=@(language.CultureName)&uiCulture=@(language.UiCultureName)&returnUrl=@Context.Request.Path">@language.DisplayName</a> <a class="dropdown-item" href="/Abp/Languages/Switch?culture=@(language.CultureName)&uiCulture=@(language.UiCultureName)&returnUrl=@Context.Request.Path">@language.DisplayName</a>
} }
</div> </div>
</div> </div>
}

9
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Components/Toolbar/UserMenu/Default.cshtml

@ -9,7 +9,14 @@
@model ApplicationMenu @model ApplicationMenu
<div class="dropdown"> <div class="dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@CurrentUser.UserName @if (CurrentUser.TenantId != null)
{
<small><i>@CurrentTenant.Name</i>\</small>@CurrentUser.UserName
}
else
{
@CurrentUser.UserName
}
</a> </a>
@if (Model.Items.Any()) @if (Model.Items.Any())

14
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml

@ -53,11 +53,14 @@
<div class="@containerClass"> <div class="@containerClass">
<abp-row> <abp-row>
<abp-column size-md="_5" class="mx-auto"> <abp-column size-md="_5" class="mx-auto">
@if (MultiTenancyOptions.Value.IsEnabled &&
(TenantResolveResultAccessor.Result?.AppliedResolvers?.Contains(CookieTenantResolveContributor.ContributorName) == true))
{
<div class="card shadow-sm rounded mb-3"> <div class="card shadow-sm rounded mb-3">
<div class="card-body px-5"> <div class="card-body px-5">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<span style="font-size: .8em;" class="text-uppercase text-muted">@MultiTenancyStringLocalizer["Tenant"]</span><br /> <span style="font-size: .8em;" class="text-uppercase text-muted">@MultiTenancyStringLocalizer["Tenant"]</span><br/>
<h6 class="m-0 d-inline-block"> <h6 class="m-0 d-inline-block">
@if (CurrentTenant.Id == null) @if (CurrentTenant.Id == null)
{ {
@ -72,15 +75,12 @@
</h6> </h6>
</div> </div>
<div class="col-auto"> <div class="col-auto">
@if (MultiTenancyOptions.Value.IsEnabled && <a id="AbpTenantSwitchLink" href="javascript:;" class="btn btn-sm mt-3 btn-outline-primary">@MultiTenancyStringLocalizer["Switch"]</a>
(TenantResolveResultAccessor.Result?.AppliedResolvers?.Contains(CookieTenantResolveContributor.ContributorName) == true))
{
<a id="AbpTenantSwitchLink" href="javascript:;" class="btn btn-sm mt-3 btn-outline-primary">@MultiTenancyStringLocalizer["Switch"]</a>
}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
}
@(await Component.InvokeAsync<PageAlertsViewComponent>()) @(await Component.InvokeAsync<PageAlertsViewComponent>())
@RenderBody() @RenderBody()
</abp-column> </abp-column>

3
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs

@ -4,6 +4,7 @@ using Volo.Abp.AspNetCore.Mvc.Auditing;
using Volo.Abp.AspNetCore.Mvc.Conventions; using Volo.Abp.AspNetCore.Mvc.Conventions;
using Volo.Abp.AspNetCore.Mvc.ExceptionHandling; using Volo.Abp.AspNetCore.Mvc.ExceptionHandling;
using Volo.Abp.AspNetCore.Mvc.Features; using Volo.Abp.AspNetCore.Mvc.Features;
using Volo.Abp.AspNetCore.Mvc.ModelBinding;
using Volo.Abp.AspNetCore.Mvc.Uow; using Volo.Abp.AspNetCore.Mvc.Uow;
using Volo.Abp.AspNetCore.Mvc.Validation; using Volo.Abp.AspNetCore.Mvc.Validation;
@ -35,7 +36,7 @@ namespace Volo.Abp.AspNetCore.Mvc
private static void AddModelBinders(MvcOptions options) private static void AddModelBinders(MvcOptions options)
{ {
//options.ModelBinderProviders.Add(new AbpDateTimeModelBinderProvider()); options.ModelBinderProviders.Insert(0, new AbpDateTimeModelBinderProvider());
} }
private static void AddMetadataProviders(MvcOptions options, IServiceCollection services) private static void AddMetadataProviders(MvcOptions options, IServiceCollection services)

49
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinder.cs

@ -0,0 +1,49 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Volo.Abp.Timing;
namespace Volo.Abp.AspNetCore.Mvc.ModelBinding
{
public class AbpDateTimeModelBinder : IModelBinder
{
private readonly Type _type;
private readonly SimpleTypeModelBinder _simpleTypeModelBinder;
private readonly IClock _clock;
public AbpDateTimeModelBinder(ModelBinderProviderContext context)
{
_type = context.Metadata.ModelType;
_clock = context.Services.GetRequiredService<IClock>();
_simpleTypeModelBinder = new SimpleTypeModelBinder(context.Metadata.ModelType,
context.Services.GetRequiredService<ILoggerFactory>());
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
await _simpleTypeModelBinder.BindModelAsync(bindingContext);
if (!bindingContext.Result.IsModelSet)
{
return;
}
if (_type == typeof(DateTime))
{
var dateTime = (DateTime) bindingContext.Result.Model;
bindingContext.Result = ModelBindingResult.Success(_clock.Normalize(dateTime));
}
else
{
var dateTime = (DateTime?) bindingContext.Result.Model;
if (dateTime != null)
{
bindingContext.Result = ModelBindingResult.Success(_clock.Normalize(dateTime.Value));
}
}
}
}
}

47
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinderProvider.cs

@ -0,0 +1,47 @@
using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Volo.Abp.Timing;
namespace Volo.Abp.AspNetCore.Mvc.ModelBinding
{
public class AbpDateTimeModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType != typeof(DateTime) &&
context.Metadata.ModelType != typeof(DateTime?))
{
return null;
}
if (context.Metadata.ContainerType == null)
{
if (context.Metadata is DefaultModelMetadata defaultModelMetadata &&
defaultModelMetadata.Attributes.Attributes.All(x => x.GetType() != typeof(DisableDateTimeNormalizationAttribute)))
{
return new AbpDateTimeModelBinder(context);
}
}
else
{
var dateNormalizationDisabledForClass =
context.Metadata.ContainerType.IsDefined(typeof(DisableDateTimeNormalizationAttribute), true);
var dateNormalizationDisabledForProperty = context.Metadata.ContainerType
.GetProperty(context.Metadata.PropertyName)
?.IsDefined(typeof(DisableDateTimeNormalizationAttribute), true);
if (!dateNormalizationDisabledForClass &&
dateNormalizationDisabledForProperty != null &&
!dateNormalizationDisabledForProperty.Value)
{
return new AbpDateTimeModelBinder(context);
}
}
return null;
}
}
}

24
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/UpdateCommand.cs

@ -44,31 +44,37 @@ namespace Volo.Abp.Cli.Commands
var updateNpm = commandLineArgs.Options.ContainsKey(Options.Packages.Npm); var updateNpm = commandLineArgs.Options.ContainsKey(Options.Packages.Npm);
var updateNuget = commandLineArgs.Options.ContainsKey(Options.Packages.NuGet); var updateNuget = commandLineArgs.Options.ContainsKey(Options.Packages.NuGet);
var directory = commandLineArgs.Options.GetOrNull(Options.SolutionPath.Short, Options.SolutionPath.Long);
if (directory == null)
{
directory = Directory.GetCurrentDirectory();
}
var both = (updateNuget && updateNpm) || (!updateNuget && !updateNpm); var both = (updateNuget && updateNpm) || (!updateNuget && !updateNpm);
if (updateNuget || both) if (updateNuget || both)
{ {
await UpdateNugetPackages(commandLineArgs); await UpdateNugetPackages(commandLineArgs, directory);
} }
if (updateNpm || both) if (updateNpm || both)
{ {
UpdateNpmPackages(); UpdateNpmPackages(directory);
} }
} }
private void UpdateNpmPackages() private void UpdateNpmPackages(string directory)
{ {
_npmPackagesUpdater.Update(Directory.GetCurrentDirectory()); _npmPackagesUpdater.Update(directory);
} }
private async Task UpdateNugetPackages(CommandLineArgs commandLineArgs) private async Task UpdateNugetPackages(CommandLineArgs commandLineArgs, string directory)
{ {
var includePreviews = var includePreviews =
commandLineArgs.Options.GetOrNull(Options.IncludePreviews.Short, Options.IncludePreviews.Long) != null; commandLineArgs.Options.GetOrNull(Options.IncludePreviews.Short, Options.IncludePreviews.Long) != null;
var solution = Directory.GetFiles(Directory.GetCurrentDirectory(), "*.sln").FirstOrDefault(); var solution = Directory.GetFiles(directory, "*.sln").FirstOrDefault();
if (solution != null) if (solution != null)
{ {
@ -131,6 +137,12 @@ namespace Volo.Abp.Cli.Commands
public static class Options public static class Options
{ {
public static class SolutionPath
{
public const string Short = "sp";
public const string Long = "solution-path";
}
public static class IncludePreviews public static class IncludePreviews
{ {
public const string Short = "p"; public const string Short = "p";

10
framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposeServicesAttribute.cs

@ -9,9 +9,9 @@ namespace Volo.Abp.DependencyInjection
{ {
public Type[] ServiceTypes { get; } public Type[] ServiceTypes { get; }
public bool? IncludeDefaults { get; set; } public bool IncludeDefaults { get; set; }
public bool? IncludeSelf { get; set; } public bool IncludeSelf { get; set; }
public ExposeServicesAttribute(params Type[] serviceTypes) public ExposeServicesAttribute(params Type[] serviceTypes)
{ {
@ -22,19 +22,19 @@ namespace Volo.Abp.DependencyInjection
{ {
var serviceList = ServiceTypes.ToList(); var serviceList = ServiceTypes.ToList();
if (IncludeDefaults == true) if (IncludeDefaults)
{ {
foreach (var type in GetDefaultServices(targetType)) foreach (var type in GetDefaultServices(targetType))
{ {
serviceList.AddIfNotContains(type); serviceList.AddIfNotContains(type);
} }
if (IncludeSelf != false) if (IncludeSelf)
{ {
serviceList.AddIfNotContains(targetType); serviceList.AddIfNotContains(targetType);
} }
} }
else if (IncludeSelf == true) else if (IncludeSelf)
{ {
serviceList.AddIfNotContains(targetType); serviceList.AddIfNotContains(targetType);
} }

3
framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposedServiceExplorer.cs

@ -10,7 +10,8 @@ namespace Volo.Abp.DependencyInjection
private static readonly ExposeServicesAttribute DefaultExposeServicesAttribute = private static readonly ExposeServicesAttribute DefaultExposeServicesAttribute =
new ExposeServicesAttribute new ExposeServicesAttribute
{ {
IncludeDefaults = true IncludeDefaults = true,
IncludeSelf = true
}; };
public static List<Type> GetExposedServices(Type type) public static List<Type> GetExposedServices(Type type)

55
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Linq; using System.Linq;
@ -18,10 +18,12 @@ using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Entities.Events; using Volo.Abp.Domain.Entities.Events;
using Volo.Abp.EntityFrameworkCore.EntityHistory; using Volo.Abp.EntityFrameworkCore.EntityHistory;
using Volo.Abp.EntityFrameworkCore.ValueConverters;
using Volo.Abp.Guids; using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
using Volo.Abp.Reflection; using Volo.Abp.Reflection;
using Volo.Abp.Threading; using Volo.Abp.Threading;
using Volo.Abp.Timing;
namespace Volo.Abp.EntityFrameworkCore namespace Volo.Abp.EntityFrameworkCore
{ {
@ -48,6 +50,8 @@ namespace Volo.Abp.EntityFrameworkCore
public IAuditingManager AuditingManager { get; set; } public IAuditingManager AuditingManager { get; set; }
public IClock Clock { get; set; }
public ILogger<AbpDbContext<TDbContext>> Logger { get; set; } public ILogger<AbpDbContext<TDbContext>> Logger { get; set; }
private static readonly MethodInfo ConfigureBasePropertiesMethodInfo private static readonly MethodInfo ConfigureBasePropertiesMethodInfo
@ -57,6 +61,13 @@ namespace Volo.Abp.EntityFrameworkCore
BindingFlags.Instance | BindingFlags.NonPublic BindingFlags.Instance | BindingFlags.NonPublic
); );
private static readonly MethodInfo ConfigureValueConverterMethodInfo
= typeof(AbpDbContext<TDbContext>)
.GetMethod(
nameof(ConfigureValueConverter),
BindingFlags.Instance | BindingFlags.NonPublic
);
protected AbpDbContext(DbContextOptions<TDbContext> options) protected AbpDbContext(DbContextOptions<TDbContext> options)
: base(options) : base(options)
{ {
@ -75,6 +86,10 @@ namespace Volo.Abp.EntityFrameworkCore
ConfigureBasePropertiesMethodInfo ConfigureBasePropertiesMethodInfo
.MakeGenericMethod(entityType.ClrType) .MakeGenericMethod(entityType.ClrType)
.Invoke(this, new object[] { modelBuilder, entityType }); .Invoke(this, new object[] { modelBuilder, entityType });
ConfigureValueConverterMethodInfo
.MakeGenericMethod(entityType.ClrType)
.Invoke(this, new object[] { modelBuilder, entityType });
} }
} }
@ -332,6 +347,11 @@ namespace Volo.Abp.EntityFrameworkCore
protected virtual void ConfigureBaseProperties<TEntity>(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType) protected virtual void ConfigureBaseProperties<TEntity>(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType)
where TEntity : class where TEntity : class
{ {
if (mutableEntityType.IsOwned())
{
return;
}
ConfigureConcurrencyStampProperty<TEntity>(modelBuilder, mutableEntityType); ConfigureConcurrencyStampProperty<TEntity>(modelBuilder, mutableEntityType);
ConfigureExtraProperties<TEntity>(modelBuilder, mutableEntityType); ConfigureExtraProperties<TEntity>(modelBuilder, mutableEntityType);
ConfigureAuditProperties<TEntity>(modelBuilder, mutableEntityType); ConfigureAuditProperties<TEntity>(modelBuilder, mutableEntityType);
@ -488,6 +508,39 @@ namespace Volo.Abp.EntityFrameworkCore
} }
} }
protected virtual void ConfigureValueConverter<TEntity>(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType)
where TEntity : class
{
if (mutableEntityType.BaseType == null &&
!typeof(TEntity).IsDefined(typeof(DisableDateTimeNormalizationAttribute), true) &&
!typeof(TEntity).IsDefined(typeof(OwnedAttribute), true) &&
!mutableEntityType.IsOwned())
{
if (Clock == null || !Clock.SupportsMultipleTimezone)
{
return;
}
var dateTimeValueConverter = new AbpDateTimeValueConverter(Clock);
var dateTimePropertyInfos = typeof(TEntity).GetProperties()
.Where(property =>
(property.PropertyType == typeof(DateTime) ||
property.PropertyType == typeof(DateTime?)) &&
property.CanWrite &&
!property.IsDefined(typeof(DisableDateTimeNormalizationAttribute), true)
).ToList();
dateTimePropertyInfos.ForEach(property =>
{
modelBuilder
.Entity<TEntity>()
.Property(property.Name)
.HasConversion(dateTimeValueConverter);
});
}
}
protected virtual bool ShouldFilterEntity<TEntity>(IMutableEntityType entityType) where TEntity : class protected virtual bool ShouldFilterEntity<TEntity>(IMutableEntityType entityType) where TEntity : class
{ {
if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity))) if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)))

17
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpDateTimeValueConverter.cs

@ -0,0 +1,17 @@
using System;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Volo.Abp.Timing;
namespace Volo.Abp.EntityFrameworkCore.ValueConverters
{
public class AbpDateTimeValueConverter : ValueConverter<DateTime?, DateTime?>
{
public AbpDateTimeValueConverter(IClock clock, [CanBeNull] ConverterMappingHints mappingHints = null)
: base(
x => x.HasValue ? clock.Normalize(x.Value) : x,
x => x.HasValue ? clock.Normalize(x.Value) : x, mappingHints)
{
}
}
}

2
framework/src/Volo.Abp.MailKit/Volo.Abp.MailKit.csproj

@ -14,7 +14,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MailKit" Version="2.3.2" /> <PackageReference Include="MailKit" Version="2.4.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

61
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController.cs

@ -0,0 +1,61 @@
using System;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Timing;
namespace Volo.Abp.AspNetCore.Mvc.ModelBinding
{
[Route("api/model-Binding-test")]
public class ModelBindingController : AbpController
{
[HttpGet("DateTimeKind")]
public string DateTimeKind(DateTime input)
{
return input.Kind.ToString().ToLower();
}
[HttpGet("NullableDateTimeKind")]
public string NullableDateTimeKind(DateTime? input)
{
return input.Value.Kind.ToString().ToLower();
}
[HttpGet("DisableDateTimeNormalizationDateTimeKind")]
public string DisableDateTimeNormalizationDateTimeKind([DisableDateTimeNormalization]DateTime input)
{
return input.Kind.ToString().ToLower();
}
[HttpGet("DisableDateTimeNormalizationNullableDateTimeKind")]
public string DisableDateTimeNormalizationNullableDateTimeKind([DisableDateTimeNormalization]DateTime? input)
{
return input.Value.Kind.ToString().ToLower();
}
[HttpGet("ComplexTypeDateTimeKind")]
public string ComplexTypeDateTimeKind(GetDateTimeKindModel input)
{
return input.Time1.Kind.ToString().ToLower() + "_" +
input.Time2.Kind.ToString().ToLower() + "_" +
input.Time3.Value.Kind.ToString().ToLower() + "_" +
input.InnerModel.Time4.Kind.ToString().ToLower();
}
}
public class GetDateTimeKindModel
{
[DisableDateTimeNormalization]
public DateTime Time1 { get; set; }
public DateTime Time2 { get; set; }
public DateTime? Time3 { get; set; }
public GetDateTimeKindInnerModel InnerModel { get; set; }
[DisableDateTimeNormalization]
public class GetDateTimeKindInnerModel
{
public DateTime Time4 { get; set; }
}
}
}

100
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController_Tests.cs

@ -0,0 +1,100 @@
using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Shouldly;
using Volo.Abp.Timing;
using Xunit;
namespace Volo.Abp.AspNetCore.Mvc.ModelBinding
{
public abstract class ModelBindingController_Tests : AspNetCoreMvcTestBase
{
protected DateTimeKind DateTimeKind { get; set; }
protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services)
{
services.Configure<AbpClockOptions>(x => x.Kind = DateTimeKind);
}
[Fact]
public async Task DateTimeKind_Test()
{
var response = await Client.GetAsync("/api/model-Binding-test/DateTimeKind?input=2010-01-01T00:00:00Z");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync();
resultAsString.ShouldBe(DateTimeKind.ToString().ToLower());
}
[Fact]
public async Task NullableDateTimeKind_Test()
{
var response =
await Client.GetAsync("/api/model-Binding-test/NullableDateTimeKind?input=2010-01-01T00:00:00Z");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync();
resultAsString.ShouldBe(DateTimeKind.ToString().ToLower());
}
[Fact]
public async Task DisableDateTimeNormalizationDateTimeKind_Test()
{
var response =
await Client.GetAsync(
"/api/model-Binding-test/DisableDateTimeNormalizationDateTimeKind?input=2010-01-01T00:00:00Z");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync();
//Time parameter(2010-01-01T00:00:00Z) with time zone information, so the default Kind is Local.
resultAsString.ShouldBe(DateTimeKind.Local.ToString().ToLower());
}
[Fact]
public async Task DisableDateTimeNormalizationNullableDateTimeKind_Test()
{
var response =
await Client.GetAsync(
"/api/model-Binding-test/DisableDateTimeNormalizationNullableDateTimeKind?input=2010-01-01T00:00:00Z");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync();
//Time parameter(2010-01-01T00:00:00Z) with time zone information, so the default Kind is Local.
resultAsString.ShouldBe(DateTimeKind.Local.ToString().ToLower());
}
[Fact]
public async Task ComplexTypeDateTimeKind_Test()
{
var response = await Client.GetAsync("/api/model-Binding-test/ComplexTypeDateTimeKind?" +
"Time1=2010-01-01T00:00:00Z&" +
"Time2=2010-01-01T00:00:00Z&" +
"Time3=2010-01-01T00:00:00Z&" +
"InnerModel.Time4=2010-01-01T00:00:00Z");
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var resultAsString = await response.Content.ReadAsStringAsync();
//Time parameter(2010-01-01T00:00:00Z) with time zone information, so the default Kind is Local.
resultAsString.ShouldBe(
$"local_{DateTimeKind.ToString().ToLower()}_{DateTimeKind.ToString().ToLower()}_local");
}
}
public class ModelBindingController_Utc_Tests : ModelBindingController_Tests
{
public ModelBindingController_Utc_Tests()
{
DateTimeKind = DateTimeKind.Utc;
}
}
public class ModelBindingController_Local_Tests : ModelBindingController_Tests
{
public ModelBindingController_Local_Tests()
{
DateTimeKind = DateTimeKind.Local;
}
}
}

9
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs

@ -1,4 +1,5 @@
using Microsoft.Data.Sqlite; using System;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
@ -10,6 +11,7 @@ using Volo.Abp.Modularity;
using Volo.Abp.TestApp; using Volo.Abp.TestApp;
using Volo.Abp.TestApp.Domain; using Volo.Abp.TestApp.Domain;
using Volo.Abp.TestApp.EntityFrameworkCore; using Volo.Abp.TestApp.EntityFrameworkCore;
using Volo.Abp.Timing;
namespace Volo.Abp.EntityFrameworkCore namespace Volo.Abp.EntityFrameworkCore
{ {
@ -41,6 +43,8 @@ namespace Volo.Abp.EntityFrameworkCore
abpDbContextConfigurationContext.DbContextOptions.UseSqlite(sqliteConnection); abpDbContextConfigurationContext.DbContextOptions.UseSqlite(sqliteConnection);
}); });
}); });
Configure<AbpClockOptions>(options => options.Kind = DateTimeKind.Utc);
} }
public override void OnPreApplicationInitialization(ApplicationInitializationContext context) public override void OnPreApplicationInitialization(ApplicationInitializationContext context)
@ -56,6 +60,9 @@ namespace Volo.Abp.EntityFrameworkCore
using (var context = new TestMigrationsDbContext(new DbContextOptionsBuilder<TestMigrationsDbContext>().UseSqlite(connection).Options)) using (var context = new TestMigrationsDbContext(new DbContextOptionsBuilder<TestMigrationsDbContext>().UseSqlite(connection).Options))
{ {
context.GetService<IRelationalDatabaseCreator>().CreateTables(); context.GetService<IRelationalDatabaseCreator>().CreateTables();
context.Database.ExecuteSqlRaw(
@"CREATE VIEW View_PersonView AS
SELECT Name, CreationTime, Birthday, LastActive FROM People");
} }
return connection; return connection;

11
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs

@ -25,12 +25,23 @@ namespace Volo.Abp.EntityFrameworkCore
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.Owned<District>();
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Phone>(b => modelBuilder.Entity<Phone>(b =>
{ {
b.HasKey(p => new { p.PersonId, p.Number }); b.HasKey(p => new { p.PersonId, p.Number });
}); });
modelBuilder.Entity<City>(b =>
{
b.OwnsMany(c => c.Districts, d =>
{
d.WithOwner().HasForeignKey(x => x.CityId);
d.HasKey(x => new { x.CityId, x.Name });
});
});
} }
} }
} }

9
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpDateTimeValueConverter_Tests.cs

@ -0,0 +1,9 @@
using Volo.Abp.TestApp.Testing;
namespace Volo.Abp.EntityFrameworkCore.ValueConverters
{
public class AbpDateTimeValueConverter_Tests : AbpDateTimeValueConverter_Tests<AbpEntityFrameworkCoreTestModule>
{
}
}

24
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/PersonRepository.cs

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.TestApp.Domain;
namespace Volo.Abp.TestApp.EntityFrameworkCore
{
public class PersonRepository : EfCoreRepository<TestAppDbContext, Person, Guid>, IPersonRepository
{
public PersonRepository(IDbContextProvider<TestAppDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public async Task<PersonView> GetViewAsync(string name)
{
return await DbContext.PersonView.Where(x => x.Name == name).FirstOrDefaultAsync();
}
}
}

20
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs

@ -11,6 +11,8 @@ namespace Volo.Abp.TestApp.EntityFrameworkCore
public DbSet<City> Cities { get; set; } public DbSet<City> Cities { get; set; }
public DbSet<PersonView> PersonView { get; set; }
public DbSet<ThirdDbContextDummyEntity> DummyEntities { get; set; } public DbSet<ThirdDbContextDummyEntity> DummyEntities { get; set; }
public DbSet<EntityWithIntPk> EntityWithIntPks { get; set; } public DbSet<EntityWithIntPk> EntityWithIntPks { get; set; }
@ -23,12 +25,30 @@ namespace Volo.Abp.TestApp.EntityFrameworkCore
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.Owned<District>();
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Phone>(b => modelBuilder.Entity<Phone>(b =>
{ {
b.HasKey(p => new {p.PersonId, p.Number}); b.HasKey(p => new {p.PersonId, p.Number});
}); });
modelBuilder
.Entity<PersonView>(p =>
{
p.HasNoKey();
p.ToView("View_PersonView");
});
modelBuilder.Entity<City>(b =>
{
b.OwnsMany(c => c.Districts, d =>
{
d.WithOwner().HasForeignKey(x => x.CityId);
d.HasKey(x => new {x.CityId, x.Name});
});
});
} }
} }
} }

10
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/City.cs

@ -1,4 +1,6 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Entities;
namespace Volo.Abp.TestApp.Domain namespace Volo.Abp.TestApp.Domain
@ -7,6 +9,8 @@ namespace Volo.Abp.TestApp.Domain
{ {
public string Name { get; set; } public string Name { get; set; }
public ICollection<District> Districts { get; set; }
private City() private City()
{ {
@ -16,6 +20,12 @@ namespace Volo.Abp.TestApp.Domain
: base(id) : base(id)
{ {
Name = name; Name = name;
Districts = new List<District>();
}
public int GetPopulation()
{
return Districts.Select(d => d.Population).Sum();
} }
} }
} }

30
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/District.cs

@ -0,0 +1,30 @@
using System;
using Volo.Abp.Domain.Entities.Auditing;
namespace Volo.Abp.TestApp.Domain
{
public class District : CreationAuditedEntity
{
public Guid CityId { get; private set; }
public string Name { get; private set; }
public int Population { get; set; }
private District()
{
}
public District(Guid cityId, string name, int population = 0)
{
CityId = cityId;
Name = name;
}
public override object[] GetKeys()
{
return new Object[] {CityId, Name};
}
}
}

13
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/IPersonRepository.cs

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;
namespace Volo.Abp.TestApp.Domain
{
public interface IPersonRepository : IBasicRepository<Person, Guid>
{
Task<PersonView> GetViewAsync(string name);
}
}

6
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using Volo.Abp.Domain.Entities.Auditing; using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
using Volo.Abp.Timing;
namespace Volo.Abp.TestApp.Domain namespace Volo.Abp.TestApp.Domain
{ {
@ -15,6 +16,11 @@ namespace Volo.Abp.TestApp.Domain
public virtual int Age { get; set; } public virtual int Age { get; set; }
public virtual DateTime? Birthday { get; set; }
[DisableDateTimeNormalization]
public virtual DateTime? LastActive { get; set; }
public virtual Collection<Phone> Phones { get; set; } public virtual Collection<Phone> Phones { get; set; }
private Person() private Person()

17
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/PersonView.cs

@ -0,0 +1,17 @@
using System;
using Volo.Abp.Timing;
namespace Volo.Abp.TestApp.Domain
{
public class PersonView
{
public string Name { get; set; }
public DateTime CreationTime { get; set; }
public DateTime? Birthday { get; set; }
[DisableDateTimeNormalization]
public DateTime? LastActive { get; set; }
}
}

7
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs

@ -38,10 +38,15 @@ namespace Volo.Abp.TestApp
private void AddCities() private void AddCities()
{ {
var istanbul = new City(IstanbulCityId, "Istanbul");
istanbul.Districts.Add(new District(istanbul.Id, "Bakirkoy", 1283999));
istanbul.Districts.Add(new District(istanbul.Id, "Mecidiyeköy", 2222321));
istanbul.Districts.Add(new District(istanbul.Id, "Uskudar", 726172));
_cityRepository.Insert(new City(Guid.NewGuid(), "Tokyo")); _cityRepository.Insert(new City(Guid.NewGuid(), "Tokyo"));
_cityRepository.Insert(new City(Guid.NewGuid(), "Madrid")); _cityRepository.Insert(new City(Guid.NewGuid(), "Madrid"));
_cityRepository.Insert(new City(LondonCityId, "London") {ExtraProperties = { { "Population", 10_470_000 } } }); _cityRepository.Insert(new City(LondonCityId, "London") {ExtraProperties = { { "Population", 10_470_000 } } });
_cityRepository.Insert(new City(IstanbulCityId, "Istanbul")); _cityRepository.Insert(istanbul);
_cityRepository.Insert(new City(Guid.NewGuid(), "Paris")); _cityRepository.Insert(new City(Guid.NewGuid(), "Paris"));
_cityRepository.Insert(new City(Guid.NewGuid(), "Washington")); _cityRepository.Insert(new City(Guid.NewGuid(), "Washington"));
_cityRepository.Insert(new City(Guid.NewGuid(), "Sao Paulo")); _cityRepository.Insert(new City(Guid.NewGuid(), "Sao Paulo"));

73
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/AbpDateTimeValueConverter_Tests.cs

@ -0,0 +1,73 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Modularity;
using Volo.Abp.TestApp.Domain;
using Volo.Abp.Timing;
using Xunit;
namespace Volo.Abp.TestApp.Testing
{
public abstract class AbpDateTimeValueConverter_Tests<TStartupModule> : TestAppTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
private readonly IPersonRepository _personRepository;
protected AbpDateTimeValueConverter_Tests()
{
_personRepository = GetRequiredService<IPersonRepository>();
}
[Fact]
public async Task DateTime_Kind_Should_Be_Normalized_To_UTC_Test()
{
var personId = Guid.Parse("4125582e-d100-4c27-aa84-e4de85830dca");
await _personRepository.InsertAsync(new Person(personId, "bob lee", 18)
{
Birthday = DateTime.Parse("2020-01-01 00:00:00"),
LastActive = DateTime.Parse("2020-01-01 00:00:00"),
}, true);
var person = await _personRepository.GetAsync(personId);
person.ShouldNotBeNull();
person.CreationTime.Kind.ShouldBe(DateTimeKind.Utc);
person.Birthday.ShouldNotBeNull();
person.Birthday.Value.Kind.ShouldBe(DateTimeKind.Utc);
person.Birthday.Value.ToString("yyy-MM-dd HH:mm:ss").ShouldBe("2020-01-01 00:00:00");
//LastActive DisableDateTimeNormalization
person.LastActive.ShouldNotBeNull();
person.LastActive.Value.Kind.ShouldBe(DateTimeKind.Unspecified);
person.LastActive.Value.ToString("yyy-MM-dd HH:mm:ss").ShouldBe("2020-01-01 00:00:00");
}
[Fact]
public async Task DateTime_Kind_Should_Be_Normalized_To_UTC_View_Test()
{
var personName = "bob lee";
await _personRepository.InsertAsync(new Person(Guid.NewGuid(), personName, 18)
{
Birthday = DateTime.Parse("2020-01-01 00:00:00"),
LastActive = DateTime.Parse("2020-01-01 00:00:00"),
}, true);
var person = await _personRepository.GetViewAsync(personName);
person.ShouldNotBeNull();
person.CreationTime.Kind.ShouldBe(DateTimeKind.Utc);
person.Birthday.ShouldNotBeNull();
person.Birthday.Value.Kind.ShouldBe(DateTimeKind.Utc);
person.Birthday.Value.ToString("yyy-MM-dd HH:mm:ss").ShouldBe("2020-01-01 00:00:00");
//LastActive DisableDateTimeNormalization
person.LastActive.ShouldNotBeNull();
person.LastActive.Value.Kind.ShouldBe(DateTimeKind.Unspecified);
person.LastActive.Value.ToString("yyy-MM-dd HH:mm:ss").ShouldBe("2020-01-01 00:00:00");
}
}
}

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

@ -1,4 +1,4 @@
using IdentityModel; using IdentityModel;
using IdentityServer4.Events; using IdentityServer4.Events;
using IdentityServer4.Models; using IdentityServer4.Models;
using IdentityServer4.Services; using IdentityServer4.Services;
@ -12,8 +12,10 @@ using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Security.Principal; using System.Security.Principal;
using System.Threading.Tasks; using System.Threading.Tasks;
using Volo.Abp.Account.Web.Settings;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
using Volo.Abp.Settings;
using Volo.Abp.Uow; using Volo.Abp.Uow;
namespace Volo.Abp.Account.Web.Pages.Account namespace Volo.Abp.Account.Web.Pages.Account
@ -40,7 +42,7 @@ namespace Volo.Abp.Account.Web.Pages.Account
IdentityServerEvents = identityServerEvents; IdentityServerEvents = identityServerEvents;
} }
public override async Task OnGetAsync() public override async Task<IActionResult> OnGetAsync()
{ {
LoginInput = new LoginInputModel(); LoginInput = new LoginInputModel();
@ -63,7 +65,7 @@ namespace Volo.Abp.Account.Web.Pages.Account
{ {
LoginInput.UserNameOrEmailAddress = context.LoginHint; LoginInput.UserNameOrEmailAddress = context.LoginHint;
ExternalProviders = new[] { new ExternalProviderModel { AuthenticationScheme = context.IdP } }; ExternalProviders = new[] { new ExternalProviderModel { AuthenticationScheme = context.IdP } };
return; return Page();
} }
var schemes = await _schemeProvider.GetAllSchemesAsync(); var schemes = await _schemeProvider.GetAllSchemesAsync();
@ -77,7 +79,7 @@ namespace Volo.Abp.Account.Web.Pages.Account
}) })
.ToList(); .ToList();
EnableLocalLogin = true; //TODO: We can get default from a setting? EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin);
if (context?.ClientId != null) if (context?.ClientId != null)
{ {
var client = await ClientStore.FindEnabledClientByIdAsync(context.ClientId); var client = await ClientStore.FindEnabledClientByIdAsync(context.ClientId);
@ -96,15 +98,16 @@ namespace Volo.Abp.Account.Web.Pages.Account
if (IsExternalLoginOnly) if (IsExternalLoginOnly)
{ {
//return await ExternalLogin(vm.ExternalLoginScheme, returnUrl); return await base.OnPostExternalLogin(providers.First().AuthenticationScheme);
throw new NotImplementedException();
} }
return Page();
} }
[UnitOfWork] //TODO: Will be removed when we implement action filter [UnitOfWork] //TODO: Will be removed when we implement action filter
public override async Task<IActionResult> OnPostAsync(string action) public override async Task<IActionResult> OnPostAsync(string action)
{ {
EnableLocalLogin = true; //TODO: We can get default from a setting? EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin);
if (action == "Cancel") if (action == "Cancel")
{ {

7
modules/account/src/Volo.Abp.Account.Web/Areas/Account/Controllers/LogoutController.cs

@ -17,10 +17,15 @@ namespace Volo.Abp.Account.Web.Areas.Account.Controllers
} }
//todo@alper: this method can be moved to AccountController like "account/logout" //todo@alper: this method can be moved to AccountController like "account/logout"
public async Task<IActionResult> Index() public async Task<IActionResult> Index(string returnUrl = null)
{ {
await _signInManager.SignOutAsync(); await _signInManager.SignOutAsync();
if (returnUrl != null)
{
return LocalRedirect(returnUrl);
}
return RedirectToPage("/Account/Login"); return RedirectToPage("/Account/Login");
} }
} }

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

@ -10,8 +10,10 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Volo.Abp.Account.Web.Settings;
using Volo.Abp.Identity; using Volo.Abp.Identity;
using Volo.Abp.Security.Claims; using Volo.Abp.Security.Claims;
using Volo.Abp.Settings;
using Volo.Abp.Uow; using Volo.Abp.Uow;
using Volo.Abp.Validation; using Volo.Abp.Validation;
using IdentityUser = Volo.Abp.Identity.IdentityUser; using IdentityUser = Volo.Abp.Identity.IdentityUser;
@ -56,7 +58,7 @@ namespace Volo.Abp.Account.Web.Pages.Account
_accountOptions = accountOptions.Value; _accountOptions = accountOptions.Value;
} }
public virtual async Task OnGetAsync() public virtual async Task<IActionResult> OnGetAsync()
{ {
LoginInput = new LoginInputModel(); LoginInput = new LoginInputModel();
@ -71,8 +73,8 @@ namespace Volo.Abp.Account.Web.Pages.Account
}) })
.ToList(); .ToList();
EnableLocalLogin = true; //TODO: We can get default from a setting? EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin);
ExternalProviders = providers.ToArray(); ExternalProviders = providers.ToArray();
if (IsExternalLoginOnly) if (IsExternalLoginOnly)
@ -80,12 +82,14 @@ namespace Volo.Abp.Account.Web.Pages.Account
//return await ExternalLogin(vm.ExternalLoginScheme, returnUrl); //return await ExternalLogin(vm.ExternalLoginScheme, returnUrl);
throw new NotImplementedException(); throw new NotImplementedException();
} }
return Page();
} }
[UnitOfWork] //TODO: Will be removed when we implement action filter [UnitOfWork] //TODO: Will be removed when we implement action filter
public virtual async Task<IActionResult> OnPostAsync(string action) public virtual async Task<IActionResult> OnPostAsync(string action)
{ {
EnableLocalLogin = true; //TODO: We can get default from a setting? EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin);
ValidateModel(); ValidateModel();

4
modules/account/src/Volo.Abp.Account.Web/Settings/AccountSettingDefinitionProvider.cs

@ -9,6 +9,10 @@ namespace Volo.Abp.Account.Web.Settings
context.Add( context.Add(
new SettingDefinition(AccountSettingNames.IsSelfRegistrationEnabled, "true") new SettingDefinition(AccountSettingNames.IsSelfRegistrationEnabled, "true")
); );
context.Add(
new SettingDefinition(AccountSettingNames.EnableLocalLogin, "true")
);
} }
} }
} }

2
modules/account/src/Volo.Abp.Account.Web/Settings/AccountSettingNames.cs

@ -3,5 +3,7 @@
public class AccountSettingNames public class AccountSettingNames
{ {
public const string IsSelfRegistrationEnabled = "Abp.Account.IsSelfRegistrationEnabled"; public const string IsSelfRegistrationEnabled = "Abp.Account.IsSelfRegistrationEnabled";
public const string EnableLocalLogin = "Abp.Account.EnableLocalLogin";
} }
} }

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

@ -34,7 +34,7 @@ namespace Volo.Abp.Identity
public virtual string Name { get; set; } public virtual string Name { get; set; }
/// <summary> /// <summary>
/// Gets or sets the Surame for the user. /// Gets or sets the Surname for the user.
/// </summary> /// </summary>
public virtual string Surname { get; set; } public virtual string Surname { get; set; }

8
modules/identityserver/src/Volo.Abp.IdentityServer.Domain.Shared/Volo/Abp/IdentityServer/Localization/Resources/tr.json

@ -0,0 +1,8 @@
{
"culture": "tr",
"texts": {
"Volo.IdentityServer:DuplicateIdentityResourceName": "Identity Resource adı zaten mevcut: {Name}",
"Volo.IdentityServer:DuplicateApiResourceName": "Api Resource adı zaten mevcut: {Name}",
"Volo.IdentityServer:DuplicateClientId": "ClientId already zaten mevcut: {ClientId}"
}
}

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

@ -17,6 +17,9 @@
var _dataTable = _$wrapper.find('table').DataTable(abp.libs.datatables.normalizeConfiguration({ var _dataTable = _$wrapper.find('table').DataTable(abp.libs.datatables.normalizeConfiguration({
order: [[1, "asc"]], order: [[1, "asc"]],
processing: true,
paging: true,
serverSide: true,
ajax: abp.libs.datatables.createAjax(_tenantAppService.getList), ajax: abp.libs.datatables.createAjax(_tenantAppService.getList),
columnDefs: [ columnDefs: [
{ {

3
npm/ng-packs/.prettierrc

@ -1,6 +1,5 @@
{ {
"printWidth": 120, "printWidth": 100,
"singleQuote": true, "singleQuote": true,
"trailingComma": "all" "trailingComma": "all"
} }

26
npm/ng-packs/packages/account/src/lib/components/register/register.component.ts

@ -1,4 +1,4 @@
import { ConfigState, GetAppConfiguration, ABP } from '@abp/ng.core'; import { ConfigState, GetAppConfiguration, ABP, SessionState } from '@abp/ng.core';
import { ToasterService } from '@abp/ng.theme.shared'; import { ToasterService } from '@abp/ng.theme.shared';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@ -11,6 +11,7 @@ import snq from 'snq';
import { RegisterRequest } from '../../models'; import { RegisterRequest } from '../../models';
import { AccountService } from '../../services/account.service'; import { AccountService } from '../../services/account.service';
import { PasswordRules, validatePassword } from '@ngx-validate/core'; import { PasswordRules, validatePassword } from '@ngx-validate/core';
import { HttpHeaders } from '@angular/common/http';
const { maxLength, minLength, required, email } = Validators; const { maxLength, minLength, required, email } = Validators;
@Component({ @Component({
@ -29,7 +30,9 @@ export class RegisterComponent implements OnInit {
private store: Store, private store: Store,
private toasterService: ToasterService, private toasterService: ToasterService,
) { ) {
this.oauthService.configure(this.store.selectSnapshot(ConfigState.getOne('environment')).oAuthConfig); this.oauthService.configure(
this.store.selectSnapshot(ConfigState.getOne('environment')).oAuthConfig,
);
this.oauthService.loadDiscoveryDocument(); this.oauthService.loadDiscoveryDocument();
} }
@ -62,7 +65,10 @@ export class RegisterComponent implements OnInit {
this.form = this.fb.group({ this.form = this.fb.group({
username: ['', [required, maxLength(255)]], username: ['', [required, maxLength(255)]],
password: ['', [required, validatePassword(passwordRulesArr), minLength(requiredLength), maxLength(32)]], password: [
'',
[required, validatePassword(passwordRulesArr), minLength(requiredLength), maxLength(32)],
],
email: ['', [required, email]], email: ['', [required, email]],
}); });
} }
@ -79,10 +85,22 @@ export class RegisterComponent implements OnInit {
appName: 'Angular', appName: 'Angular',
} as RegisterRequest; } as RegisterRequest;
const tenant = this.store.selectSnapshot(SessionState.getTenant);
this.accountService this.accountService
.register(newUser) .register(newUser)
.pipe( .pipe(
switchMap(() => from(this.oauthService.fetchTokenUsingPasswordFlow(newUser.userName, newUser.password))), switchMap(() =>
from(
this.oauthService.fetchTokenUsingPasswordFlow(
newUser.userName,
newUser.password,
new HttpHeaders({
...(tenant && tenant.id && { __tenant: tenant.id }),
}),
),
),
),
switchMap(() => this.store.dispatch(new GetAppConfiguration())), switchMap(() => this.store.dispatch(new GetAppConfiguration())),
tap(() => this.store.dispatch(new Navigate(['/']))), tap(() => this.store.dispatch(new Navigate(['/']))),
take(1), take(1),

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

@ -28,6 +28,7 @@ import { ConfigState } from './states/config.state';
import { ProfileState } from './states/profile.state'; import { ProfileState } from './states/profile.state';
import { SessionState } from './states/session.state'; import { SessionState } from './states/session.state';
import { getInitialData, localeInitializer } from './utils/initial-utils'; import { getInitialData, localeInitializer } from './utils/initial-utils';
import './utils/date-extensions';
@NgModule({ @NgModule({
imports: [ imports: [

67
npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts

@ -1,31 +1,68 @@
import { Directive, ElementRef, Input, OnDestroy, OnInit, Optional, Renderer2 } from '@angular/core'; import {
Directive,
ElementRef,
Input,
OnDestroy,
OnInit,
Renderer2,
ViewContainerRef,
TemplateRef,
Optional,
SimpleChanges,
OnChanges,
} from '@angular/core';
import { Store } from '@ngxs/store'; import { Store } from '@ngxs/store';
import { ConfigState } from '../states'; import { ConfigState } from '../states';
import { takeUntilDestroy } from '../utils'; import { takeUntilDestroy } from '../utils';
import { Subscription } from 'rxjs';
@Directive({ @Directive({
selector: '[abpPermission]', selector: '[abpPermission]',
}) })
export class PermissionDirective implements OnInit, OnDestroy { export class PermissionDirective implements OnInit, OnDestroy, OnChanges {
@Input('abpPermission') condition: string; @Input('abpPermission') condition: string;
constructor(@Optional() private elRef: ElementRef, private renderer: Renderer2, private store: Store) {} subscription: Subscription;
constructor(
private elRef: ElementRef,
private renderer: Renderer2,
private store: Store,
@Optional() private templateRef: TemplateRef<any>,
private vcRef: ViewContainerRef,
) {}
private check() {
if (this.subscription) {
this.subscription.unsubscribe();
}
this.subscription = this.store
.select(ConfigState.getGrantedPolicy(this.condition))
.pipe(takeUntilDestroy(this))
.subscribe(isGranted => {
if (this.templateRef && isGranted) {
this.vcRef.clear();
this.vcRef.createEmbeddedView(this.templateRef);
} else if (this.templateRef && !isGranted) {
this.vcRef.clear();
} else if (!isGranted && !this.templateRef) {
this.renderer.removeChild((this.elRef.nativeElement as HTMLElement).parentElement, this.elRef.nativeElement);
}
});
}
ngOnInit() { ngOnInit() {
if (this.condition) { if (this.templateRef && !this.condition) {
this.store this.vcRef.createEmbeddedView(this.templateRef);
.select(ConfigState.getGrantedPolicy(this.condition))
.pipe(takeUntilDestroy(this))
.subscribe(isGranted => {
if (!isGranted) {
this.renderer.removeChild(
(this.elRef.nativeElement as HTMLElement).parentElement,
this.elRef.nativeElement,
);
}
});
} }
} }
ngOnDestroy(): void {} ngOnDestroy(): void {}
ngOnChanges({ condition }: SimpleChanges) {
if ((condition || { currentValue: null }).currentValue) {
this.check();
}
}
} }

15
npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts

@ -1,10 +1,11 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate } from '@angular/router'; import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { Store } from '@ngxs/store'; import { Store } from '@ngxs/store';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ConfigState } from '../states';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import snq from 'snq';
import { RestOccurError } from '../actions'; import { RestOccurError } from '../actions';
import { ConfigState } from '../states';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -12,8 +13,14 @@ import { RestOccurError } from '../actions';
export class PermissionGuard implements CanActivate { export class PermissionGuard implements CanActivate {
constructor(private store: Store) {} constructor(private store: Store) {}
canActivate({ data }: ActivatedRouteSnapshot): Observable<boolean> { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
const resource = data.requiredPolicy as string; let resource = snq(() => route.data.routes.requiredPolicy) || snq(() => route.data.requiredPolicy as string);
if (!resource) {
resource = snq(
() => route.routeConfig.children.find(child => state.url.indexOf(child.path) > -1).data.requiredPolicy,
);
}
return this.store.select(ConfigState.getGrantedPolicy(resource)).pipe( return this.store.select(ConfigState.getGrantedPolicy(resource)).pipe(
tap(access => { tap(access => {
if (!access) { if (!access) {

20
npm/ng-packs/packages/core/src/lib/pipes/sort.pipe.ts

@ -5,7 +5,11 @@ export type SortOrder = 'asc' | 'desc';
name: 'abpSort', name: 'abpSort',
}) })
export class SortPipe implements PipeTransform { export class SortPipe implements PipeTransform {
transform(value: any[], sortOrder: SortOrder | string = 'asc', sortKey?: string): any { transform(
value: any[],
sortOrder: SortOrder | string = 'asc',
sortKey?: string,
): any {
sortOrder = sortOrder && (sortOrder.toLowerCase() as any); sortOrder = sortOrder && (sortOrder.toLowerCase() as any);
if (!value || (sortOrder !== 'asc' && sortOrder !== 'desc')) return value; if (!value || (sortOrder !== 'asc' && sortOrder !== 'desc')) return value;
@ -17,7 +21,9 @@ export class SortPipe implements PipeTransform {
numberArray = value.filter(item => typeof item === 'number').sort(); numberArray = value.filter(item => typeof item === 'number').sort();
stringArray = value.filter(item => typeof item === 'string').sort(); stringArray = value.filter(item => typeof item === 'string').sort();
} else { } else {
numberArray = value.filter(item => typeof item[sortKey] === 'number').sort((a, b) => a[sortKey] - b[sortKey]); numberArray = value
.filter(item => typeof item[sortKey] === 'number')
.sort((a, b) => a[sortKey] - b[sortKey]);
stringArray = value stringArray = value
.filter(item => typeof item[sortKey] === 'string') .filter(item => typeof item[sortKey] === 'string')
.sort((a, b) => { .sort((a, b) => {
@ -26,7 +32,15 @@ export class SortPipe implements PipeTransform {
else return 0; else return 0;
}); });
} }
const sorted = numberArray.concat(stringArray); const sorted = [
...numberArray,
...stringArray,
...value.filter(
item =>
typeof (sortKey ? item[sortKey] : item) !== 'number' &&
typeof (sortKey ? item[sortKey] : item) !== 'string',
),
];
return sortOrder === 'asc' ? sorted : sorted.reverse(); return sortOrder === 'asc' ? sorted : sorted.reverse();
} }
} }

34
npm/ng-packs/packages/core/src/lib/services/rest.service.ts

@ -23,17 +23,31 @@ export class RestService {
config = config || ({} as Rest.Config); config = config || ({} as Rest.Config);
const { observe = Rest.Observe.Body, skipHandleError } = config; const { observe = Rest.Observe.Body, skipHandleError } = config;
const url = (api || this.store.selectSnapshot(ConfigState.getApiUrl())) + request.url; const url = (api || this.store.selectSnapshot(ConfigState.getApiUrl())) + request.url;
const { method, ...options } = request; const { method, params, ...options } = request;
return this.http.request<T>(method, url, { observe, ...options } as any).pipe( return this.http
observe === Rest.Observe.Body ? take(1) : tap(), .request<T>(method, url, {
catchError(err => { observe,
if (skipHandleError) { ...(params && {
return throwError(err); params: Object.keys(params).reduce(
} (acc, key) => ({
...acc,
...(typeof params[key] !== 'undefined' && params[key] !== '' && { [key]: params[key] }),
}),
{},
),
}),
...options,
} as any)
.pipe(
observe === Rest.Observe.Body ? take(1) : tap(),
catchError(err => {
if (skipHandleError) {
return throwError(err);
}
return this.handleError(err); return this.handleError(err);
}), }),
); );
} }
} }

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

@ -1,8 +1,18 @@
import { Action, createSelector, Selector, State, StateContext, Store } from '@ngxs/store'; import {
Action,
createSelector,
Selector,
State,
StateContext,
Store,
} from '@ngxs/store';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators'; import { switchMap, tap } from 'rxjs/operators';
import snq from 'snq'; import snq from 'snq';
import { GetAppConfiguration, PatchRouteByName } from '../actions/config.actions'; import {
GetAppConfiguration,
PatchRouteByName,
} from '../actions/config.actions';
import { SetLanguage } from '../actions/session.actions'; import { SetLanguage } from '../actions/session.actions';
import { ABP } from '../models/common'; import { ABP } from '../models/common';
import { Config } from '../models/config'; import { Config } from '../models/config';
@ -26,12 +36,9 @@ export class ConfigState {
} }
static getOne(key: string) { static getOne(key: string) {
const selector = createSelector( const selector = createSelector([ConfigState], (state: Config.State) => {
[ConfigState], return state[key];
(state: Config.State) => { });
return state[key];
},
);
return selector; return selector;
} }
@ -45,36 +52,32 @@ export class ConfigState {
throw new Error('The argument must be a dot string or an string array.'); throw new Error('The argument must be a dot string or an string array.');
} }
const selector = createSelector( const selector = createSelector([ConfigState], (state: Config.State) => {
[ConfigState], return (keys as string[]).reduce((acc, val) => {
(state: Config.State) => { if (acc) {
return (keys as string[]).reduce((acc, val) => { return acc[val];
if (acc) { }
return acc[val];
} return undefined;
}, state);
return undefined; });
}, state);
},
);
return selector; return selector;
} }
static getRoute(path?: string, name?: string) { static getRoute(path?: string, name?: string, url?: string) {
const selector = createSelector( const selector = createSelector([ConfigState], (state: Config.State) => {
[ConfigState], const { flattedRoutes } = state;
(state: Config.State) => { return (flattedRoutes as ABP.FullRoute[]).find(route => {
const { flattedRoutes } = state; if (path && route.path === path) {
return (flattedRoutes as ABP.FullRoute[]).find(route => { return route;
if (path && route.path === path) { } else if (name && route.name === name) {
return route; return route;
} else if (name && route.name === name) { } else if (url && route.url === url) {
return route; return route;
} }
}); });
}, });
);
return selector; return selector;
} }
@ -91,30 +94,33 @@ export class ConfigState {
} }
static getSetting(key: string) { static getSetting(key: string) {
const selector = createSelector( const selector = createSelector([ConfigState], (state: Config.State) => {
[ConfigState], return snq(() => state.setting.values[key]);
(state: Config.State) => { });
return snq(() => state.setting.values[key]);
},
);
return selector; return selector;
} }
static getSettings(keyword?: string) { static getSettings(keyword?: string) {
const selector = createSelector( const selector = createSelector([ConfigState], (state: Config.State) => {
[ConfigState], if (keyword) {
(state: Config.State) => { const keys = snq(
if (keyword) { () =>
const keys = snq(() => Object.keys(state.setting.values).filter(key => key.indexOf(keyword) > -1), []); Object.keys(state.setting.values).filter(
key => key.indexOf(keyword) > -1,
if (keys.length) { ),
return keys.reduce((acc, key) => ({ ...acc, [key]: state.setting.values[key] }), {}); [],
} );
if (keys.length) {
return keys.reduce(
(acc, key) => ({ ...acc, [key]: state.setting.values[key] }),
{},
);
} }
}
return snq(() => state.setting.values, {}); return snq(() => state.setting.values, {});
}, });
);
return selector; return selector;
} }
@ -123,14 +129,36 @@ export class ConfigState {
[ConfigState], [ConfigState],
(state: Config.State): boolean => { (state: Config.State): boolean => {
if (!key) return true; if (!key) return true;
return snq(() => state.auth.grantedPolicies[key], false); const getPolicy = k => snq(() => state.auth.grantedPolicies[k], false);
const orRegexp = /\|\|/g;
const andRegexp = /&&/g;
if (orRegexp.test(key)) {
const keys = key.split('||').filter(k => !!k);
if (keys.length !== 2) return false;
return getPolicy(keys[0].trim()) || getPolicy(keys[1].trim());
} else if (andRegexp.test(key)) {
const keys = key.split('&&').filter(k => !!k);
if (keys.length !== 2) return false;
return getPolicy(keys[0].trim()) && getPolicy(keys[1].trim());
}
return getPolicy(key);
}, },
); );
return selector; return selector;
} }
static getLocalization(key: string | Config.LocalizationWithDefault, ...interpolateParams: string[]) { static getLocalization(
key: string | Config.LocalizationWithDefault,
...interpolateParams: string[]
) {
let defaultValue: string; let defaultValue: string;
if (typeof key !== 'string') { if (typeof key !== 'string') {
@ -141,52 +169,52 @@ export class ConfigState {
if (!key) key = ''; if (!key) key = '';
const keys = key.split('::') as string[]; const keys = key.split('::') as string[];
const selector = createSelector( const selector = createSelector([ConfigState], (state: Config.State) => {
[ConfigState], if (!state.localization) return defaultValue || key;
(state: Config.State) => {
if (!state.localization) return defaultValue || key; const { defaultResourceName } = state.environment.localization;
if (keys[0] === '') {
const { defaultResourceName } = state.environment.localization; if (!defaultResourceName) {
if (keys[0] === '') { throw new Error(
if (!defaultResourceName) { `Please check your environment. May you forget set defaultResourceName?
throw new Error(
`Please check your environment. May you forget set defaultResourceName?
Here is the example: Here is the example:
{ production: false, { production: false,
localization: { localization: {
defaultResourceName: 'MyProjectName' defaultResourceName: 'MyProjectName'
} }
}`, }`,
); );
}
keys[0] = snq(() => defaultResourceName);
} }
let localization = (keys as any).reduce((acc, val) => { keys[0] = snq(() => defaultResourceName);
if (acc) { }
return acc[val];
}
return undefined;
}, state.localization.values);
interpolateParams = interpolateParams.filter(params => params != null); let localization = (keys as any).reduce((acc, val) => {
if (localization && interpolateParams && interpolateParams.length) { if (acc) {
interpolateParams.forEach(param => { return acc[val];
localization = localization.replace(/[\'\"]?\{[\d]+\}[\'\"]?/, param);
});
} }
if (typeof localization !== 'string') localization = ''; return undefined;
return localization || defaultValue || key; }, state.localization.values);
},
); interpolateParams = interpolateParams.filter(params => params != null);
if (localization && interpolateParams && interpolateParams.length) {
interpolateParams.forEach(param => {
localization = localization.replace(/[\'\"]?\{[\d]+\}[\'\"]?/, param);
});
}
if (typeof localization !== 'string') localization = '';
return localization || defaultValue || key;
});
return selector; return selector;
} }
constructor(private appConfigurationService: ApplicationConfigurationService, private store: Store) {} constructor(
private appConfigurationService: ApplicationConfigurationService,
private store: Store,
) {}
@Action(GetAppConfiguration) @Action(GetAppConfiguration)
addData({ patchState, dispatch }: StateContext<Config.State>) { addData({ patchState, dispatch }: StateContext<Config.State>) {
@ -197,19 +225,25 @@ export class ConfigState {
}), }),
), ),
switchMap(configuration => { switchMap(configuration => {
let defaultLang: string = configuration.setting.values['Abp.Localization.DefaultLanguage']; let defaultLang: string =
configuration.setting.values['Abp.Localization.DefaultLanguage'];
if (defaultLang.includes(';')) { if (defaultLang.includes(';')) {
defaultLang = defaultLang.split(';')[0]; defaultLang = defaultLang.split(';')[0];
} }
return this.store.selectSnapshot(SessionState.getLanguage) ? of(null) : dispatch(new SetLanguage(defaultLang)); return this.store.selectSnapshot(SessionState.getLanguage)
? of(null)
: dispatch(new SetLanguage(defaultLang));
}), }),
); );
} }
@Action(PatchRouteByName) @Action(PatchRouteByName)
patchRoute({ patchState, getState }: StateContext<Config.State>, { name, newValue }: PatchRouteByName) { patchRoute(
{ patchState, getState }: StateContext<Config.State>,
{ name, newValue }: PatchRouteByName,
) {
let routes: ABP.FullRoute[] = getState().routes; let routes: ABP.FullRoute[] = getState().routes;
const index = routes.findIndex(route => route.name === name); const index = routes.findIndex(route => route.name === name);
@ -230,7 +264,9 @@ function patchRouteDeep(
): ABP.FullRoute[] { ): ABP.FullRoute[] {
routes = routes.map(route => { routes = routes.map(route => {
if (route.name === name) { if (route.name === name) {
newValue.url = `${parentUrl}/${(!newValue.path && newValue.path === '' ? route.path : newValue.path) || ''}`; newValue.url = `${parentUrl}/${(!newValue.path && newValue.path === ''
? route.path
: newValue.path) || ''}`;
if (newValue.children && newValue.children.length) { if (newValue.children && newValue.children.length) {
newValue.children = newValue.children.map(child => ({ newValue.children = newValue.children.map(child => ({
@ -241,7 +277,12 @@ function patchRouteDeep(
return { ...route, ...newValue }; return { ...route, ...newValue };
} else if (route.children && route.children.length) { } else if (route.children && route.children.length) {
route.children = patchRouteDeep(route.children, name, newValue, (parentUrl || '/') + route.path); route.children = patchRouteDeep(
route.children,
name,
newValue,
(parentUrl || '/') + route.path,
);
} }
return route; return route;

138
npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts

@ -1,8 +1,15 @@
import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator/jest'; import {
createServiceFactory,
SpectatorService,
SpyObject,
} from '@ngneat/spectator/jest';
import { Store } from '@ngxs/store'; import { Store } from '@ngxs/store';
import { ReplaySubject, timer, Subject, of } from 'rxjs'; import { ReplaySubject, timer, Subject, of } from 'rxjs';
import { Config } from '../models/config'; import { Config } from '../models/config';
import { ApplicationConfigurationService, ConfigStateService } from '../services'; import {
ApplicationConfigurationService,
ConfigStateService,
} from '../services';
import { ConfigState } from '../states'; import { ConfigState } from '../states';
import { SetLanguage, PatchRouteByName } from '../actions'; import { SetLanguage, PatchRouteByName } from '../actions';
@ -91,6 +98,7 @@ export const CONFIG_STATE_DATA = {
}, },
grantedPolicies: { grantedPolicies: {
'Abp.Identity': false, 'Abp.Identity': false,
'Abp.Account': true,
}, },
}, },
setting: { setting: {
@ -126,7 +134,10 @@ describe('ConfigState', () => {
store = spectator.get(Store); store = spectator.get(Store);
service = spectator.service; service = spectator.service;
appConfigService = spectator.get(ApplicationConfigurationService); appConfigService = spectator.get(ApplicationConfigurationService);
state = new ConfigState(spectator.get(ApplicationConfigurationService), store); state = new ConfigState(
spectator.get(ApplicationConfigurationService),
store,
);
}); });
describe('#getAll', () => { describe('#getAll', () => {
@ -137,24 +148,34 @@ describe('ConfigState', () => {
describe('#getApplicationInfo', () => { describe('#getApplicationInfo', () => {
it('should return application property', () => { it('should return application property', () => {
expect(ConfigState.getApplicationInfo(CONFIG_STATE_DATA)).toEqual(CONFIG_STATE_DATA.environment.application); expect(ConfigState.getApplicationInfo(CONFIG_STATE_DATA)).toEqual(
CONFIG_STATE_DATA.environment.application,
);
}); });
}); });
describe('#getOne', () => { describe('#getOne', () => {
it('should return one property', () => { it('should return one property', () => {
expect(ConfigState.getOne('environment')(CONFIG_STATE_DATA)).toEqual(CONFIG_STATE_DATA.environment); expect(ConfigState.getOne('environment')(CONFIG_STATE_DATA)).toEqual(
CONFIG_STATE_DATA.environment,
);
}); });
}); });
describe('#getDeep', () => { describe('#getDeep', () => {
it('should return deeper', () => { it('should return deeper', () => {
expect(ConfigState.getDeep('environment.localization.defaultResourceName')(CONFIG_STATE_DATA)).toEqual( expect(
CONFIG_STATE_DATA.environment.localization.defaultResourceName, ConfigState.getDeep('environment.localization.defaultResourceName')(
); CONFIG_STATE_DATA,
expect(ConfigState.getDeep(['environment', 'localization', 'defaultResourceName'])(CONFIG_STATE_DATA)).toEqual( ),
CONFIG_STATE_DATA.environment.localization.defaultResourceName, ).toEqual(CONFIG_STATE_DATA.environment.localization.defaultResourceName);
); expect(
ConfigState.getDeep([
'environment',
'localization',
'defaultResourceName',
])(CONFIG_STATE_DATA),
).toEqual(CONFIG_STATE_DATA.environment.localization.defaultResourceName);
expect(ConfigState.getDeep('test')(null)).toBeFalsy(); expect(ConfigState.getDeep('test')(null)).toBeFalsy();
}); });
@ -162,21 +183,33 @@ describe('ConfigState', () => {
describe('#getRoute', () => { describe('#getRoute', () => {
it('should return route', () => { it('should return route', () => {
expect(ConfigState.getRoute(null, '::Menu:Home')(CONFIG_STATE_DATA)).toEqual(CONFIG_STATE_DATA.flattedRoutes[0]); expect(
expect(ConfigState.getRoute('identity')(CONFIG_STATE_DATA)).toEqual(CONFIG_STATE_DATA.flattedRoutes[1]); ConfigState.getRoute(null, '::Menu:Home')(CONFIG_STATE_DATA),
).toEqual(CONFIG_STATE_DATA.flattedRoutes[0]);
expect(ConfigState.getRoute('identity')(CONFIG_STATE_DATA)).toEqual(
CONFIG_STATE_DATA.flattedRoutes[1],
);
}); });
}); });
describe('#getApiUrl', () => { describe('#getApiUrl', () => {
it('should return api url', () => { it('should return api url', () => {
expect(ConfigState.getApiUrl('other')(CONFIG_STATE_DATA)).toEqual(CONFIG_STATE_DATA.environment.apis.other.url); expect(ConfigState.getApiUrl('other')(CONFIG_STATE_DATA)).toEqual(
expect(ConfigState.getApiUrl()(CONFIG_STATE_DATA)).toEqual(CONFIG_STATE_DATA.environment.apis.default.url); CONFIG_STATE_DATA.environment.apis.other.url,
);
expect(ConfigState.getApiUrl()(CONFIG_STATE_DATA)).toEqual(
CONFIG_STATE_DATA.environment.apis.default.url,
);
}); });
}); });
describe('#getSetting', () => { describe('#getSetting', () => {
it('should return a setting', () => { it('should return a setting', () => {
expect(ConfigState.getSetting('Abp.Localization.DefaultLanguage')(CONFIG_STATE_DATA)).toEqual( expect(
ConfigState.getSetting('Abp.Localization.DefaultLanguage')(
CONFIG_STATE_DATA,
),
).toEqual(
CONFIG_STATE_DATA.setting.values['Abp.Localization.DefaultLanguage'], CONFIG_STATE_DATA.setting.values['Abp.Localization.DefaultLanguage'],
); );
}); });
@ -184,41 +217,82 @@ describe('ConfigState', () => {
describe('#getSettings', () => { describe('#getSettings', () => {
it('should return settings', () => { it('should return settings', () => {
expect(ConfigState.getSettings('Localization')(CONFIG_STATE_DATA)).toEqual({ expect(
ConfigState.getSettings('Localization')(CONFIG_STATE_DATA),
).toEqual({
'Abp.Localization.DefaultLanguage': 'en', 'Abp.Localization.DefaultLanguage': 'en',
}); });
expect(ConfigState.getSettings('AllSettings')(CONFIG_STATE_DATA)).toEqual(CONFIG_STATE_DATA.setting.values); expect(ConfigState.getSettings('AllSettings')(CONFIG_STATE_DATA)).toEqual(
CONFIG_STATE_DATA.setting.values,
);
}); });
}); });
describe('#getGrantedPolicy', () => { describe('#getGrantedPolicy', () => {
it('should return a granted policy', () => { it('should return a granted policy', () => {
expect(ConfigState.getGrantedPolicy('Abp.Identity')(CONFIG_STATE_DATA)).toBe(false); expect(
ConfigState.getGrantedPolicy('Abp.Identity')(CONFIG_STATE_DATA),
).toBe(false);
expect(
ConfigState.getGrantedPolicy('Abp.Identity || Abp.Account')(
CONFIG_STATE_DATA,
),
).toBe(true);
expect(
ConfigState.getGrantedPolicy('Abp.Account && Abp.Identity')(
CONFIG_STATE_DATA,
),
).toBe(false);
expect(
ConfigState.getGrantedPolicy('Abp.Account &&')(CONFIG_STATE_DATA),
).toBe(false);
expect(
ConfigState.getGrantedPolicy('|| Abp.Account')(CONFIG_STATE_DATA),
).toBe(false);
expect(ConfigState.getGrantedPolicy('')(CONFIG_STATE_DATA)).toBe(true); expect(ConfigState.getGrantedPolicy('')(CONFIG_STATE_DATA)).toBe(true);
}); });
}); });
describe('#getLocalization', () => { describe('#getLocalization', () => {
it('should return a localization', () => { it('should return a localization', () => {
expect(ConfigState.getLocalization('AbpIdentity::Identity')(CONFIG_STATE_DATA)).toBe('identity'); expect(
ConfigState.getLocalization('AbpIdentity::Identity')(CONFIG_STATE_DATA),
expect(ConfigState.getLocalization('AbpIdentity::NoIdentity')(CONFIG_STATE_DATA)).toBe('AbpIdentity::NoIdentity'); ).toBe('identity');
expect(ConfigState.getLocalization({ key: '', defaultValue: 'default' })(CONFIG_STATE_DATA)).toBe('default'); expect(
ConfigState.getLocalization('AbpIdentity::NoIdentity')(
expect(ConfigState.getLocalization("::'{0}' and '{1}' do not match.", 'first', 'second')(CONFIG_STATE_DATA)).toBe( CONFIG_STATE_DATA,
'first and second do not match.', ),
); ).toBe('AbpIdentity::NoIdentity');
expect(
ConfigState.getLocalization({ key: '', defaultValue: 'default' })(
CONFIG_STATE_DATA,
),
).toBe('default');
expect(
ConfigState.getLocalization(
"::'{0}' and '{1}' do not match.",
'first',
'second',
)(CONFIG_STATE_DATA),
).toBe('first and second do not match.');
try { try {
ConfigState.getLocalization('::Test')({ ConfigState.getLocalization('::Test')({
...CONFIG_STATE_DATA, ...CONFIG_STATE_DATA,
environment: { ...CONFIG_STATE_DATA.environment, localization: {} as any }, environment: {
...CONFIG_STATE_DATA.environment,
localization: {} as any,
},
}); });
expect(false).toBeTruthy(); // fail expect(false).toBeTruthy(); // fail
} catch (error) { } catch (error) {
expect((error as Error).message).toContain('Please check your environment'); expect((error as Error).message).toContain(
'Please check your environment',
);
} }
}); });
}); });
@ -273,7 +347,9 @@ describe('ConfigState', () => {
name: 'Home', name: 'Home',
path: 'home', path: 'home',
url: '/home', url: '/home',
children: [{ path: 'dashboard', name: 'Dashboard', url: '/home/dashboard' }], children: [
{ path: 'dashboard', name: 'Dashboard', url: '/home/dashboard' },
],
}); });
}); });

39
npm/ng-packs/packages/core/src/lib/tests/permission.directive.spec.ts

@ -43,4 +43,43 @@ describe('PermissionDirective', () => {
expect(spy.mock.calls).toHaveLength(0); expect(spy.mock.calls).toHaveLength(0);
}); });
}); });
describe('structural', () => {
beforeEach(() => {
spectator = createDirective(
'<div id="test-element" *abpPermission="condition">Testing Permission Directive</div>',
{ hostProps: { condition: '' } },
);
directive = spectator.directive;
});
it('should be created', () => {
expect(directive).toBeTruthy();
});
it('should remove the element from DOM', () => {
expect(spectator.query('#test-element')).toBeTruthy();
expect(spectator.directive.subscription).toBeUndefined();
spectator.setHostInput({ condition: 'test' });
expect(spectator.directive.subscription).toBeTruthy();
grantedPolicy$.next(true);
expect(spectator.query('#test-element')).toBeTruthy();
grantedPolicy$.next(false);
expect(spectator.query('#test-element')).toBeFalsy();
grantedPolicy$.next(true);
grantedPolicy$.next(true);
expect(spectator.queryAll('#test-element')).toHaveLength(1);
});
describe('#subscription', () => {
it('should call the unsubscribe', () => {
const spy = jest.fn(() => {});
spectator.setHostInput({ condition: 'test' });
spectator.directive.subscription.unsubscribe = spy;
spectator.setHostInput({ condition: 'test2' });
expect(spy).toHaveBeenCalled();
});
});
});
}); });

18
npm/ng-packs/packages/core/src/lib/tests/permission.guard.spec.ts

@ -23,7 +23,7 @@ describe('PermissionGuard', () => {
it('should return true when the grantedPolicy is true', done => { it('should return true when the grantedPolicy is true', done => {
store.select.andReturn(of(true)); store.select.andReturn(of(true));
const spy = jest.spyOn(store, 'dispatch'); const spy = jest.spyOn(store, 'dispatch');
guard.canActivate({ data: { requiredPolicy: '' } } as any).subscribe(res => { guard.canActivate({ data: { requiredPolicy: 'test' } } as any, null).subscribe(res => {
expect(res).toBe(true); expect(res).toBe(true);
expect(spy.mock.calls).toHaveLength(0); expect(spy.mock.calls).toHaveLength(0);
done(); done();
@ -33,11 +33,25 @@ describe('PermissionGuard', () => {
it('should return false and dispatch RestOccurError when the grantedPolicy is false', done => { it('should return false and dispatch RestOccurError when the grantedPolicy is false', done => {
store.select.andReturn(of(false)); store.select.andReturn(of(false));
const spy = jest.spyOn(store, 'dispatch'); const spy = jest.spyOn(store, 'dispatch');
guard.canActivate({ data: { requiredPolicy: '' } } as any).subscribe(res => { guard.canActivate({ data: { requiredPolicy: 'test' } } as any, null).subscribe(res => {
expect(res).toBe(false); expect(res).toBe(false);
expect(spy.mock.calls[0][0] instanceof RestOccurError).toBeTruthy(); expect(spy.mock.calls[0][0] instanceof RestOccurError).toBeTruthy();
expect((spy.mock.calls[0][0] as RestOccurError).payload).toEqual({ status: 403 }); expect((spy.mock.calls[0][0] as RestOccurError).payload).toEqual({ status: 403 });
done(); done();
}); });
}); });
it('should find the requiredPolicy from child route', done => {
store.select.andReturn(of(false));
const spy = jest.spyOn(store, 'select');
guard
.canActivate(
{ data: {}, routeConfig: { children: [{ path: 'test', data: { requiredPolicy: 'TestPolicy' } }] } } as any,
{ url: 'test' } as any,
)
.subscribe(() => {
expect(spy.mock.calls[0][0]({ auth: { grantedPolicies: { TestPolicy: true } } })).toBe(true);
done();
});
});
}); });

24
npm/ng-packs/packages/core/src/lib/tests/sort.pipe.spec.ts

@ -17,10 +17,28 @@ describe('SortPipe', () => {
}); });
test('should sort object array in given order with given key', () => { test('should sort object array in given order with given key', () => {
const array = [{ key: 5 }, { key: 'b' }, { key: 1 }, { key: 'a' }]; const array = [
{ key: 5 },
{ key: 'b' },
{ key: 1 },
{ key: 'a' },
{ key: null },
];
expect(pipe.transform(array, 'asc', 'key')).toEqual([{ key: 1 }, { key: 5 }, { key: 'a' }, { key: 'b' }]); expect(pipe.transform(array, 'asc', 'key')).toEqual([
expect(pipe.transform(array, 'desc', 'key')).toEqual([{ key: 'b' }, { key: 'a' }, { key: 5 }, { key: 1 }]); { key: 1 },
{ key: 5 },
{ key: 'a' },
{ key: 'b' },
{ key: null },
]);
expect(pipe.transform(array, 'desc', 'key')).toEqual([
{ key: null },
{ key: 'b' },
{ key: 'a' },
{ key: 5 },
{ key: 1 },
]);
}); });
test('should require an array as value', () => { test('should require an array as value', () => {

13
npm/ng-packs/packages/core/src/lib/utils/date-extensions.ts

@ -0,0 +1,13 @@
export {};
declare global {
interface Date {
toLocalISOString(): string;
}
}
Date.prototype.toLocalISOString = function(this: Date): string {
const timezoneOffset = this.getTimezoneOffset();
return new Date(this.getTime() - timezoneOffset * 60000).toISOString();
};

8
npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.html

@ -6,7 +6,7 @@
</div> </div>
<div class="text-right col col-md-6"> <div class="text-right col col-md-6">
<button <button
[abpPermission]="'AbpIdentity.Roles.Create'" *abpPermission="'AbpIdentity.Roles.Create'"
id="create-role" id="create-role"
class="btn btn-primary" class="btn btn-primary"
type="button" type="button"
@ -73,18 +73,18 @@
<i class="fa fa-cog mr-1"></i>{{ 'AbpIdentity::Actions' | abpLocalization }} <i class="fa fa-cog mr-1"></i>{{ 'AbpIdentity::Actions' | abpLocalization }}
</button> </button>
<div ngbDropdownMenu> <div ngbDropdownMenu>
<button [abpPermission]="'AbpIdentity.Roles.Update'" ngbDropdownItem (click)="edit(data.id)"> <button *abpPermission="'AbpIdentity.Roles.Update'" ngbDropdownItem (click)="edit(data.id)">
{{ 'AbpIdentity::Edit' | abpLocalization }} {{ 'AbpIdentity::Edit' | abpLocalization }}
</button> </button>
<button <button
[abpPermission]="'AbpIdentity.Roles.ManagePermissions'" *abpPermission="'AbpIdentity.Roles.ManagePermissions'"
ngbDropdownItem ngbDropdownItem
(click)="providerKey = data.name; visiblePermissions = true" (click)="providerKey = data.name; visiblePermissions = true"
> >
{{ 'AbpIdentity::Permissions' | abpLocalization }} {{ 'AbpIdentity::Permissions' | abpLocalization }}
</button> </button>
<button <button
[abpPermission]="'AbpIdentity.Roles.Delete'" *abpPermission="'AbpIdentity.Roles.Delete'"
ngbDropdownItem ngbDropdownItem
(click)="delete(data.id, data.name)" (click)="delete(data.id, data.name)"
> >

8
npm/ng-packs/packages/identity/src/lib/components/users/users.component.html

@ -6,7 +6,7 @@
</div> </div>
<div class="text-right col col-md-6"> <div class="text-right col col-md-6">
<button <button
[abpPermission]="'AbpIdentity.Users.Create'" *abpPermission="'AbpIdentity.Users.Create'"
id="create-role" id="create-role"
class="btn btn-primary" class="btn btn-primary"
type="button" type="button"
@ -86,18 +86,18 @@
<i class="fa fa-cog mr-1"></i>{{ 'AbpIdentity::Actions' | abpLocalization }} <i class="fa fa-cog mr-1"></i>{{ 'AbpIdentity::Actions' | abpLocalization }}
</button> </button>
<div ngbDropdownMenu> <div ngbDropdownMenu>
<button [abpPermission]="'AbpIdentity.Users.Update'" ngbDropdownItem (click)="edit(data.id)"> <button *abpPermission="'AbpIdentity.Users.Update'" ngbDropdownItem (click)="edit(data.id)">
{{ 'AbpIdentity::Edit' | abpLocalization }} {{ 'AbpIdentity::Edit' | abpLocalization }}
</button> </button>
<button <button
[abpPermission]="'AbpIdentity.Users.ManagePermissions'" *abpPermission="'AbpIdentity.Users.ManagePermissions'"
ngbDropdownItem ngbDropdownItem
(click)="providerKey = data.id; visiblePermissions = true" (click)="providerKey = data.id; visiblePermissions = true"
> >
{{ 'AbpIdentity::Permissions' | abpLocalization }} {{ 'AbpIdentity::Permissions' | abpLocalization }}
</button> </button>
<button <button
[abpPermission]="'AbpIdentity.Users.Delete'" *abpPermission="'AbpIdentity.Users.Delete'"
ngbDropdownItem ngbDropdownItem
(click)="delete(data.id, data.userName)" (click)="delete(data.id, data.userName)"
> >

10
npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.html

@ -6,7 +6,7 @@
</div> </div>
<div class="text-right col col-md-6"> <div class="text-right col col-md-6">
<button <button
[abpPermission]="'AbpTenantManagement.Tenants.Create'" *abpPermission="'AbpTenantManagement.Tenants.Create'"
id="create-tenants" id="create-tenants"
class="btn btn-primary" class="btn btn-primary"
type="button" type="button"
@ -79,28 +79,28 @@
</button> </button>
<div ngbDropdownMenu> <div ngbDropdownMenu>
<button <button
[abpPermission]="'AbpTenantManagement.Tenants.Update'" *abpPermission="'AbpTenantManagement.Tenants.Update'"
ngbDropdownItem ngbDropdownItem
(click)="editTenant(data.id)" (click)="editTenant(data.id)"
> >
{{ 'AbpTenantManagement::Edit' | abpLocalization }} {{ 'AbpTenantManagement::Edit' | abpLocalization }}
</button> </button>
<button <button
[abpPermission]="'AbpTenantManagement.Tenants.ManageConnectionStrings'" *abpPermission="'AbpTenantManagement.Tenants.ManageConnectionStrings'"
ngbDropdownItem ngbDropdownItem
(click)="onEditConnectionString(data.id)" (click)="onEditConnectionString(data.id)"
> >
{{ 'AbpTenantManagement::Permission:ManageConnectionStrings' | abpLocalization }} {{ 'AbpTenantManagement::Permission:ManageConnectionStrings' | abpLocalization }}
</button> </button>
<button <button
[abpPermission]="'AbpTenantManagement.Tenants.ManageFeatures'" *abpPermission="'AbpTenantManagement.Tenants.ManageFeatures'"
ngbDropdownItem ngbDropdownItem
(click)="providerKey = data.id; visibleFeatures = true" (click)="providerKey = data.id; visibleFeatures = true"
> >
{{ 'AbpTenantManagement::Permission:ManageFeatures' | abpLocalization }} {{ 'AbpTenantManagement::Permission:ManageFeatures' | abpLocalization }}
</button> </button>
<button <button
[abpPermission]="'AbpTenantManagement.Tenants.Delete'" *abpPermission="'AbpTenantManagement.Tenants.Delete'"
ngbDropdownItem ngbDropdownItem
(click)="delete(data.id, data.name)" (click)="delete(data.id, data.name)"
> >

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

@ -77,9 +77,13 @@ export class TenantsComponent implements OnInit {
get isDisabledSaveButton(): boolean { get isDisabledSaveButton(): boolean {
if (!this.selectedModalContent) return false; if (!this.selectedModalContent) return false;
if (this.selectedModalContent.type === 'saveConnStr' && this.defaultConnectionStringForm.invalid) { if (
this.selectedModalContent.type === 'saveConnStr' &&
this.defaultConnectionStringForm &&
this.defaultConnectionStringForm.invalid
) {
return true; return true;
} else if (this.selectedModalContent.type === 'saveTenant' && this.tenantForm.invalid) { } else if (this.selectedModalContent.type === 'saveTenant' && this.tenantForm && this.tenantForm.invalid) {
return true; return true;
} else { } else {
return false; return false;

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

@ -32,7 +32,7 @@
</ng-container> </ng-container>
<ng-template #defaultLink let-route> <ng-template #defaultLink let-route>
<li class="nav-item" [abpPermission]="route.requiredPolicy"> <li class="nav-item" *abpPermission="route.requiredPolicy">
<a class="nav-link" [routerLink]="[route.url]" <a class="nav-link" [routerLink]="[route.url]"
><i *ngIf="route.iconClass" [ngClass]="route.iconClass"></i> {{ route.name | abpLocalization }}</a ><i *ngIf="route.iconClass" [ngClass]="route.iconClass"></i> {{ route.name | abpLocalization }}</a
> >
@ -42,7 +42,7 @@
<ng-template #dropdownLink let-route> <ng-template #dropdownLink let-route>
<li <li
#navbarRootDropdown #navbarRootDropdown
[abpPermission]="route.requiredPolicy" *abpPermission="route.requiredPolicy"
[abpVisibility]="routeContainer" [abpVisibility]="routeContainer"
class="nav-item dropdown" class="nav-item dropdown"
display="static" display="static"
@ -86,7 +86,7 @@
</ng-template> </ng-template>
<ng-template #defaultChild let-child> <ng-template #defaultChild let-child>
<div class="dropdown-submenu" [abpPermission]="child.requiredPolicy"> <div class="dropdown-submenu" *abpPermission="child.requiredPolicy">
<a class="dropdown-item" [routerLink]="[child.url]"> <a class="dropdown-item" [routerLink]="[child.url]">
<i *ngIf="child.iconClass" [ngClass]="child.iconClass"></i> <i *ngIf="child.iconClass" [ngClass]="child.iconClass"></i>
{{ child.name | abpLocalization }}</a {{ child.name | abpLocalization }}</a
@ -103,7 +103,7 @@
[display]="isDropdownChildDynamic ? 'dynamic' : 'static'" [display]="isDropdownChildDynamic ? 'dynamic' : 'static'"
placement="right-top" placement="right-top"
[autoClose]="true" [autoClose]="true"
[abpPermission]="child.requiredPolicy" *abpPermission="child.requiredPolicy"
(openChange)="openChange($event, childrenContainer)" (openChange)="openChange($event, childrenContainer)"
> >
<div ngbDropdownToggle [class.dropdown-toggle]="false"> <div ngbDropdownToggle [class.dropdown-toggle]="false">
@ -158,7 +158,7 @@
</ng-template> </ng-template>
<ng-template #language> <ng-template #language>
<li *ngIf="(dropdownLanguages$ | async)?.length > 1" class="nav-item"> <li *ngIf="(dropdownLanguages$ | async)?.length > 0" class="nav-item">
<div class="dropdown" ngbDropdown #languageDropdown="ngbDropdown" display="static"> <div class="dropdown" ngbDropdown #languageDropdown="ngbDropdown" display="static">
<a <a
ngbDropdownToggle ngbDropdownToggle
@ -213,10 +213,10 @@
[class.d-block]="smallScreen" [class.d-block]="smallScreen"
[class.abp-mh-25]="smallScreen && currentUserDropdown.isOpen()" [class.abp-mh-25]="smallScreen && currentUserDropdown.isOpen()"
> >
<a class="dropdown-item" routerLink="/account/manage-profile">{{ <a class="dropdown-item" routerLink="/account/manage-profile"><i class="fa fa-cog mr-1"></i>{{
'AbpAccount::ManageYourProfile' | abpLocalization 'AbpAccount::ManageYourProfile' | abpLocalization
}}</a> }}</a>
<a class="dropdown-item" href="javascript:void(0)" (click)="logout()">{{ <a class="dropdown-item" href="javascript:void(0)" (click)="logout()"><i class="fa fa-power-off mr-1"></i>{{
'AbpUi::Logout' | abpLocalization 'AbpUi::Logout' | abpLocalization
}}</a> }}</a>
</div> </div>

33
npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.ts

@ -16,21 +16,32 @@ export class BreadcrumbComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.show = !!this.store.selectSnapshot(state => state.LeptonLayoutState); this.show = !!this.store.selectSnapshot(state => state.LeptonLayoutState);
if (this.show) {
let splittedUrl = this.router.url.split('/').filter(chunk => chunk);
let currentUrl: ABP.FullRoute = this.store.selectSnapshot(ConfigState.getRoute(splittedUrl[0]));
if (!currentUrl) {
currentUrl = this.store.selectSnapshot(ConfigState.getRoute(null, null, this.router.url));
splittedUrl = [this.router.url];
if (!currentUrl) {
this.show = false;
return;
}
}
const splittedUrl = this.router.url.split('/').filter(chunk => chunk); this.segments.push(currentUrl.name);
const currentUrl: ABP.FullRoute = this.store.selectSnapshot(ConfigState.getRoute(splittedUrl[0]));
this.segments.push(currentUrl.name);
if (splittedUrl.length > 1) { if (splittedUrl.length > 1) {
const [, ...arr] = splittedUrl; const [, ...arr] = splittedUrl;
let childRoute: ABP.FullRoute = currentUrl; let childRoute: ABP.FullRoute = currentUrl;
for (let i = 0; i < arr.length; i++) { for (let i = 0; i < arr.length; i++) {
const element = arr[i]; const element = arr[i];
childRoute = childRoute.children.find(child => child.path === element); childRoute = childRoute.children.find(child => child.path === element);
this.segments.push(childRoute.name); this.segments.push(childRoute.name);
}
} }
} }
} }

2
npm/ng-packs/packages/theme-shared/src/lib/components/error/error.component.html → npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.html

@ -1,4 +1,4 @@
<div #container id="abp-error" class="error"> <div #container id="abp-http-error-container" class="error" [style.backgroundColor]="backgroundColor">
<button *ngIf="!hideCloseIcon" id="abp-close-button" type="button" class="close mr-2" (click)="destroy()"> <button *ngIf="!hideCloseIcon" id="abp-close-button" type="button" class="close mr-2" (click)="destroy()">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>

1
npm/ng-packs/packages/theme-shared/src/lib/components/error/error.component.scss → npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.scss

@ -1,7 +1,6 @@
.error { .error {
position: fixed; position: fixed;
top: 0; top: 0;
background-color: #fff;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
z-index: 999999; z-index: 999999;

21
npm/ng-packs/packages/theme-shared/src/lib/components/error/error.component.ts → npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.ts

@ -1,25 +1,27 @@
import { Config, takeUntilDestroy } from '@abp/ng.core'; import { Config, takeUntilDestroy } from '@abp/ng.core';
import { import {
AfterViewInit, AfterViewInit,
ApplicationRef,
Component, Component,
ComponentFactoryResolver, ComponentFactoryResolver,
ElementRef, ElementRef,
EmbeddedViewRef, EmbeddedViewRef,
Injector,
OnDestroy, OnDestroy,
OnInit,
Type, Type,
ViewChild, ViewChild,
ApplicationRef,
Injector,
} from '@angular/core'; } from '@angular/core';
import { fromEvent, Subject } from 'rxjs'; import { fromEvent, Subject } from 'rxjs';
import { debounceTime, filter } from 'rxjs/operators'; import { debounceTime, filter } from 'rxjs/operators';
import snq from 'snq';
@Component({ @Component({
selector: 'abp-error', selector: 'abp-http-error-wrapper',
templateUrl: './error.component.html', templateUrl: './http-error-wrapper.component.html',
styleUrls: ['error.component.scss'], styleUrls: ['http-error-wrapper.component.scss'],
}) })
export class ErrorComponent implements AfterViewInit, OnDestroy { export class HttpErrorWrapperComponent implements AfterViewInit, OnDestroy, OnInit {
appRef: ApplicationRef; appRef: ApplicationRef;
cfRes: ComponentFactoryResolver; cfRes: ComponentFactoryResolver;
@ -38,6 +40,8 @@ export class ErrorComponent implements AfterViewInit, OnDestroy {
hideCloseIcon = false; hideCloseIcon = false;
backgroundColor: string;
@ViewChild('container', { static: false }) @ViewChild('container', { static: false })
containerRef: ElementRef<HTMLDivElement>; containerRef: ElementRef<HTMLDivElement>;
@ -45,6 +49,11 @@ export class ErrorComponent implements AfterViewInit, OnDestroy {
return this.status ? `[${this.status}]` : ''; return this.status ? `[${this.status}]` : '';
} }
ngOnInit() {
this.backgroundColor =
snq(() => window.getComputedStyle(document.body).getPropertyValue('background-color')) || '#fff';
}
ngAfterViewInit() { ngAfterViewInit() {
if (this.customComponent) { if (this.customComponent) {
const customComponentRef = this.cfRes.resolveComponentFactory(this.customComponent).create(this.injector); const customComponentRef = this.cfRes.resolveComponentFactory(this.customComponent).create(this.injector);

58
npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.html

@ -1,30 +1,34 @@
<ng-container *ngIf="visible"> <div
<div id="modal-container" class="modal show {{ modalClass }}" tabindex="-1" role="dialog"> *ngIf="visible"
<div class="modal-backdrop" [@fade]="isModalOpen" (click)="close()"></div> [@fade]="isModalOpen"
<div id="modal-container"
id="abp-modal-dialog" class="modal show {{ modalClass }}"
class="modal-dialog modal-{{ size }}" tabindex="-1"
role="document" role="dialog"
[class.modal-dialog-centered]="centered" >
[@dialog]="isModalOpen" <div class="modal-backdrop" (click)="close()"></div>
#abpModalContent <div
> id="abp-modal-dialog"
<div id="abp-modal-content" class="modal-content"> class="modal-dialog modal-{{ size }}"
<div id="abp-modal-header" class="modal-header"> role="document"
<ng-container *ngTemplateOutlet="abpHeader"></ng-container> [class.modal-dialog-centered]="centered"
​ #abpModalContent
<button id="abp-modal-close-button" type="button" class="close" aria-label="Close" (click)="close()"> >
<span aria-hidden="true">&times;</span> <div id="abp-modal-content" class="modal-content">
</button> <div id="abp-modal-header" class="modal-header">
</div> <ng-container *ngTemplateOutlet="abpHeader"></ng-container>
<div id="abp-modal-body" class="modal-body"> ​
<ng-container *ngTemplateOutlet="abpBody"></ng-container> <button id="abp-modal-close-button" type="button" class="close" aria-label="Close" (click)="close()">
</div> <span aria-hidden="true">&times;</span>
<div id="abp-modal-footer" class="modal-footer"> </button>
<ng-container *ngTemplateOutlet="abpFooter"></ng-container> </div>
</div> <div id="abp-modal-body" class="modal-body">
<ng-container *ngTemplateOutlet="abpBody"></ng-container>
</div>
<div id="abp-modal-footer" class="modal-footer">
<ng-container *ngTemplateOutlet="abpFooter"></ng-container>
</div> </div>
</div> </div>
<ng-content></ng-content>
</div> </div>
</ng-container> <ng-content></ng-content>
</div>

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

@ -13,7 +13,7 @@ import {
} from '@angular/core'; } from '@angular/core';
import { fromEvent, Subject } from 'rxjs'; import { fromEvent, Subject } from 'rxjs';
import { debounceTime, filter, takeUntil } from 'rxjs/operators'; import { debounceTime, filter, takeUntil } from 'rxjs/operators';
import { dialogAnimation, fadeAnimation } from '../../animations/modal.animations'; import { fadeAnimation } from '../../animations/modal.animations';
import { Toaster } from '../../models/toaster'; import { Toaster } from '../../models/toaster';
import { ConfirmationService } from '../../services/confirmation.service'; import { ConfirmationService } from '../../services/confirmation.service';
import { ButtonComponent } from '../button/button.component'; import { ButtonComponent } from '../button/button.component';
@ -23,7 +23,7 @@ export type ModalSize = 'sm' | 'md' | 'lg' | 'xl';
@Component({ @Component({
selector: 'abp-modal', selector: 'abp-modal',
templateUrl: './modal.component.html', templateUrl: './modal.component.html',
animations: [fadeAnimation, dialogAnimation], animations: [fadeAnimation],
}) })
export class ModalComponent implements OnDestroy { export class ModalComponent implements OnDestroy {
@Input() @Input()

49
npm/ng-packs/packages/theme-shared/src/lib/components/sort-order-icon/sort-order-icon.component.ts

@ -5,29 +5,53 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
templateUrl: './sort-order-icon.component.html', templateUrl: './sort-order-icon.component.html',
}) })
export class SortOrderIconComponent { export class SortOrderIconComponent {
private _order: string; private _order: 'asc' | 'desc' | '';
private _selectedKey: string; private _selectedSortKey: string;
/**
* @deprecated use selectedSortKey instead.
*/
@Input() @Input()
set selectedKey(value: string) { set selectedKey(value: string) {
this._selectedKey = value; this.selectedSortKey = value;
this.selectedKeyChange.emit(value); this.selectedKeyChange.emit(value);
} }
get selectedKey(): string { get selectedKey(): string {
return this._selectedKey; return this._selectedSortKey;
}
@Input()
set selectedSortKey(value: string) {
this._selectedSortKey = value;
this.selectedSortKeyChange.emit(value);
}
get selectedSortKey(): string {
return this._selectedSortKey;
} }
@Output() readonly selectedKeyChange = new EventEmitter<string>(); @Output() readonly selectedKeyChange = new EventEmitter<string>();
@Output() readonly selectedSortKeyChange = new EventEmitter<string>();
/**
* @deprecated use sortKey instead.
*/
@Input()
get key(): string {
return this.sortKey;
}
set key(value: string) {
this.sortKey = value;
}
@Input() @Input()
key: string; sortKey: string;
@Input() @Input()
set order(value: string) { set order(value: 'asc' | 'desc' | '') {
this._order = value; this._order = value;
this.orderChange.emit(value); this.orderChange.emit(value);
} }
get order(): string { get order(): 'asc' | 'desc' | '' {
return this._order; return this._order;
} }
@ -37,16 +61,18 @@ export class SortOrderIconComponent {
iconClass: string; iconClass: string;
get icon(): string { get icon(): string {
if (!this.selectedKey) return 'fa-sort'; if (!this.selectedSortKey) return 'fa-sort';
if (this.selectedKey === this.key) return `fa-sort-${this.order}`; if (this.selectedSortKey === this.sortKey) return `fa-sort-${this.order}`;
else return ''; else return '';
} }
sort(key: string) { sort(key: string) {
this.selectedKey = key; this.selectedKey = key; // TODO: To be removed
this.selectedSortKey = key;
switch (this.order) { switch (this.order) {
case '': case '':
this.order = 'asc'; this.order = 'asc';
this.orderChange.emit('asc');
break; break;
case 'asc': case 'asc':
this.order = 'desc'; this.order = 'desc';
@ -54,7 +80,8 @@ export class SortOrderIconComponent {
break; break;
case 'desc': case 'desc':
this.order = ''; this.order = '';
this.selectedKey = ''; this.selectedKey = ''; // TODO: To be removed
this.orderChange.emit('');
break; break;
} }
} }

0
npm/ng-packs/packages/theme-shared/src/lib/contants/scripts.ts → npm/ng-packs/packages/theme-shared/src/lib/constants/scripts.ts

16
npm/ng-packs/packages/theme-shared/src/lib/contants/styles.ts → npm/ng-packs/packages/theme-shared/src/lib/constants/styles.ts

@ -45,15 +45,27 @@ export default `
} }
.modal-backdrop { .modal-backdrop {
position: absolute !important; position: fixed !important;
top: 0 !important; top: 0 !important;
left: 0 !important; left: 0 !important;
width: 100% !important; width: calc(100% - 7px) !important;
height: 100% !important; height: 100% !important;
background-color: rgba(0, 0, 0, 0.6) !important; background-color: rgba(0, 0, 0, 0.6) !important;
z-index: 1040 !important; z-index: 1040 !important;
} }
.modal::-webkit-scrollbar {
width: 7px;
}
.modal::-webkit-scrollbar-track {
background: #ddd;
}
.modal::-webkit-scrollbar-thumb {
background: #8a8686;
}
.modal-dialog { .modal-dialog {
z-index: 1050 !important; z-index: 1050 !important;
} }

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

@ -15,7 +15,7 @@ import { Navigate, RouterError, RouterState, RouterDataResolved } from '@ngxs/ro
import { Actions, ofActionSuccessful, Store } from '@ngxs/store'; import { Actions, ofActionSuccessful, Store } from '@ngxs/store';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import snq from 'snq'; import snq from 'snq';
import { ErrorComponent } from '../components/error/error.component'; import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component';
import { HttpErrorConfig, ErrorScreenErrorCodes } from '../models/common'; import { HttpErrorConfig, ErrorScreenErrorCodes } from '../models/common';
import { Toaster } from '../models/toaster'; import { Toaster } from '../models/toaster';
import { ConfirmationService } from '../services/confirmation.service'; import { ConfirmationService } from '../services/confirmation.service';
@ -45,7 +45,7 @@ export const DEFAULT_ERROR_MESSAGES = {
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class ErrorHandler { export class ErrorHandler {
componentRef: ComponentRef<ErrorComponent>; componentRef: ComponentRef<HttpErrorWrapperComponent>;
constructor( constructor(
private actions: Actions, private actions: Actions,
@ -196,11 +196,11 @@ export class ErrorHandler {
); );
} }
createErrorComponent(instance: Partial<ErrorComponent>) { createErrorComponent(instance: Partial<HttpErrorWrapperComponent>) {
const renderer = this.rendererFactory.createRenderer(null, null); const renderer = this.rendererFactory.createRenderer(null, null);
const host = renderer.selectRootElement(document.body, true); const host = renderer.selectRootElement(document.body, true);
this.componentRef = this.cfRes.resolveComponentFactory(ErrorComponent).create(this.injector); this.componentRef = this.cfRes.resolveComponentFactory(HttpErrorWrapperComponent).create(this.injector);
for (const key in this.componentRef.instance) { for (const key in this.componentRef.instance) {
if (this.componentRef.instance.hasOwnProperty(key)) { if (this.componentRef.instance.hasOwnProperty(key)) {

15
npm/ng-packs/packages/theme-shared/src/lib/tests/breadcrumb.component.spec.ts

@ -46,4 +46,19 @@ describe('BreadcrumbComponent', () => {
expect(elements[1]).toHaveText('Identity'); expect(elements[1]).toHaveText('Identity');
expect(elements[2]).toHaveText('Users'); expect(elements[2]).toHaveText('Users');
}); });
it('should not display the breadcrumb', () => {
const router = spectator.get(Router);
(router as any).url = '/identity/users';
spectator.detectChanges();
expect(spectator.query('ol.breadcrumb')).toBeFalsy();
});
it('should not display the breadcrumb when abp route not found', () => {
const router = spectator.get(Router);
(router as any).url = '/identity/users';
store.selectSnapshot.mockReturnValueOnce({ LeptonLayoutState: {} });
spectator.detectChanges();
expect(spectator.query('ol.breadcrumb')).toBeFalsy();
});
}); });

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

@ -22,7 +22,7 @@ describe('ConfirmationService', () => {
let service: ConfirmationService; let service: ConfirmationService;
const createComponent = createComponentFactory({ const createComponent = createComponentFactory({
component: DummyComponent, component: DummyComponent,
imports: [CoreModule, ThemeSharedModule, NgxsModule.forRoot(), RouterTestingModule], imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot(), RouterTestingModule],
providers: [MessageService], providers: [MessageService],
}); });

8
npm/ng-packs/packages/theme-shared/src/lib/tests/error.component.spec.ts

@ -1,14 +1,14 @@
import { SpectatorHost, createHostFactory } from '@ngneat/spectator/jest'; import { SpectatorHost, createHostFactory } from '@ngneat/spectator/jest';
import { ErrorComponent } from '../components/error/error.component'; import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component';
import { LocalizationPipe } from '@abp/ng.core'; import { LocalizationPipe } from '@abp/ng.core';
import { Store } from '@ngxs/store'; import { Store } from '@ngxs/store';
import { Renderer2, ElementRef } from '@angular/core'; import { Renderer2, ElementRef } from '@angular/core';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
describe('ErrorComponent', () => { describe('ErrorComponent', () => {
let spectator: SpectatorHost<ErrorComponent>; let spectator: SpectatorHost<HttpErrorWrapperComponent>;
const createHost = createHostFactory({ const createHost = createHostFactory({
component: ErrorComponent, component: HttpErrorWrapperComponent,
declarations: [LocalizationPipe], declarations: [LocalizationPipe],
mocks: [Store], mocks: [Store],
providers: [ providers: [
@ -18,7 +18,7 @@ describe('ErrorComponent', () => {
}); });
beforeEach(() => { beforeEach(() => {
spectator = createHost('<abp-error></abp-error>'); spectator = createHost('<abp-http-error-wrapper></abp-http-error-wrapper>');
spectator.component.destroy$ = new Subject(); spectator.component.destroy$ = new Subject();
}); });

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

@ -22,14 +22,17 @@ describe('ErrorHandler', () => {
component: DummyComponent, component: DummyComponent,
imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot([])], imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot([])],
stubsEnabled: false, stubsEnabled: false,
routes: [{ path: '', component: DummyComponent }, { path: 'account/login', component: RouterOutletComponent }], routes: [
{ path: '', component: DummyComponent },
{ path: 'account/login', component: RouterOutletComponent },
],
}); });
beforeEach(() => { beforeEach(() => {
spectator = createComponent(); spectator = createComponent();
store = spectator.get(Store); store = spectator.get(Store);
const abpError = document.querySelector('abp-error'); const abpError = document.querySelector('abp-http-error-wrapper');
if (abpError) document.body.removeChild(abpError); if (abpError) document.body.removeChild(abpError);
}); });
@ -133,14 +136,17 @@ describe('ErrorHandler with custom error component', () => {
ErrorModule, ErrorModule,
], ],
stubsEnabled: false, stubsEnabled: false,
routes: [{ path: '', component: DummyComponent }, { path: 'account/login', component: RouterOutletComponent }], routes: [
{ path: '', component: DummyComponent },
{ path: 'account/login', component: RouterOutletComponent },
],
}); });
beforeEach(() => { beforeEach(() => {
spectator = createComponent(); spectator = createComponent();
store = spectator.get(Store); store = spectator.get(Store);
const abpError = document.querySelector('abp-error'); const abpError = document.querySelector('abp-http-error-wrapper');
if (abpError) document.body.removeChild(abpError); if (abpError) document.body.removeChild(abpError);
}); });

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

@ -77,18 +77,21 @@ describe('ModalComponent', () => {
expect(disappearFn).toHaveBeenCalled(); expect(disappearFn).toHaveBeenCalled();
}); });
it('should open the confirmation popup and works correct', () => { it('should open the confirmation popup and works correct', done => {
spectator.click('#abp-modal-close-button'); setTimeout(() => {
expect(disappearFn).not.toHaveBeenCalled(); spectator.click('#abp-modal-close-button');
expect(disappearFn).not.toHaveBeenCalled();
expect(spectator.query('p-toast')).toBeTruthy(); expect(spectator.query('p-toast')).toBeTruthy();
spectator.click('button#cancel'); spectator.click('button#cancel');
expect(spectator.query('div.modal')).toBeTruthy(); expect(spectator.query('div.modal')).toBeTruthy();
spectator.click('#abp-modal-close-button'); spectator.click('#abp-modal-close-button');
spectator.click('button#confirm'); spectator.click('button#confirm');
expect(spectator.query('div.modal')).toBeFalsy(); expect(spectator.query('div.modal')).toBeFalsy();
expect(disappearFn).toHaveBeenCalled(); expect(disappearFn).toHaveBeenCalled();
done();
}, 100);
}); });
it('should close with the abpClose', done => { it('should close with the abpClose', done => {

46
npm/ng-packs/packages/theme-shared/src/lib/tests/sort-order-icon.component.spec.ts

@ -0,0 +1,46 @@
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
import { SortOrderIconComponent } from '../components/sort-order-icon/sort-order-icon.component';
describe('SortOrderIconComponent', () => {
let spectator: SpectatorHost<SortOrderIconComponent>;
let component: SortOrderIconComponent;
const createHost = createHostFactory(SortOrderIconComponent);
beforeEach(() => {
spectator = createHost(
'<abp-sort-order-icon key="testKey" [(selectedSortKey)]="selectedSortKey" [(order)]="order"></abp-sort-order-icon>',
{
hostProps: {
selectedSortKey: '',
order: '',
},
},
);
component = spectator.component;
});
test('should have correct icon class when selectedSortKey and sortKey are the same', () => {
const newKey = 'testKey';
component.sort(newKey);
expect(component.selectedSortKey).toBe(newKey);
expect(component.order).toBe('asc');
expect(component.icon).toBe('fa-sort-asc');
});
test("shouldn't have any icon class when sortKey and selectedSortKey are different", () => {
const newKey = 'otherKey';
component.sort(newKey);
expect(component.selectedSortKey).toBe(newKey);
expect(component.order).toBe('asc');
expect(component.icon).toBe('');
});
test('should change order correctly when sort function called', () => {
component.sort('testKey');
expect(component.order).toBe('asc');
component.sort('testKey');
expect(component.order).toBe('desc');
component.sort('testKey');
expect(component.order).toBe('');
});
});

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

@ -1,12 +1,11 @@
import { CoreModule } from '@abp/ng.core'; import { CoreModule } from '@abp/ng.core';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { NgxsModule } from '@ngxs/store'; import { NgxsModule } from '@ngxs/store';
import { MessageService } from 'primeng/components/common/messageservice';
import { ToasterService } from '../services/toaster.service'; import { ToasterService } from '../services/toaster.service';
import { ThemeSharedModule } from '../theme-shared.module'; import { ThemeSharedModule } from '../theme-shared.module';
import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { MessageService } from 'primeng/components/common/messageservice';
@Component({ @Component({
selector: 'abp-dummy', selector: 'abp-dummy',
@ -23,7 +22,7 @@ describe('ToasterService', () => {
let service: ToasterService; let service: ToasterService;
const createComponent = createComponentFactory({ const createComponent = createComponentFactory({
component: DummyComponent, component: DummyComponent,
imports: [CoreModule, ThemeSharedModule, NgxsModule.forRoot(), RouterTestingModule], imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot(), RouterTestingModule],
providers: [MessageService], providers: [MessageService],
}); });
@ -63,7 +62,10 @@ describe('ToasterService', () => {
}); });
it('should display multiple toasts', () => { it('should display multiple toasts', () => {
service.addAll([{ summary: 'summary1', detail: 'detail1' }, { summary: 'summary2', detail: 'detail2' }]); service.addAll([
{ summary: 'summary1', detail: 'detail1' },
{ summary: 'summary2', detail: 'detail2' },
]);
spectator.detectChanges(); spectator.detectChanges();
expect(spectator.queryAll('div.ui-toast-summary').map(node => node.textContent.trim())).toEqual([ expect(spectator.queryAll('div.ui-toast-summary').map(node => node.textContent.trim())).toEqual([
'summary1', 'summary1',

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

@ -8,13 +8,13 @@ import { BreadcrumbComponent } from './components/breadcrumb/breadcrumb.componen
import { ButtonComponent } from './components/button/button.component'; import { ButtonComponent } from './components/button/button.component';
import { ChartComponent } from './components/chart/chart.component'; import { ChartComponent } from './components/chart/chart.component';
import { ConfirmationComponent } from './components/confirmation/confirmation.component'; import { ConfirmationComponent } from './components/confirmation/confirmation.component';
import { ErrorComponent } from './components/error/error.component'; import { HttpErrorWrapperComponent } from './components/http-error-wrapper/http-error-wrapper.component';
import { LoaderBarComponent } from './components/loader-bar/loader-bar.component'; import { LoaderBarComponent } from './components/loader-bar/loader-bar.component';
import { ModalComponent } from './components/modal/modal.component'; import { ModalComponent } from './components/modal/modal.component';
import { SortOrderIconComponent } from './components/sort-order-icon/sort-order-icon.component'; import { SortOrderIconComponent } from './components/sort-order-icon/sort-order-icon.component';
import { TableEmptyMessageComponent } from './components/table-empty-message/table-empty-message.component'; import { TableEmptyMessageComponent } from './components/table-empty-message/table-empty-message.component';
import { ToastComponent } from './components/toast/toast.component'; import { ToastComponent } from './components/toast/toast.component';
import styles from './contants/styles'; import styles from './constants/styles';
import { TableSortDirective } from './directives/table-sort.directive'; import { TableSortDirective } from './directives/table-sort.directive';
import { ErrorHandler } from './handlers/error.handler'; import { ErrorHandler } from './handlers/error.handler';
import { chartJsLoaded$ } from './utils/widget-utils'; import { chartJsLoaded$ } from './utils/widget-utils';
@ -29,16 +29,7 @@ export function appendScript(injector: Injector) {
import('chart.js').then(() => chartJsLoaded$.next(true)); import('chart.js').then(() => chartJsLoaded$.next(true));
const lazyLoadService: LazyLoadService = injector.get(LazyLoadService); const lazyLoadService: LazyLoadService = injector.get(LazyLoadService);
return lazyLoadService.load(null, 'style', styles, 'head', 'afterbegin').toPromise();
return forkJoin(
lazyLoadService.load(
null,
'style',
styles,
'head',
'afterbegin',
) /* lazyLoadService.load(null, 'script', scripts) */,
).toPromise();
}; };
return fn; return fn;
@ -51,7 +42,7 @@ export function appendScript(injector: Injector) {
ButtonComponent, ButtonComponent,
ChartComponent, ChartComponent,
ConfirmationComponent, ConfirmationComponent,
ErrorComponent, HttpErrorWrapperComponent,
LoaderBarComponent, LoaderBarComponent,
ModalComponent, ModalComponent,
TableEmptyMessageComponent, TableEmptyMessageComponent,
@ -72,9 +63,11 @@ export function appendScript(injector: Injector) {
TableSortDirective, TableSortDirective,
], ],
providers: [DatePipe], providers: [DatePipe],
entryComponents: [ErrorComponent], entryComponents: [HttpErrorWrapperComponent],
}) })
export class ThemeSharedModule { export class ThemeSharedModule {
constructor(private errorHandler: ErrorHandler) {}
static forRoot(options = {} as RootParams): ModuleWithProviders { static forRoot(options = {} as RootParams): ModuleWithProviders {
return { return {
ngModule: ThemeSharedModule, ngModule: ThemeSharedModule,
@ -82,7 +75,7 @@ export class ThemeSharedModule {
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
multi: true, multi: true,
deps: [Injector, ErrorHandler], deps: [Injector],
useFactory: appendScript, useFactory: appendScript,
}, },
{ provide: MessageService, useClass: MessageService }, { provide: MessageService, useClass: MessageService },

2
samples/BasicConsoleApplication/AbpConsoleDemo/AbpConsoleDemo.csproj

@ -6,7 +6,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Volo.Abp.Autofac" Version="0.20.0" /> <ProjectReference Include="..\..\..\framework\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

6
samples/BookStore-Modular/modules/book-management/test/Acme.BookStore.BookManagement.TestBase/Acme.BookStore.BookManagement.TestBase.csproj

@ -14,9 +14,9 @@
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.extensibility.execution" Version="2.4.1" /> <PackageReference Include="xunit.extensibility.execution" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="Volo.Abp.Autofac" Version="0.21.0" /> <ProjectReference Include="..\..\..\..\..\..\framework\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" />
<PackageReference Include="Volo.Abp.Authorization" Version="0.21.0" /> <ProjectReference Include="..\..\..\..\..\..\framework\src\Volo.Abp.Authorization\Volo.Abp.Authorization.csproj" />
<PackageReference Include="Volo.Abp.TestBase" Version="0.21.0" /> <ProjectReference Include="..\..\..\..\..\..\framework\src\Volo.Abp.TestBase\Volo.Abp.TestBase.csproj" />
<ProjectReference Include="..\..\src\Acme.BookStore.BookManagement.Domain\Acme.BookStore.BookManagement.Domain.csproj" /> <ProjectReference Include="..\..\src\Acme.BookStore.BookManagement.Domain\Acme.BookStore.BookManagement.Domain.csproj" />
</ItemGroup> </ItemGroup>

2
samples/MicroserviceDemo/_run/01_AuthServer.ps1

@ -0,0 +1,2 @@
cd ../applications/AuthServer.Host
dotnet run

2
samples/MicroserviceDemo/_run/02_IdentityService.ps1

@ -0,0 +1,2 @@
cd ../microservices/IdentityService.Host
dotnet run

2
samples/MicroserviceDemo/_run/03_BloggingService.ps1

@ -0,0 +1,2 @@
cd ../microservices/BloggingService.Host
dotnet run

2
samples/MicroserviceDemo/_run/04_ProductService.ps1

@ -0,0 +1,2 @@
cd ../microservices/ProductService.Host
dotnet run

2
samples/MicroserviceDemo/_run/05_InternalGateway.ps1

@ -0,0 +1,2 @@
cd ../gateways/InternalGateway.Host
dotnet run

2
samples/MicroserviceDemo/_run/06_BackendAdminAppGateway.ps1

@ -0,0 +1,2 @@
cd ../gateways/BackendAdminAppGateway.Host
dotnet run

2
samples/MicroserviceDemo/_run/07_PublicWebSiteGateway.ps1

@ -0,0 +1,2 @@
cd ../gateways/PublicWebSiteGateway.Host
dotnet run

2
samples/MicroserviceDemo/_run/08_BackendAdminApp.ps1

@ -0,0 +1,2 @@
cd ../applications/BackendAdminApp.Host
dotnet run

2
samples/MicroserviceDemo/_run/09_PublicWebSite.ps1

@ -0,0 +1,2 @@
cd ../applications/PublicWebSite.Host
dotnet run

2
samples/MicroserviceDemo/_run/10_ConsoleClientDemo.ps1

@ -0,0 +1,2 @@
cd ../applications/ConsoleClientDemo
dotnet run

19
samples/MicroserviceDemo/_run/README.md

@ -0,0 +1,19 @@
# Run the Microservice Demo Solution
This folder contains PowerShell files to run the system:
* **__Run_All_Application.ps1**: Runs all the applications in one click.
* **__Run_Browser.ps1**: Opens all the UIs of the applications in Google Chrome browser.
Other files runs the individual applications.
> This files are only tested in Windows 10.
### Pre-Requirements
- [SQL Server](https://www.microsoft.com/en-us/sql-server/sql-server-downloads) 2015+ (can be [express edition](https://www.microsoft.com/en-us/sql-server/sql-server-editions-express))
- [Redis](https://redis.io/download) 5.0+
- [RabbitMQ](https://www.rabbitmq.com/install-windows.html) 3.7.11+
- [MongoDB](https://www.mongodb.com/download-center) 4.0+
- [ElasticSearch](https://www.elastic.co/downloads/elasticsearch) 6.6+
- [Kibana](https://www.elastic.co/downloads/kibana) 6.6+ (optional, recommended to see logs)

9
samples/MicroserviceDemo/_run/__Run_All_Application.ps1

@ -0,0 +1,9 @@
start powershell .\01_AuthServer.ps1
start powershell .\02_IdentityService.ps1
start powershell .\03_BloggingService.ps1
start powershell .\04_ProductService.ps1
start powershell .\05_InternalGateway.ps1
start powershell .\06_BackendAdminAppGateway.ps1
start powershell .\07_PublicWebSiteGateway.ps1
start powershell .\08_BackendAdminApp.ps1
start powershell .\09_PublicWebSite.ps1

1
samples/MicroserviceDemo/_run/__Run_Browser.ps1

@ -0,0 +1 @@
start chrome http://localhost:64999,http://localhost:63568,http://localhost:62157,http://localhost:60244,http://localhost:65129/swagger,http://localhost:65115/swagger,http://localhost:64897/swagger,http://localhost:51954,http://localhost:53435

2
samples/MicroserviceDemo/applications/AuthServer.Host/Properties/launchSettings.json

@ -18,7 +18,7 @@
"AuthServer.Host": { "AuthServer.Host": {
"commandName": "Project", "commandName": "Project",
"launchBrowser": true, "launchBrowser": true,
"applicationUrl": "http://localhost:5000", "applicationUrl": "http://localhost:64999",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }

2
samples/MicroserviceDemo/applications/BackendAdminApp.Host/Properties/launchSettings.json

@ -18,7 +18,7 @@
"BackendAdminApp.Host": { "BackendAdminApp.Host": {
"commandName": "Project", "commandName": "Project",
"launchBrowser": true, "launchBrowser": true,
"applicationUrl": "http://localhost:5000", "applicationUrl": "http://localhost:51954",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }

2
samples/MicroserviceDemo/applications/PublicWebSite.Host/Properties/launchSettings.json

@ -18,7 +18,7 @@
"PublicWebSite.Host": { "PublicWebSite.Host": {
"commandName": "Project", "commandName": "Project",
"launchBrowser": true, "launchBrowser": true,
"applicationUrl": "http://localhost:5000", "applicationUrl": "http://localhost:53435",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }

29
samples/MicroserviceDemo/gateways/BackendAdminAppGateway.Host/BackendAdminAppGatewayHostModule.cs

@ -1,4 +1,7 @@
using Microsoft.AspNetCore.Builder; using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Ocelot.DependencyInjection; using Ocelot.DependencyInjection;
@ -52,14 +55,6 @@ namespace BackendAdminAppGateway.Host
options.Authority = configuration["AuthServer:Authority"]; options.Authority = configuration["AuthServer:Authority"];
options.ApiName = configuration["AuthServer:ApiName"]; options.ApiName = configuration["AuthServer:ApiName"];
options.RequireHttpsMetadata = false; options.RequireHttpsMetadata = false;
//TODO: Should create an extension method for that (may require to create a new ABP package depending on the IdentityServer4.AccessTokenValidation)
//options.InboundJwtClaimTypeMap["sub"] = AbpClaimTypes.UserId;
//options.InboundJwtClaimTypeMap["role"] = AbpClaimTypes.Role;
//options.InboundJwtClaimTypeMap["email"] = AbpClaimTypes.Email;
//options.InboundJwtClaimTypeMap["email_verified"] = AbpClaimTypes.EmailVerified;
//options.InboundJwtClaimTypeMap["phone_number"] = AbpClaimTypes.PhoneNumber;
//options.InboundJwtClaimTypeMap["phone_number_verified"] = AbpClaimTypes.PhoneNumberVerified;
//options.InboundJwtClaimTypeMap["name"] = AbpClaimTypes.UserName;
}); });
context.Services.AddSwaggerGen(options => context.Services.AddSwaggerGen(options =>
@ -94,6 +89,22 @@ namespace BackendAdminAppGateway.Host
app.UseVirtualFiles(); app.UseVirtualFiles();
app.UseRouting(); app.UseRouting();
app.UseAuthentication(); app.UseAuthentication();
app.Use(async (ctx, next) =>
{
var currentPrincipalAccessor = ctx.RequestServices.GetRequiredService<ICurrentPrincipalAccessor>();
var map = new Dictionary<string, string>()
{
{ "sub", AbpClaimTypes.UserId },
{ "role", AbpClaimTypes.Role },
{ "email", AbpClaimTypes.Email },
//any other map
};
var mapClaims = currentPrincipalAccessor.Principal.Claims.Where(p => map.Keys.Contains(p.Type)).ToList();
currentPrincipalAccessor.Principal.AddIdentity(new ClaimsIdentity(mapClaims.Select(p => new Claim(map[p.Type], p.Value, p.ValueType, p.Issuer))));
await next();
});
app.UseSwagger(); app.UseSwagger();
app.UseSwaggerUI(options => app.UseSwaggerUI(options =>
{ {

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

Loading…
Cancel
Save