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/gateways/PublicWebSiteGateway.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

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.",
"SourceCodeUpper": "CÓDIGO FONTE",
"LatestReleaseLogs": "Últimos registros de lançamento",
"Infrastructure": "Infra-estrutura",
"Infrastructure": "Infraestrutura",
"Architecture": "Arquitetura",
"Modular": "Modular",
"DontRepeatYourself": "Não se repita",
"DontRepeatYourself": "Não repita a si mesmo",
"DeveloperFocused": "Desenvolvedor focado",
"FullStackApplicationInfrastructure": "Infraestrutura de aplicativos full stack.",
"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.",
"MultiTenancy": "Múltiplos inquilinos",
"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.",
"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!",

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

@ -114,7 +114,7 @@ public class MyWebModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<BundlingOptions>(options =>
Configure<AbpBundlingOptions>(options =>
{
options
.ScriptBundles
@ -151,7 +151,7 @@ public class MyWebExtensionModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<BundlingOptions>(options =>
Configure<AbpBundlingOptions>(options =>
{
options
.ScriptBundles
@ -189,7 +189,7 @@ public class MyExtensionGlobalStyleContributor : BundleContributor
Then you can use this contributor as like below:
````C#
services.Configure<BundlingOptions>(options =>
services.Configure<AbpBundlingOptions>(options =>
{
options
.ScriptBundles
@ -317,7 +317,7 @@ In some specific cases, it may be needed to create a **new** bundle **inherited*
Example:
````c#
services.Configure<BundlingOptions>(options =>
services.Configure<AbpBundlingOptions>(options =>
{
options
.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:
* **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 depends on the **Domain.Share** package.
* Domain package depends on the **Domain.Shared** package.
#### Application Layer
@ -85,5 +85,5 @@ Next section describes the packages in more details.
#### 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.

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
"__tenant" parameter name can be changed using AspNetCoreMultiTenancyOptions. Example:
"__tenant" parameter name can be changed using AbpAspNetCoreMultiTenancyOptions. Example:
````C#
services.Configure<AspNetCoreMultiTenancyOptions>(options =>
services.Configure<AbpAspNetCoreMultiTenancyOptions>(options =>
{
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.
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
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.
#### 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)
#### Saving the Book

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

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

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#underscores_in_headers
可以使用AspNetCoreMultiTenancyOptions修改默认的参数名"__tenant".例如:
可以使用AbpAspNetCoreMultiTenancyOptions修改默认的参数名"__tenant".例如:
````C#
services.Configure<AspNetCoreMultiTenancyOptions>(options =>
services.Configure<AbpAspNetCoreMultiTenancyOptions>(options =>
{
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
<div class="dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@Model.CurrentLanguage.DisplayName
</a>
@if (Model.OtherLanguages.Any())
{
<div class="dropdown">
<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">
@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>
}
</div>
</div>
<div class="dropdown-menu dropdown-menu-right border-0 shadow-sm" aria-labelledby="dropdownMenuLink">
@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>
}
</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
<div class="dropdown">
<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>
@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">
<abp-row>
<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-body px-5">
<div class="row">
<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">
@if (CurrentTenant.Id == null)
{
@ -72,15 +75,12 @@
</h6>
</div>
<div class="col-auto">
@if (MultiTenancyOptions.Value.IsEnabled &&
(TenantResolveResultAccessor.Result?.AppliedResolvers?.Contains(CookieTenantResolveContributor.ContributorName) == true))
{
<a id="AbpTenantSwitchLink" href="javascript:;" class="btn btn-sm mt-3 btn-outline-primary">@MultiTenancyStringLocalizer["Switch"]</a>
}
<a id="AbpTenantSwitchLink" href="javascript:;" class="btn btn-sm mt-3 btn-outline-primary">@MultiTenancyStringLocalizer["Switch"]</a>
</div>
</div>
</div>
</div>
</div>
}
@(await Component.InvokeAsync<PageAlertsViewComponent>())
@RenderBody()
</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.ExceptionHandling;
using Volo.Abp.AspNetCore.Mvc.Features;
using Volo.Abp.AspNetCore.Mvc.ModelBinding;
using Volo.Abp.AspNetCore.Mvc.Uow;
using Volo.Abp.AspNetCore.Mvc.Validation;
@ -35,7 +36,7 @@ namespace Volo.Abp.AspNetCore.Mvc
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)

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 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);
if (updateNuget || both)
{
await UpdateNugetPackages(commandLineArgs);
await UpdateNugetPackages(commandLineArgs, directory);
}
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 =
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)
{
@ -131,6 +137,12 @@ namespace Volo.Abp.Cli.Commands
public static class Options
{
public static class SolutionPath
{
public const string Short = "sp";
public const string Long = "solution-path";
}
public static class IncludePreviews
{
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 bool? IncludeDefaults { get; set; }
public bool IncludeDefaults { get; set; }
public bool? IncludeSelf { get; set; }
public bool IncludeSelf { get; set; }
public ExposeServicesAttribute(params Type[] serviceTypes)
{
@ -22,19 +22,19 @@ namespace Volo.Abp.DependencyInjection
{
var serviceList = ServiceTypes.ToList();
if (IncludeDefaults == true)
if (IncludeDefaults)
{
foreach (var type in GetDefaultServices(targetType))
{
serviceList.AddIfNotContains(type);
}
if (IncludeSelf != false)
if (IncludeSelf)
{
serviceList.AddIfNotContains(targetType);
}
}
else if (IncludeSelf == true)
else if (IncludeSelf)
{
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 =
new ExposeServicesAttribute
{
IncludeDefaults = true
IncludeDefaults = true,
IncludeSelf = true
};
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.ComponentModel.DataAnnotations.Schema;
using System.Linq;
@ -18,10 +18,12 @@ using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Entities.Events;
using Volo.Abp.EntityFrameworkCore.EntityHistory;
using Volo.Abp.EntityFrameworkCore.ValueConverters;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Reflection;
using Volo.Abp.Threading;
using Volo.Abp.Timing;
namespace Volo.Abp.EntityFrameworkCore
{
@ -48,6 +50,8 @@ namespace Volo.Abp.EntityFrameworkCore
public IAuditingManager AuditingManager { get; set; }
public IClock Clock { get; set; }
public ILogger<AbpDbContext<TDbContext>> Logger { get; set; }
private static readonly MethodInfo ConfigureBasePropertiesMethodInfo
@ -57,6 +61,13 @@ namespace Volo.Abp.EntityFrameworkCore
BindingFlags.Instance | BindingFlags.NonPublic
);
private static readonly MethodInfo ConfigureValueConverterMethodInfo
= typeof(AbpDbContext<TDbContext>)
.GetMethod(
nameof(ConfigureValueConverter),
BindingFlags.Instance | BindingFlags.NonPublic
);
protected AbpDbContext(DbContextOptions<TDbContext> options)
: base(options)
{
@ -75,6 +86,10 @@ namespace Volo.Abp.EntityFrameworkCore
ConfigureBasePropertiesMethodInfo
.MakeGenericMethod(entityType.ClrType)
.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)
where TEntity : class
{
if (mutableEntityType.IsOwned())
{
return;
}
ConfigureConcurrencyStampProperty<TEntity>(modelBuilder, mutableEntityType);
ConfigureExtraProperties<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
{
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>
<ItemGroup>
<PackageReference Include="MailKit" Version="2.3.2" />
<PackageReference Include="MailKit" Version="2.4.1" />
</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.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
@ -10,6 +11,7 @@ using Volo.Abp.Modularity;
using Volo.Abp.TestApp;
using Volo.Abp.TestApp.Domain;
using Volo.Abp.TestApp.EntityFrameworkCore;
using Volo.Abp.Timing;
namespace Volo.Abp.EntityFrameworkCore
{
@ -41,6 +43,8 @@ namespace Volo.Abp.EntityFrameworkCore
abpDbContextConfigurationContext.DbContextOptions.UseSqlite(sqliteConnection);
});
});
Configure<AbpClockOptions>(options => options.Kind = DateTimeKind.Utc);
}
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))
{
context.GetService<IRelationalDatabaseCreator>().CreateTables();
context.Database.ExecuteSqlRaw(
@"CREATE VIEW View_PersonView AS
SELECT Name, CreationTime, Birthday, LastActive FROM People");
}
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)
{
modelBuilder.Owned<District>();
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Phone>(b =>
{
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<PersonView> PersonView { get; set; }
public DbSet<ThirdDbContextDummyEntity> DummyEntities { get; set; }
public DbSet<EntityWithIntPk> EntityWithIntPks { get; set; }
@ -23,12 +25,30 @@ namespace Volo.Abp.TestApp.EntityFrameworkCore
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Owned<District>();
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Phone>(b =>
{
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.Collections.Generic;
using System.Linq;
using Volo.Abp.Domain.Entities;
namespace Volo.Abp.TestApp.Domain
@ -7,6 +9,8 @@ namespace Volo.Abp.TestApp.Domain
{
public string Name { get; set; }
public ICollection<District> Districts { get; set; }
private City()
{
@ -16,6 +20,12 @@ namespace Volo.Abp.TestApp.Domain
: base(id)
{
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 Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Timing;
namespace Volo.Abp.TestApp.Domain
{
@ -15,6 +16,11 @@ namespace Volo.Abp.TestApp.Domain
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; }
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()
{
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(), "Madrid"));
_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(), "Washington"));
_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.Models;
using IdentityServer4.Services;
@ -12,8 +12,10 @@ using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using Volo.Abp.Account.Web.Settings;
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Settings;
using Volo.Abp.Uow;
namespace Volo.Abp.Account.Web.Pages.Account
@ -40,7 +42,7 @@ namespace Volo.Abp.Account.Web.Pages.Account
IdentityServerEvents = identityServerEvents;
}
public override async Task OnGetAsync()
public override async Task<IActionResult> OnGetAsync()
{
LoginInput = new LoginInputModel();
@ -63,7 +65,7 @@ namespace Volo.Abp.Account.Web.Pages.Account
{
LoginInput.UserNameOrEmailAddress = context.LoginHint;
ExternalProviders = new[] { new ExternalProviderModel { AuthenticationScheme = context.IdP } };
return;
return Page();
}
var schemes = await _schemeProvider.GetAllSchemesAsync();
@ -77,7 +79,7 @@ namespace Volo.Abp.Account.Web.Pages.Account
})
.ToList();
EnableLocalLogin = true; //TODO: We can get default from a setting?
EnableLocalLogin = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin);
if (context?.ClientId != null)
{
var client = await ClientStore.FindEnabledClientByIdAsync(context.ClientId);
@ -96,15 +98,16 @@ namespace Volo.Abp.Account.Web.Pages.Account
if (IsExternalLoginOnly)
{
//return await ExternalLogin(vm.ExternalLoginScheme, returnUrl);
throw new NotImplementedException();
return await base.OnPostExternalLogin(providers.First().AuthenticationScheme);
}
return Page();
}
[UnitOfWork] //TODO: Will be removed when we implement action filter
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")
{

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

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

@ -9,6 +9,10 @@ namespace Volo.Abp.Account.Web.Settings
context.Add(
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 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; }
/// <summary>
/// Gets or sets the Surame for the user.
/// Gets or sets the Surname for the user.
/// </summary>
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({
order: [[1, "asc"]],
processing: true,
paging: true,
serverSide: true,
ajax: abp.libs.datatables.createAjax(_tenantAppService.getList),
columnDefs: [
{

3
npm/ng-packs/.prettierrc

@ -1,6 +1,5 @@
{
"printWidth": 120,
"printWidth": 100,
"singleQuote": true,
"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 { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@ -11,6 +11,7 @@ import snq from 'snq';
import { RegisterRequest } from '../../models';
import { AccountService } from '../../services/account.service';
import { PasswordRules, validatePassword } from '@ngx-validate/core';
import { HttpHeaders } from '@angular/common/http';
const { maxLength, minLength, required, email } = Validators;
@Component({
@ -29,7 +30,9 @@ export class RegisterComponent implements OnInit {
private store: Store,
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();
}
@ -62,7 +65,10 @@ export class RegisterComponent implements OnInit {
this.form = this.fb.group({
username: ['', [required, maxLength(255)]],
password: ['', [required, validatePassword(passwordRulesArr), minLength(requiredLength), maxLength(32)]],
password: [
'',
[required, validatePassword(passwordRulesArr), minLength(requiredLength), maxLength(32)],
],
email: ['', [required, email]],
});
}
@ -79,10 +85,22 @@ export class RegisterComponent implements OnInit {
appName: 'Angular',
} as RegisterRequest;
const tenant = this.store.selectSnapshot(SessionState.getTenant);
this.accountService
.register(newUser)
.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())),
tap(() => this.store.dispatch(new Navigate(['/']))),
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 { SessionState } from './states/session.state';
import { getInitialData, localeInitializer } from './utils/initial-utils';
import './utils/date-extensions';
@NgModule({
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 { ConfigState } from '../states';
import { takeUntilDestroy } from '../utils';
import { Subscription } from 'rxjs';
@Directive({
selector: '[abpPermission]',
})
export class PermissionDirective implements OnInit, OnDestroy {
export class PermissionDirective implements OnInit, OnDestroy, OnChanges {
@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() {
if (this.condition) {
this.store
.select(ConfigState.getGrantedPolicy(this.condition))
.pipe(takeUntilDestroy(this))
.subscribe(isGranted => {
if (!isGranted) {
this.renderer.removeChild(
(this.elRef.nativeElement as HTMLElement).parentElement,
this.elRef.nativeElement,
);
}
});
if (this.templateRef && !this.condition) {
this.vcRef.createEmbeddedView(this.templateRef);
}
}
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 { ActivatedRouteSnapshot, CanActivate } from '@angular/router';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { ConfigState } from '../states';
import { tap } from 'rxjs/operators';
import snq from 'snq';
import { RestOccurError } from '../actions';
import { ConfigState } from '../states';
@Injectable({
providedIn: 'root',
@ -12,8 +13,14 @@ import { RestOccurError } from '../actions';
export class PermissionGuard implements CanActivate {
constructor(private store: Store) {}
canActivate({ data }: ActivatedRouteSnapshot): Observable<boolean> {
const resource = data.requiredPolicy as string;
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
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(
tap(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',
})
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);
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();
stringArray = value.filter(item => typeof item === 'string').sort();
} 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
.filter(item => typeof item[sortKey] === 'string')
.sort((a, b) => {
@ -26,7 +32,15 @@ export class SortPipe implements PipeTransform {
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();
}
}

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

@ -23,17 +23,31 @@ export class RestService {
config = config || ({} as Rest.Config);
const { observe = Rest.Observe.Body, skipHandleError } = config;
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(
observe === Rest.Observe.Body ? take(1) : tap(),
catchError(err => {
if (skipHandleError) {
return throwError(err);
}
return this.http
.request<T>(method, url, {
observe,
...(params && {
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 { switchMap, tap } from 'rxjs/operators';
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 { ABP } from '../models/common';
import { Config } from '../models/config';
@ -26,12 +36,9 @@ export class ConfigState {
}
static getOne(key: string) {
const selector = createSelector(
[ConfigState],
(state: Config.State) => {
return state[key];
},
);
const selector = createSelector([ConfigState], (state: Config.State) => {
return state[key];
});
return selector;
}
@ -45,36 +52,32 @@ export class ConfigState {
throw new Error('The argument must be a dot string or an string array.');
}
const selector = createSelector(
[ConfigState],
(state: Config.State) => {
return (keys as string[]).reduce((acc, val) => {
if (acc) {
return acc[val];
}
return undefined;
}, state);
},
);
const selector = createSelector([ConfigState], (state: Config.State) => {
return (keys as string[]).reduce((acc, val) => {
if (acc) {
return acc[val];
}
return undefined;
}, state);
});
return selector;
}
static getRoute(path?: string, name?: string) {
const selector = createSelector(
[ConfigState],
(state: Config.State) => {
const { flattedRoutes } = state;
return (flattedRoutes as ABP.FullRoute[]).find(route => {
if (path && route.path === path) {
return route;
} else if (name && route.name === name) {
return route;
}
});
},
);
static getRoute(path?: string, name?: string, url?: string) {
const selector = createSelector([ConfigState], (state: Config.State) => {
const { flattedRoutes } = state;
return (flattedRoutes as ABP.FullRoute[]).find(route => {
if (path && route.path === path) {
return route;
} else if (name && route.name === name) {
return route;
} else if (url && route.url === url) {
return route;
}
});
});
return selector;
}
@ -91,30 +94,33 @@ export class ConfigState {
}
static getSetting(key: string) {
const selector = createSelector(
[ConfigState],
(state: Config.State) => {
return snq(() => state.setting.values[key]);
},
);
const selector = createSelector([ConfigState], (state: Config.State) => {
return snq(() => state.setting.values[key]);
});
return selector;
}
static getSettings(keyword?: string) {
const selector = createSelector(
[ConfigState],
(state: Config.State) => {
if (keyword) {
const keys = snq(() => 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] }), {});
}
const selector = createSelector([ConfigState], (state: Config.State) => {
if (keyword) {
const keys = snq(
() =>
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] }),
{},
);
}
}
return snq(() => state.setting.values, {});
},
);
return snq(() => state.setting.values, {});
});
return selector;
}
@ -123,14 +129,36 @@ export class ConfigState {
[ConfigState],
(state: Config.State): boolean => {
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;
}
static getLocalization(key: string | Config.LocalizationWithDefault, ...interpolateParams: string[]) {
static getLocalization(
key: string | Config.LocalizationWithDefault,
...interpolateParams: string[]
) {
let defaultValue: string;
if (typeof key !== 'string') {
@ -141,52 +169,52 @@ export class ConfigState {
if (!key) key = '';
const keys = key.split('::') as string[];
const selector = createSelector(
[ConfigState],
(state: Config.State) => {
if (!state.localization) return defaultValue || key;
const { defaultResourceName } = state.environment.localization;
if (keys[0] === '') {
if (!defaultResourceName) {
throw new Error(
`Please check your environment. May you forget set defaultResourceName?
const selector = createSelector([ConfigState], (state: Config.State) => {
if (!state.localization) return defaultValue || key;
const { defaultResourceName } = state.environment.localization;
if (keys[0] === '') {
if (!defaultResourceName) {
throw new Error(
`Please check your environment. May you forget set defaultResourceName?
Here is the example:
{ production: false,
localization: {
defaultResourceName: 'MyProjectName'
}
}`,
);
}
keys[0] = snq(() => defaultResourceName);
);
}
let localization = (keys as any).reduce((acc, val) => {
if (acc) {
return acc[val];
}
return undefined;
}, state.localization.values);
keys[0] = snq(() => defaultResourceName);
}
interpolateParams = interpolateParams.filter(params => params != null);
if (localization && interpolateParams && interpolateParams.length) {
interpolateParams.forEach(param => {
localization = localization.replace(/[\'\"]?\{[\d]+\}[\'\"]?/, param);
});
let localization = (keys as any).reduce((acc, val) => {
if (acc) {
return acc[val];
}
if (typeof localization !== 'string') localization = '';
return localization || defaultValue || key;
},
);
return undefined;
}, 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;
}
constructor(private appConfigurationService: ApplicationConfigurationService, private store: Store) {}
constructor(
private appConfigurationService: ApplicationConfigurationService,
private store: Store,
) {}
@Action(GetAppConfiguration)
addData({ patchState, dispatch }: StateContext<Config.State>) {
@ -197,19 +225,25 @@ export class ConfigState {
}),
),
switchMap(configuration => {
let defaultLang: string = configuration.setting.values['Abp.Localization.DefaultLanguage'];
let defaultLang: string =
configuration.setting.values['Abp.Localization.DefaultLanguage'];
if (defaultLang.includes(';')) {
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)
patchRoute({ patchState, getState }: StateContext<Config.State>, { name, newValue }: PatchRouteByName) {
patchRoute(
{ patchState, getState }: StateContext<Config.State>,
{ name, newValue }: PatchRouteByName,
) {
let routes: ABP.FullRoute[] = getState().routes;
const index = routes.findIndex(route => route.name === name);
@ -230,7 +264,9 @@ function patchRouteDeep(
): ABP.FullRoute[] {
routes = routes.map(route => {
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) {
newValue.children = newValue.children.map(child => ({
@ -241,7 +277,12 @@ function patchRouteDeep(
return { ...route, ...newValue };
} 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;

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 { ReplaySubject, timer, Subject, of } from 'rxjs';
import { Config } from '../models/config';
import { ApplicationConfigurationService, ConfigStateService } from '../services';
import {
ApplicationConfigurationService,
ConfigStateService,
} from '../services';
import { ConfigState } from '../states';
import { SetLanguage, PatchRouteByName } from '../actions';
@ -91,6 +98,7 @@ export const CONFIG_STATE_DATA = {
},
grantedPolicies: {
'Abp.Identity': false,
'Abp.Account': true,
},
},
setting: {
@ -126,7 +134,10 @@ describe('ConfigState', () => {
store = spectator.get(Store);
service = spectator.service;
appConfigService = spectator.get(ApplicationConfigurationService);
state = new ConfigState(spectator.get(ApplicationConfigurationService), store);
state = new ConfigState(
spectator.get(ApplicationConfigurationService),
store,
);
});
describe('#getAll', () => {
@ -137,24 +148,34 @@ describe('ConfigState', () => {
describe('#getApplicationInfo', () => {
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', () => {
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', () => {
it('should return deeper', () => {
expect(ConfigState.getDeep('environment.localization.defaultResourceName')(CONFIG_STATE_DATA)).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('environment.localization.defaultResourceName')(
CONFIG_STATE_DATA,
),
).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();
});
@ -162,21 +183,33 @@ describe('ConfigState', () => {
describe('#getRoute', () => {
it('should return route', () => {
expect(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]);
expect(
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', () => {
it('should return api url', () => {
expect(ConfigState.getApiUrl('other')(CONFIG_STATE_DATA)).toEqual(CONFIG_STATE_DATA.environment.apis.other.url);
expect(ConfigState.getApiUrl()(CONFIG_STATE_DATA)).toEqual(CONFIG_STATE_DATA.environment.apis.default.url);
expect(ConfigState.getApiUrl('other')(CONFIG_STATE_DATA)).toEqual(
CONFIG_STATE_DATA.environment.apis.other.url,
);
expect(ConfigState.getApiUrl()(CONFIG_STATE_DATA)).toEqual(
CONFIG_STATE_DATA.environment.apis.default.url,
);
});
});
describe('#getSetting', () => {
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'],
);
});
@ -184,41 +217,82 @@ describe('ConfigState', () => {
describe('#getSettings', () => {
it('should return settings', () => {
expect(ConfigState.getSettings('Localization')(CONFIG_STATE_DATA)).toEqual({
expect(
ConfigState.getSettings('Localization')(CONFIG_STATE_DATA),
).toEqual({
'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', () => {
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);
});
});
describe('#getLocalization', () => {
it('should return a localization', () => {
expect(ConfigState.getLocalization('AbpIdentity::Identity')(CONFIG_STATE_DATA)).toBe('identity');
expect(ConfigState.getLocalization('AbpIdentity::NoIdentity')(CONFIG_STATE_DATA)).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.',
);
expect(
ConfigState.getLocalization('AbpIdentity::Identity')(CONFIG_STATE_DATA),
).toBe('identity');
expect(
ConfigState.getLocalization('AbpIdentity::NoIdentity')(
CONFIG_STATE_DATA,
),
).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 {
ConfigState.getLocalization('::Test')({
...CONFIG_STATE_DATA,
environment: { ...CONFIG_STATE_DATA.environment, localization: {} as any },
environment: {
...CONFIG_STATE_DATA.environment,
localization: {} as any,
},
});
expect(false).toBeTruthy(); // fail
} 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',
path: '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);
});
});
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 => {
store.select.andReturn(of(true));
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(spy.mock.calls).toHaveLength(0);
done();
@ -33,11 +33,25 @@ describe('PermissionGuard', () => {
it('should return false and dispatch RestOccurError when the grantedPolicy is false', done => {
store.select.andReturn(of(false));
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(spy.mock.calls[0][0] instanceof RestOccurError).toBeTruthy();
expect((spy.mock.calls[0][0] as RestOccurError).payload).toEqual({ status: 403 });
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', () => {
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, 'desc', 'key')).toEqual([{ key: 'b' }, { key: 'a' }, { key: 5 }, { key: 1 }]);
expect(pipe.transform(array, 'asc', 'key')).toEqual([
{ 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', () => {

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 class="text-right col col-md-6">
<button
[abpPermission]="'AbpIdentity.Roles.Create'"
*abpPermission="'AbpIdentity.Roles.Create'"
id="create-role"
class="btn btn-primary"
type="button"
@ -73,18 +73,18 @@
<i class="fa fa-cog mr-1"></i>{{ 'AbpIdentity::Actions' | abpLocalization }}
</button>
<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 }}
</button>
<button
[abpPermission]="'AbpIdentity.Roles.ManagePermissions'"
*abpPermission="'AbpIdentity.Roles.ManagePermissions'"
ngbDropdownItem
(click)="providerKey = data.name; visiblePermissions = true"
>
{{ 'AbpIdentity::Permissions' | abpLocalization }}
</button>
<button
[abpPermission]="'AbpIdentity.Roles.Delete'"
*abpPermission="'AbpIdentity.Roles.Delete'"
ngbDropdownItem
(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 class="text-right col col-md-6">
<button
[abpPermission]="'AbpIdentity.Users.Create'"
*abpPermission="'AbpIdentity.Users.Create'"
id="create-role"
class="btn btn-primary"
type="button"
@ -86,18 +86,18 @@
<i class="fa fa-cog mr-1"></i>{{ 'AbpIdentity::Actions' | abpLocalization }}
</button>
<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 }}
</button>
<button
[abpPermission]="'AbpIdentity.Users.ManagePermissions'"
*abpPermission="'AbpIdentity.Users.ManagePermissions'"
ngbDropdownItem
(click)="providerKey = data.id; visiblePermissions = true"
>
{{ 'AbpIdentity::Permissions' | abpLocalization }}
</button>
<button
[abpPermission]="'AbpIdentity.Users.Delete'"
*abpPermission="'AbpIdentity.Users.Delete'"
ngbDropdownItem
(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 class="text-right col col-md-6">
<button
[abpPermission]="'AbpTenantManagement.Tenants.Create'"
*abpPermission="'AbpTenantManagement.Tenants.Create'"
id="create-tenants"
class="btn btn-primary"
type="button"
@ -79,28 +79,28 @@
</button>
<div ngbDropdownMenu>
<button
[abpPermission]="'AbpTenantManagement.Tenants.Update'"
*abpPermission="'AbpTenantManagement.Tenants.Update'"
ngbDropdownItem
(click)="editTenant(data.id)"
>
{{ 'AbpTenantManagement::Edit' | abpLocalization }}
</button>
<button
[abpPermission]="'AbpTenantManagement.Tenants.ManageConnectionStrings'"
*abpPermission="'AbpTenantManagement.Tenants.ManageConnectionStrings'"
ngbDropdownItem
(click)="onEditConnectionString(data.id)"
>
{{ 'AbpTenantManagement::Permission:ManageConnectionStrings' | abpLocalization }}
</button>
<button
[abpPermission]="'AbpTenantManagement.Tenants.ManageFeatures'"
*abpPermission="'AbpTenantManagement.Tenants.ManageFeatures'"
ngbDropdownItem
(click)="providerKey = data.id; visibleFeatures = true"
>
{{ 'AbpTenantManagement::Permission:ManageFeatures' | abpLocalization }}
</button>
<button
[abpPermission]="'AbpTenantManagement.Tenants.Delete'"
*abpPermission="'AbpTenantManagement.Tenants.Delete'"
ngbDropdownItem
(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 {
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;
} else if (this.selectedModalContent.type === 'saveTenant' && this.tenantForm.invalid) {
} else if (this.selectedModalContent.type === 'saveTenant' && this.tenantForm && this.tenantForm.invalid) {
return true;
} else {
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-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]"
><i *ngIf="route.iconClass" [ngClass]="route.iconClass"></i> {{ route.name | abpLocalization }}</a
>
@ -42,7 +42,7 @@
<ng-template #dropdownLink let-route>
<li
#navbarRootDropdown
[abpPermission]="route.requiredPolicy"
*abpPermission="route.requiredPolicy"
[abpVisibility]="routeContainer"
class="nav-item dropdown"
display="static"
@ -86,7 +86,7 @@
</ng-template>
<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]">
<i *ngIf="child.iconClass" [ngClass]="child.iconClass"></i>
{{ child.name | abpLocalization }}</a
@ -103,7 +103,7 @@
[display]="isDropdownChildDynamic ? 'dynamic' : 'static'"
placement="right-top"
[autoClose]="true"
[abpPermission]="child.requiredPolicy"
*abpPermission="child.requiredPolicy"
(openChange)="openChange($event, childrenContainer)"
>
<div ngbDropdownToggle [class.dropdown-toggle]="false">
@ -158,7 +158,7 @@
</ng-template>
<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">
<a
ngbDropdownToggle
@ -213,10 +213,10 @@
[class.d-block]="smallScreen"
[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
}}</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
}}</a>
</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 {
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);
const currentUrl: ABP.FullRoute = this.store.selectSnapshot(ConfigState.getRoute(splittedUrl[0]));
this.segments.push(currentUrl.name);
this.segments.push(currentUrl.name);
if (splittedUrl.length > 1) {
const [, ...arr] = splittedUrl;
if (splittedUrl.length > 1) {
const [, ...arr] = splittedUrl;
let childRoute: ABP.FullRoute = currentUrl;
for (let i = 0; i < arr.length; i++) {
const element = arr[i];
childRoute = childRoute.children.find(child => child.path === element);
let childRoute: ABP.FullRoute = currentUrl;
for (let i = 0; i < arr.length; i++) {
const element = arr[i];
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()">
<span aria-hidden="true">&times;</span>
</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 {
position: fixed;
top: 0;
background-color: #fff;
width: 100vw;
height: 100vh;
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 {
AfterViewInit,
ApplicationRef,
Component,
ComponentFactoryResolver,
ElementRef,
EmbeddedViewRef,
Injector,
OnDestroy,
OnInit,
Type,
ViewChild,
ApplicationRef,
Injector,
} from '@angular/core';
import { fromEvent, Subject } from 'rxjs';
import { debounceTime, filter } from 'rxjs/operators';
import snq from 'snq';
@Component({
selector: 'abp-error',
templateUrl: './error.component.html',
styleUrls: ['error.component.scss'],
selector: 'abp-http-error-wrapper',
templateUrl: './http-error-wrapper.component.html',
styleUrls: ['http-error-wrapper.component.scss'],
})
export class ErrorComponent implements AfterViewInit, OnDestroy {
export class HttpErrorWrapperComponent implements AfterViewInit, OnDestroy, OnInit {
appRef: ApplicationRef;
cfRes: ComponentFactoryResolver;
@ -38,6 +40,8 @@ export class ErrorComponent implements AfterViewInit, OnDestroy {
hideCloseIcon = false;
backgroundColor: string;
@ViewChild('container', { static: false })
containerRef: ElementRef<HTMLDivElement>;
@ -45,6 +49,11 @@ export class ErrorComponent implements AfterViewInit, OnDestroy {
return this.status ? `[${this.status}]` : '';
}
ngOnInit() {
this.backgroundColor =
snq(() => window.getComputedStyle(document.body).getPropertyValue('background-color')) || '#fff';
}
ngAfterViewInit() {
if (this.customComponent) {
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 id="modal-container" class="modal show {{ modalClass }}" tabindex="-1" role="dialog">
<div class="modal-backdrop" [@fade]="isModalOpen" (click)="close()"></div>
<div
id="abp-modal-dialog"
class="modal-dialog modal-{{ size }}"
role="document"
[class.modal-dialog-centered]="centered"
[@dialog]="isModalOpen"
#abpModalContent
>
<div id="abp-modal-content" class="modal-content">
<div id="abp-modal-header" class="modal-header">
<ng-container *ngTemplateOutlet="abpHeader"></ng-container>
​
<button id="abp-modal-close-button" type="button" class="close" aria-label="Close" (click)="close()">
<span aria-hidden="true">&times;</span>
</button>
</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
*ngIf="visible"
[@fade]="isModalOpen"
id="modal-container"
class="modal show {{ modalClass }}"
tabindex="-1"
role="dialog"
>
<div class="modal-backdrop" (click)="close()"></div>
<div
id="abp-modal-dialog"
class="modal-dialog modal-{{ size }}"
role="document"
[class.modal-dialog-centered]="centered"
#abpModalContent
>
<div id="abp-modal-content" class="modal-content">
<div id="abp-modal-header" class="modal-header">
<ng-container *ngTemplateOutlet="abpHeader"></ng-container>
​
<button id="abp-modal-close-button" type="button" class="close" aria-label="Close" (click)="close()">
<span aria-hidden="true">&times;</span>
</button>
</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>
<ng-content></ng-content>
</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';
import { fromEvent, Subject } from 'rxjs';
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 { ConfirmationService } from '../../services/confirmation.service';
import { ButtonComponent } from '../button/button.component';
@ -23,7 +23,7 @@ export type ModalSize = 'sm' | 'md' | 'lg' | 'xl';
@Component({
selector: 'abp-modal',
templateUrl: './modal.component.html',
animations: [fadeAnimation, dialogAnimation],
animations: [fadeAnimation],
})
export class ModalComponent implements OnDestroy {
@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',
})
export class SortOrderIconComponent {
private _order: string;
private _selectedKey: string;
private _order: 'asc' | 'desc' | '';
private _selectedSortKey: string;
/**
* @deprecated use selectedSortKey instead.
*/
@Input()
set selectedKey(value: string) {
this._selectedKey = value;
this.selectedSortKey = value;
this.selectedKeyChange.emit(value);
}
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 selectedSortKeyChange = new EventEmitter<string>();
/**
* @deprecated use sortKey instead.
*/
@Input()
get key(): string {
return this.sortKey;
}
set key(value: string) {
this.sortKey = value;
}
@Input()
key: string;
sortKey: string;
@Input()
set order(value: string) {
set order(value: 'asc' | 'desc' | '') {
this._order = value;
this.orderChange.emit(value);
}
get order(): string {
get order(): 'asc' | 'desc' | '' {
return this._order;
}
@ -37,16 +61,18 @@ export class SortOrderIconComponent {
iconClass: string;
get icon(): string {
if (!this.selectedKey) return 'fa-sort';
if (this.selectedKey === this.key) return `fa-sort-${this.order}`;
if (!this.selectedSortKey) return 'fa-sort';
if (this.selectedSortKey === this.sortKey) return `fa-sort-${this.order}`;
else return '';
}
sort(key: string) {
this.selectedKey = key;
this.selectedKey = key; // TODO: To be removed
this.selectedSortKey = key;
switch (this.order) {
case '':
this.order = 'asc';
this.orderChange.emit('asc');
break;
case 'asc':
this.order = 'desc';
@ -54,7 +80,8 @@ export class SortOrderIconComponent {
break;
case 'desc':
this.order = '';
this.selectedKey = '';
this.selectedKey = ''; // TODO: To be removed
this.orderChange.emit('');
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 {
position: absolute !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
width: calc(100% - 7px) !important;
height: 100% !important;
background-color: rgba(0, 0, 0, 0.6) !important;
z-index: 1040 !important;
}
.modal::-webkit-scrollbar {
width: 7px;
}
.modal::-webkit-scrollbar-track {
background: #ddd;
}
.modal::-webkit-scrollbar-thumb {
background: #8a8686;
}
.modal-dialog {
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 { Observable, Subject } from 'rxjs';
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 { Toaster } from '../models/toaster';
import { ConfirmationService } from '../services/confirmation.service';
@ -45,7 +45,7 @@ export const DEFAULT_ERROR_MESSAGES = {
@Injectable({ providedIn: 'root' })
export class ErrorHandler {
componentRef: ComponentRef<ErrorComponent>;
componentRef: ComponentRef<HttpErrorWrapperComponent>;
constructor(
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 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) {
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[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;
const createComponent = createComponentFactory({
component: DummyComponent,
imports: [CoreModule, ThemeSharedModule, NgxsModule.forRoot(), RouterTestingModule],
imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot(), RouterTestingModule],
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 { ErrorComponent } from '../components/error/error.component';
import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component';
import { LocalizationPipe } from '@abp/ng.core';
import { Store } from '@ngxs/store';
import { Renderer2, ElementRef } from '@angular/core';
import { Subject } from 'rxjs';
describe('ErrorComponent', () => {
let spectator: SpectatorHost<ErrorComponent>;
let spectator: SpectatorHost<HttpErrorWrapperComponent>;
const createHost = createHostFactory({
component: ErrorComponent,
component: HttpErrorWrapperComponent,
declarations: [LocalizationPipe],
mocks: [Store],
providers: [
@ -18,7 +18,7 @@ describe('ErrorComponent', () => {
});
beforeEach(() => {
spectator = createHost('<abp-error></abp-error>');
spectator = createHost('<abp-http-error-wrapper></abp-http-error-wrapper>');
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,
imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot([])],
stubsEnabled: false,
routes: [{ path: '', component: DummyComponent }, { path: 'account/login', component: RouterOutletComponent }],
routes: [
{ path: '', component: DummyComponent },
{ path: 'account/login', component: RouterOutletComponent },
],
});
beforeEach(() => {
spectator = createComponent();
store = spectator.get(Store);
const abpError = document.querySelector('abp-error');
const abpError = document.querySelector('abp-http-error-wrapper');
if (abpError) document.body.removeChild(abpError);
});
@ -133,14 +136,17 @@ describe('ErrorHandler with custom error component', () => {
ErrorModule,
],
stubsEnabled: false,
routes: [{ path: '', component: DummyComponent }, { path: 'account/login', component: RouterOutletComponent }],
routes: [
{ path: '', component: DummyComponent },
{ path: 'account/login', component: RouterOutletComponent },
],
});
beforeEach(() => {
spectator = createComponent();
store = spectator.get(Store);
const abpError = document.querySelector('abp-error');
const abpError = document.querySelector('abp-http-error-wrapper');
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();
});
it('should open the confirmation popup and works correct', () => {
spectator.click('#abp-modal-close-button');
expect(disappearFn).not.toHaveBeenCalled();
it('should open the confirmation popup and works correct', done => {
setTimeout(() => {
spectator.click('#abp-modal-close-button');
expect(disappearFn).not.toHaveBeenCalled();
expect(spectator.query('p-toast')).toBeTruthy();
spectator.click('button#cancel');
expect(spectator.query('div.modal')).toBeTruthy();
expect(spectator.query('p-toast')).toBeTruthy();
spectator.click('button#cancel');
expect(spectator.query('div.modal')).toBeTruthy();
spectator.click('#abp-modal-close-button');
spectator.click('button#confirm');
expect(spectator.query('div.modal')).toBeFalsy();
expect(disappearFn).toHaveBeenCalled();
spectator.click('#abp-modal-close-button');
spectator.click('button#confirm');
expect(spectator.query('div.modal')).toBeFalsy();
expect(disappearFn).toHaveBeenCalled();
done();
}, 100);
});
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 { Component } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { NgxsModule } from '@ngxs/store';
import { MessageService } from 'primeng/components/common/messageservice';
import { ToasterService } from '../services/toaster.service';
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({
selector: 'abp-dummy',
@ -23,7 +22,7 @@ describe('ToasterService', () => {
let service: ToasterService;
const createComponent = createComponentFactory({
component: DummyComponent,
imports: [CoreModule, ThemeSharedModule, NgxsModule.forRoot(), RouterTestingModule],
imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot(), RouterTestingModule],
providers: [MessageService],
});
@ -63,7 +62,10 @@ describe('ToasterService', () => {
});
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();
expect(spectator.queryAll('div.ui-toast-summary').map(node => node.textContent.trim())).toEqual([
'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 { ChartComponent } from './components/chart/chart.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 { ModalComponent } from './components/modal/modal.component';
import { SortOrderIconComponent } from './components/sort-order-icon/sort-order-icon.component';
import { TableEmptyMessageComponent } from './components/table-empty-message/table-empty-message.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 { ErrorHandler } from './handlers/error.handler';
import { chartJsLoaded$ } from './utils/widget-utils';
@ -29,16 +29,7 @@ export function appendScript(injector: Injector) {
import('chart.js').then(() => chartJsLoaded$.next(true));
const lazyLoadService: LazyLoadService = injector.get(LazyLoadService);
return forkJoin(
lazyLoadService.load(
null,
'style',
styles,
'head',
'afterbegin',
) /* lazyLoadService.load(null, 'script', scripts) */,
).toPromise();
return lazyLoadService.load(null, 'style', styles, 'head', 'afterbegin').toPromise();
};
return fn;
@ -51,7 +42,7 @@ export function appendScript(injector: Injector) {
ButtonComponent,
ChartComponent,
ConfirmationComponent,
ErrorComponent,
HttpErrorWrapperComponent,
LoaderBarComponent,
ModalComponent,
TableEmptyMessageComponent,
@ -72,9 +63,11 @@ export function appendScript(injector: Injector) {
TableSortDirective,
],
providers: [DatePipe],
entryComponents: [ErrorComponent],
entryComponents: [HttpErrorWrapperComponent],
})
export class ThemeSharedModule {
constructor(private errorHandler: ErrorHandler) {}
static forRoot(options = {} as RootParams): ModuleWithProviders {
return {
ngModule: ThemeSharedModule,
@ -82,7 +75,7 @@ export class ThemeSharedModule {
{
provide: APP_INITIALIZER,
multi: true,
deps: [Injector, ErrorHandler],
deps: [Injector],
useFactory: appendScript,
},
{ provide: MessageService, useClass: MessageService },

2
samples/BasicConsoleApplication/AbpConsoleDemo/AbpConsoleDemo.csproj

@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.Autofac" Version="0.20.0" />
<ProjectReference Include="..\..\..\framework\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" />
</ItemGroup>
</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.extensibility.execution" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="Volo.Abp.Autofac" Version="0.21.0" />
<PackageReference Include="Volo.Abp.Authorization" Version="0.21.0" />
<PackageReference Include="Volo.Abp.TestBase" Version="0.21.0" />
<ProjectReference Include="..\..\..\..\..\..\framework\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" />
<ProjectReference Include="..\..\..\..\..\..\framework\src\Volo.Abp.Authorization\Volo.Abp.Authorization.csproj" />
<ProjectReference Include="..\..\..\..\..\..\framework\src\Volo.Abp.TestBase\Volo.Abp.TestBase.csproj" />
<ProjectReference Include="..\..\src\Acme.BookStore.BookManagement.Domain\Acme.BookStore.BookManagement.Domain.csproj" />
</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": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "http://localhost:5000",
"applicationUrl": "http://localhost:64999",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}

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

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

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

@ -18,7 +18,7 @@
"PublicWebSite.Host": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "http://localhost:5000",
"applicationUrl": "http://localhost:53435",
"environmentVariables": {
"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.Extensions.DependencyInjection;
using Ocelot.DependencyInjection;
@ -52,14 +55,6 @@ namespace BackendAdminAppGateway.Host
options.Authority = configuration["AuthServer:Authority"];
options.ApiName = configuration["AuthServer:ApiName"];
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 =>
@ -94,6 +89,22 @@ namespace BackendAdminAppGateway.Host
app.UseVirtualFiles();
app.UseRouting();
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.UseSwaggerUI(options =>
{

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

Loading…
Cancel
Save