@@ -546,7 +546,7 @@ Now you can see the final result on your browser:
## Create a Books Page
-It's time to create something visible and usable! Right click to the `Pages` folder under the `Acme.BookStore.Blazor` project and add a new **razor component**, named `Books.razor`:
+It's time to create something visible and usable! Right click on the `Pages` folder under the `Acme.BookStore.Blazor` project and add a new **razor component**, named `Books.razor`:

@@ -562,7 +562,7 @@ Replace the contents of this component as shown below:
}
````
-### Add Books Page to the Main Menu
+### Add the Books Page to the Main Menu
Open the `BookStoreMenuContributor` class in the `Blazor` project add the following code to the end of the `ConfigureMainMenuAsync` method:
@@ -582,17 +582,17 @@ context.Menu.AddItem(
);
````
-Run the project, login to the application with the username `admin` and the password `1q2w3E*` and see the new menu item has been added to the main menu:
+Run the project, login to the application with the username `admin` and the password `1q2w3E*` and see that the new menu item has been added to the main menu:

-When you click to the Books menu item under the Book Store parent, you are being redirected to the new empty Books Page.
+When you click on the Books menu item under the Book Store parent, you will be redirected to the new empty Books Page.
### Book List
-We will use the [Blazorise library](https://blazorise.com/) as the UI component kit. It is a very powerful library that supports major HTML/CSS frameworks, including the Bootstrap.
+We will use the [Blazorise library](https://blazorise.com/) as the UI component kit. It is a very powerful library that supports major HTML/CSS frameworks, including Bootstrap.
-ABP Framework provides a generic base class, `AbpCrudPageBase<...>`, to create CRUD style pages. This base class is compatible to the `ICrudAppService` that was used to build the `IBookAppService`. So, we can inherit from the `AbpCrudPageBase` to automate the code behind for the standard CRUD stuff.
+ABP Framework provides a generic base class - `AbpCrudPageBase<...>`, to create CRUD style pages. This base class is compatible with the `ICrudAppService` that was used to build the `IBookAppService`. So, we can inherit from the `AbpCrudPageBase` to automate the code behind for the standard CRUD stuff.
Open the `Books.razor` and replace the content as the following:
@@ -651,17 +651,17 @@ Open the `Books.razor` and replace the content as the following:
````
-> If you see some syntax errors, you can ignore them if your application properly built and run. Visual Studio still has some bugs with Blazor.
+> If you see some syntax errors, you can ignore them if your application is properly built and running. Visual Studio still has some bugs with Blazor.
-* Inherited from the `AbpCrudPageBase
` which implements all the CRUD details for us.
+* Inherited from `AbpCrudPageBase` which implements all the CRUD details for us.
* `Entities`, `TotalCount`, `PageSize`, `OnDataGridReadAsync` are defined in the base class.
* Injected `IStringLocalizer` (as `L` object) and used for localization.
-While the code above pretty easy to understand, you can check the Blazorise [Card](https://blazorise.com/docs/components/card/) and [DataGrid](https://blazorise.com/docs/extensions/datagrid/) documents to understand them better.
+While the code above is pretty easy to understand, you can check the Blazorise [Card](https://blazorise.com/docs/components/card/) and [DataGrid](https://blazorise.com/docs/extensions/datagrid/) documents to understand them better.
#### About the AbpCrudPageBase
-We will continue to benefit from the `AbpCrudPageBase` for the books page. You could just inject the `IBookAppService` and perform all the server side calls yourself (thanks to the [Dynamic C# HTTP API Client Proxy](../API/Dynamic-CSharp-API-Clients.md) system of the ABP Framework). We will do it manually for the authors page to demonstrate how to call server side HTTP APIs in your Blazor applications.
+We will continue benefitting from `AbpCrudPageBase` for the books page. You could just inject the `IBookAppService` and perform all the server side calls yourself (thanks to the [Dynamic C# HTTP API Client Proxy](../API/Dynamic-CSharp-API-Clients.md) system of the ABP Framework). We will do it manually for the authors page to demonstrate how to call the server side HTTP APIs in your Blazor applications.
## Run the Final Application
@@ -675,4 +675,4 @@ This is a fully working, server side paged, sorted and localized table of books.
## The Next Part
-See the [next part](Part-3.md) of this tutorial.
+Check the [next part](Part-3.md) of this tutorial.
diff --git a/docs/en/Tutorials/Part-3.md b/docs/en/Tutorials/Part-3.md
index 92e04d0b41..f6a59f1108 100644
--- a/docs/en/Tutorials/Part-3.md
+++ b/docs/en/Tutorials/Part-3.md
@@ -13,7 +13,7 @@ In this tutorial series, you will build an ABP based web application named `Acme
* **{{DB_Value}}** as the ORM provider.
* **{{UI_Value}}** as the UI Framework.
-This tutorial is organized as the following parts;
+This tutorial is organized as the following parts:
- [Part 1: Creating the server side](Part-1.md)
- [Part 2: The book list page](Part-2.md)
@@ -51,19 +51,19 @@ This part is also recorded as a video tutorial and **
```
-* Added `New book` button to the card header..
+* Added a `New book` button to the card header..
* Added the `abp-modal` which renders a modal to allow user to create a new book. `abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits.
-You can open your browser and click **New book** button to see the new modal.
+You can open your browser and click the **New book** button to see the new modal.

@@ -801,11 +801,11 @@ export class BookComponent implements OnInit {
```
* Imported `FormGroup`, `FormBuilder` and `Validators` from `@angular/forms`.
-* Added `form: FormGroup` property.
-* Added `bookTypes` property as a list of `BookType` enum members. That will be used in form options.
+* Added a `form: FormGroup` property.
+* Added a `bookTypes` property as a list of `BookType` enum members. That will be used in form options.
* Injected `FormBuilder` into the constructor. [FormBuilder](https://angular.io/api/forms/FormBuilder) provides convenient methods for generating form controls. It reduces the amount of boilerplate needed to build complex forms.
-* Added `buildForm` method to the end of the file and executed the `buildForm()` in the `createBook` method.
-* Added `save` method.
+* Added a `buildForm` method to the end of the file and executed the `buildForm()` in the `createBook` method.
+* Added a `save` method.
Open `/src/app/book/book.component.html` and replace ` ` with the following code part:
@@ -863,7 +863,7 @@ Also replace ` ` with the following code p
### Datepicker
-We've used [NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview) in this component. So, need to arrange dependencies related to this component.
+We've used [NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview) in this component. So, we need to arrange the dependencies related to this component.
Open `/src/app/book/book.module.ts` and replace the content as below:
@@ -959,7 +959,7 @@ export class BookComponent implements OnInit {
```
* Imported ` NgbDateNativeAdapter` and `NgbDateAdapter`.
-* We added a new provider `NgbDateAdapter` that converts Datepicker value to `Date` type. See the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details.
+* We added a new provider `NgbDateAdapter` that converts the Datepicker value to `Date` type. Check out the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details.
Now, you can open your browser to see the changes:
@@ -1054,14 +1054,14 @@ export class BookComponent implements OnInit {
```
* We declared a variable named `selectedBook` as `BookDto`.
-* We added `editBook` method. This method fetches the book with the given `id` and sets it to `selectedBook` object.
+* We added an `editBook` method. This method fetches the book with the given `id` and sets it to `selectedBook` object.
* We replaced the `buildForm` method so that it creates the form with the `selectedBook` data.
* We replaced the `createBook` method so it sets `selectedBook` to an empty object.
* We changed the `save` method to handle both of create and update operations.
### Add "Actions" Dropdown to the Table
-Open the `/src/app/book/book.component.html` and add the following `ngx-datatable-column` definition as the first column in the `ngx-datatable`:
+Open `/src/app/book/book.component.html` and add the following `ngx-datatable-column` definition as the first column in the `ngx-datatable`:
```html
```
-This template will show **Edit** text for edit record operation, **New Book** for new record operation in the title.
+This template will show the **Edit** text for edit record operation, **New Book** for new record operation in the title.
## Deleting a Book
-Open the `/src/app/book/book.component.ts` and inject the `ConfirmationService`.
+Open the `/src/app/book/book.component.ts` file and inject the `ConfirmationService`.
Replace the constructor as below:
@@ -1137,7 +1137,7 @@ delete(id: string) {
* We injected `ConfirmationService` to the constructor.
* Added a `delete` method.
-> See the [Confirmation Popup documentation](../UI/Angular/Confirmation-Service) for more about this service.
+> Check out the [Confirmation Popup documentation](../UI/Angular/Confirmation-Service) for more about this service.
### Add a Delete Button
@@ -1169,7 +1169,7 @@ Clicking the "Delete" action calls the `delete` method which then shows a confir
In this section, you will learn how to create a new modal dialog form to create a new book. Since we've inherited from the `AbpCrudPageBase`, we only need to develop the view part.
-### Add "New Button" Button
+### Add a "New Button" Button
Open the `Books.razor` and replace the `` section with the following code:
@@ -1191,7 +1191,7 @@ This will change the card header by adding a "New book" button to the right side

-Now, we can add a modal that will be opened when we click to the button.
+Now, we can add a modal that will be opened when we click the button.
### Book Creation Modal
@@ -1259,7 +1259,7 @@ This code requires a service; Inject the `AbpBlazorMessageLocalizerHelper` at
````
* The form implements validation and the `AbpBlazorMessageLocalizerHelper` is used to simply localize the validation messages.
-* `CreateModal` object, `CloseCreateModalAsync` and `CreateEntityAsync` method are defined by the base class. See the [Blazorise documentation](https://blazorise.com/docs/) if you want to understand the `Modal` and the other components.
+* The `CreateModal` object, `CloseCreateModalAsync` and `CreateEntityAsync` methods are defined by the base class. Check out the [Blazorise documentation](https://blazorise.com/docs/) if you want to understand the `Modal` and the other components.
That's all. Run the application and try to add a new book:
@@ -1267,7 +1267,7 @@ That's all. Run the application and try to add a new book:
## Updating a Book
-Editing a books is similar to the creating a new book.
+Editing a book is similar to creating a new book.
### Actions Dropdown
@@ -1287,7 +1287,7 @@ Open the `Books.razor` and add the following `DataGridEntityActionsColumn` secti
* `OpenEditModalAsync` is defined in the base class which takes the entity (book) to edit.
-`DataGridEntityActionsColumn` component is used to show an "Actions" dropdown for each row in the `DataGrid`. `DataGridEntityActionsColumn` shows a **single button** instead of a dropdown if there is only one available action inside it:
+The `DataGridEntityActionsColumn` component is used to show an "Actions" dropdown for each row in the `DataGrid`. The `DataGridEntityActionsColumn` shows a **single button** instead of a dropdown if there is only one available action inside it:

@@ -1384,7 +1384,7 @@ You can now run the application and try to edit a book.
## Deleting a Book
-Open the `Books.razor` page and add the following `EntityAction` under the "Edit" action inside the `EntityActions`:
+Open the `Books.razor` page and add the following `EntityAction` code under the "Edit" action inside `EntityActions`:
````xml
+
+ {{ data.textTemplate.text | abpLocalization }}
+
+ `,
+})
+export class UserMenuItemComponent {
+ // you can inject the data through `INJECTOR_PIPE_DATA_TOKEN` token
+ constructor(@Inject(INJECTOR_PIPE_DATA_TOKEN) public data: UserMenu) {}
+}
@Component({/* component metadata */})
export class AppComponent {
- constructor(private navItems: NavItemsService) {
- this.navItems.patchItem(eThemeBasicComponents.CurrentUser, { component: MyUserMenuComponent });
+ constructor(private userMenu: UserMenuService, private router: Router) {
+ this.userMenu.addItems([
+ {
+ id: 'UserMenu.MyAccount',
+ order: 1,
+
+ // HTML example
+ html: `My account `,
+
+ // text template example
+ textTemplate: {
+ text: 'AbpAccount::MyAccount',
+ icon: 'fa fa-cog',
+ },
+ // component example
+ component: UserMenuItemComponent,
+
+ action: () => {
+ this.router.navigateByUrl('/account/manage');
+ },
+ },
+ ]);
+
+ this.userMenu.patchItem(eUserMenuItems.MyAccount, {
+ html: `My profile `,
+ });
+
+ this.userMenu.removeItem(eUserMenuItems.Logout);
}
}
````
-[`ConfigStateService`](Config-State-Service.md) service can be used to obtain the `application-configuration` API response (e.g. getting current user or tenant).
+In the example above, added a new user menu item labeled "Reports", updated the "My account" item HTML content, and removed the "Logout" item.
+
+See the result:
+
+
#### Page Alerts
-`PageAlertService` service is used to get the current page alerts to render on the layout. See the [Page Alerts](Page-Alerts.md) document to learn more.
\ No newline at end of file
+`PageAlertService` service is used to get the current page alerts to render on the layout. See the [Page Alerts](Page-Alerts.md) document to learn more.
diff --git a/docs/zh-Hans/Autofac-Integration.md b/docs/zh-Hans/Autofac-Integration.md
index 9b1d249e4e..6fd2a91e6f 100644
--- a/docs/zh-Hans/Autofac-Integration.md
+++ b/docs/zh-Hans/Autofac-Integration.md
@@ -32,26 +32,23 @@ namespace MyCompany.MyProject
### ASP.NET Core 应用程序
-如下所示, 在 **Startup.cs** 文件中调用 `UseAutofac()`:
+如下所示, 在 **Program.cs** 文件中调用 `UseAutofac()`:
````csharp
-public class Startup
+public class Program
{
- public IServiceProvider ConfigureServices(IServiceCollection services)
+ public static int Main(string[] args)
{
- services.AddApplication(options =>
- {
- //Integrate Autofac!
- options.UseAutofac();
- });
-
- return services.BuildServiceProviderFromFactory();
+ CreateHostBuilder(args).Build().Run();
}
- public void Configure(IApplicationBuilder app)
- {
- app.InitializeApplication();
- }
+ internal static IHostBuilder CreateHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.UseStartup();
+ })
+ .UseAutofac(); //Integrate Autofac!
}
````
diff --git a/docs/zh-Hans/Background-Workers-Hangfire.md b/docs/zh-Hans/Background-Workers-Hangfire.md
index bd63ff91e9..5e53928ca1 100644
--- a/docs/zh-Hans/Background-Workers-Hangfire.md
+++ b/docs/zh-Hans/Background-Workers-Hangfire.md
@@ -67,6 +67,33 @@ public class MyLogWorker : HangfireBackgroundWorkerBase
> 你可以直接实现 `IHangfireBackgroundWorker`, 但是 `HangfireBackgroundWorkerBase` 提供了一些有用的属性,例如 `Logger`.
+### UnitOfWork
+
+使用 `UnitOfWorkAttribute` 你需要为工作者定义一个接口:
+
+```csharp
+public interface IMyLogWorker : IHangfireBackgroundWorker
+{
+}
+
+[ExposeServices(typeof(IMyLogWorker))]
+public class MyLogWorker : HangfireBackgroundWorkerBase, IMyLogWorker
+{
+ public MyLogWorker()
+ {
+ RecurringJobId = nameof(MyLogWorker);
+ CronExpression = Cron.Daily();
+ }
+
+ [UnitOfWork]
+ public override Task DoWorkAsync()
+ {
+ Logger.LogInformation("Executed MyLogWorker..!");
+ return Task.CompletedTask;
+ }
+}
+```
+
## 注册到后台工作者管理器
创建一个后台工作者后, 你应该添加到 `IBackgroundWorkerManager`, 最常用的地方是在你模块类的 `OnApplicationInitialization` 方法中:
@@ -79,6 +106,9 @@ public class MyModule : AbpModule
ApplicationInitializationContext context)
{
context.AddBackgroundWorker();
+
+ //如果定义了接口
+ //context.AddBackgroundWorker();
}
}
````
diff --git a/docs/zh-Hans/Modules/Cms-Kit.md b/docs/zh-Hans/Modules/Cms-Kit.md
deleted file mode 100644
index e13f1e585b..0000000000
--- a/docs/zh-Hans/Modules/Cms-Kit.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# CMS Kit Module
-
-TODO
\ No newline at end of file
diff --git a/docs/zh-Hans/Modules/Cms-Kit/Blogging.md b/docs/zh-Hans/Modules/Cms-Kit/Blogging.md
new file mode 100644
index 0000000000..1d2c3ecd9e
--- /dev/null
+++ b/docs/zh-Hans/Modules/Cms-Kit/Blogging.md
@@ -0,0 +1,3 @@
+# CMS Kit: Blogging
+
+TODO...
\ No newline at end of file
diff --git a/docs/zh-Hans/Modules/Cms-Kit/Comments.md b/docs/zh-Hans/Modules/Cms-Kit/Comments.md
new file mode 100644
index 0000000000..faeb6116c9
--- /dev/null
+++ b/docs/zh-Hans/Modules/Cms-Kit/Comments.md
@@ -0,0 +1,3 @@
+# CMS Kit: Comments
+
+TODO...
\ No newline at end of file
diff --git a/docs/zh-Hans/Modules/Cms-Kit/Index.md b/docs/zh-Hans/Modules/Cms-Kit/Index.md
new file mode 100644
index 0000000000..0751eb0ad0
--- /dev/null
+++ b/docs/zh-Hans/Modules/Cms-Kit/Index.md
@@ -0,0 +1,79 @@
+# 内容管理系统套件模块
+
+此模块为您的应用程序提供内容管理系统 (Content Management System, CMS) 功能. 它提供 **核心构建块** 和完整工作的 **子系统**, 以创建启用 CMS 功能的您自己的网站, 或出于任何目的使用网站中的构建块.
+
+> **此模块目前仅适用于 MVC / Razor 页面 UI**. 虽然没有官方的 Blazor 软件包, 但它也可以在 Blazor 服务器 UI 中工作, 因为实际上 Blazor 服务器 UI 实际上是一个运行在 ASP.NET Core MVC / Razor 页面应用程序的混合型应用程序.
+
+目前提供以下功能:
+
+* 提供 [**页面**](Pages.md) 管理系统来管理具有动态 URL 的动态页面.
+* 提供 [**博客**](Blogging.md) 系统来创建发表具有多种博客支持的博客文章.
+* 提供 [**标签**](Tags.md) 系统来标记任何资源, 如博客文章.
+* 提供 [**评论**](Comments.md) 系统来添加对任何资源的评论功能, 如博客文章或产品评价页面.
+* 提供 [**反应**](Reactions.md) 系统来添加对任何资源的反应 (表情符号) 功能, 如博客文章或评论.
+* 提供 [**评级**](Ratings.md) 系统来添加对任何资源的评级功能.
+* 提供 [**菜单**](Menus.md) 系统来动态管理公共菜单.
+
+点击功能以了解和学习如何去使用它.
+
+所有功能均可单独使用. 如果你禁用了一个功能, 则在 [全局功能](../../Global-Features.md) 系统的帮助下, 该功能会从你的应用程序甚至数据库表中完全消失.
+
+## 预备要求
+- 此模块依赖于 [Blob 存储](../../Blob-Storing.md) 模块来保存媒体内容.
+> 确保 `BlobStoring` 模块已安装并至少正确地配置了一个提供程序. 请查阅 [文档](../../Blob-Storing.md) 了解更多信息.
+
+- CMS Kit 使用 [分布式缓存](../../Caching.md) 来提高响应速度.
+> 强烈建议在分布式/集群部署中为实现数据一致性使用分布式缓存, 如 [Redis](../../Redis-Cache.md).
+
+## 如何安装
+
+可以使用 [ABP CLI](../../CLI.md) 的 `add-module` 命令为解决方案安装模块. 您可以使用以下命令在命令行中安装 CMS Kit 模块:
+
+```bash
+abp add-module Volo.CmsKit
+```
+
+> 默认情况下, Cms-Kit `GlobalFeature` 被禁用. 因此初始迁移将为空. 所以, 当你使用 EF Core 安装时,你可以添加 `--skip-db-migrations` 命令来跳过迁移. 启用 Cms-Kit 全局功能后, 请添加新的迁移.
+
+安装过程完成后, 在您的解决方案 `Domain.Shared` 项目中打开 `GlobalFeatureConfigurator` 类, 并将以下代码写入 `Configure` 方法中, 以启用 CMS Kit 模块的全部功能.
+
+```csharp
+GlobalFeatureManager.Instance.Modules.CmsKit(cmsKit =>
+{
+ cmsKit.EnableAll();
+});
+```
+
+你可能更愿意逐个启用这些功能, 而不是启用全部功能. 以下示例仅启用了 [标签](Tags.md) 和 [评论](Comments.md) 功能:
+
+````csharp
+GlobalFeatureManager.Instance.Modules.CmsKit(cmsKit =>
+{
+ cmsKit.Tags.Enable();
+ cmsKit.Comments.Enable();
+});
+````
+
+> 如果你使用 EF Core, 不要忘记添加一个新的迁移并更新你的数据库.
+
+## 软件包
+
+此模块遵循 [模块开发最佳实践指南](https://docs.abp.io/zh-Hans/abp/latest/Best-Practices/Index), 由多个 NuGet 和 NPM 软件包组成. 如果你想了解软件包及其之间的关系, 请参阅指南.
+
+CMS Kit 软件包专为各种使用场景而设计. 如果您查阅了 [CMS Kit 软件包](https://www.nuget.org/packages?q=Volo.CmsKit) 您将看到一些有 `Admin` 和 `Public` 后缀的软件包. 该模块有两个应用程序层, 原因是他们可能被用于不同类型的应用程序. 这些应用程序层仅使用一个领域层.
+
+ - `Volo.CmsKit.Admin.*` 软件包包括管理员 (后台) 应用程序所必须的功能.
+ - `Volo.CmsKit.Public.*` 软件包包括被用于用户阅读博客文章和发表评论的公共网站上的功能.
+ - `Volo.CmsKit.*` (不带 Admin/Public 后缀) 软件包称为统一包. 统一包分别是添加 Admin 和 Public (相关层的) 软件包的快照. 如果您有一个用于管理和公共网站的单应用程序, 您可以使用这些软件包.
+
+## 内部结构
+
+### 表/集合 前缀&架构
+
+所有表/集合使用 `Cms` 作为默认前缀. 如果需要更改表的前缀或者设置一个架构名称 (如果你的数据库提供程序支持), 请在 `CmsKitDbProperties` 类中设置静态属性.
+
+### 连接字符串
+
+此模块使用 `CmsKit` 作为连接字符串的名称. 如果您未使用此名称定义连接字符串, 它将回退为 `Default` 连接字符串.
+
+有关详细信息, 请参阅 [连接字符串](https://docs.abp.io/en/abp/latest/Connection-Strings) 文档.
diff --git a/docs/zh-Hans/Modules/Cms-Kit/Menus.md b/docs/zh-Hans/Modules/Cms-Kit/Menus.md
new file mode 100644
index 0000000000..fb214e358c
--- /dev/null
+++ b/docs/zh-Hans/Modules/Cms-Kit/Menus.md
@@ -0,0 +1,3 @@
+# CMS Kit: Pages
+
+TODO...
\ No newline at end of file
diff --git a/docs/zh-Hans/Modules/Cms-Kit/Pages.md b/docs/zh-Hans/Modules/Cms-Kit/Pages.md
new file mode 100644
index 0000000000..fb214e358c
--- /dev/null
+++ b/docs/zh-Hans/Modules/Cms-Kit/Pages.md
@@ -0,0 +1,3 @@
+# CMS Kit: Pages
+
+TODO...
\ No newline at end of file
diff --git a/docs/zh-Hans/Modules/Cms-Kit/Ratings.md b/docs/zh-Hans/Modules/Cms-Kit/Ratings.md
new file mode 100644
index 0000000000..2c6cf44460
--- /dev/null
+++ b/docs/zh-Hans/Modules/Cms-Kit/Ratings.md
@@ -0,0 +1,3 @@
+# Rating System
+
+TODO...
\ No newline at end of file
diff --git a/docs/zh-Hans/Modules/Cms-Kit/Reactions.md b/docs/zh-Hans/Modules/Cms-Kit/Reactions.md
new file mode 100644
index 0000000000..9b714adf26
--- /dev/null
+++ b/docs/zh-Hans/Modules/Cms-Kit/Reactions.md
@@ -0,0 +1,3 @@
+# Reaction System
+
+TODO...
\ No newline at end of file
diff --git a/docs/zh-Hans/Modules/Cms-Kit/Tags.md b/docs/zh-Hans/Modules/Cms-Kit/Tags.md
new file mode 100644
index 0000000000..3977d06f5a
--- /dev/null
+++ b/docs/zh-Hans/Modules/Cms-Kit/Tags.md
@@ -0,0 +1,3 @@
+# Tag Management
+
+TODO...
\ No newline at end of file
diff --git a/docs/zh-Hans/Modules/Identity.md b/docs/zh-Hans/Modules/Identity.md
index 73f05af7bb..bf715bc7b6 100644
--- a/docs/zh-Hans/Modules/Identity.md
+++ b/docs/zh-Hans/Modules/Identity.md
@@ -4,7 +4,7 @@
## 如何安装
-当你使用 ABP 框架 [创建一个新的解决方案](https://abp.io/get-started) 时,此模块将被预安装(作为 NuGet/NPM 包)。你可以继续用其作为包并轻松地获取更新,也可以将其源代码包含在解决方案中(请参阅 `get-source` [CLI](../CLI.md))以开发自定义模块。
+当你使用 ABP 框架 [创建一个新的解决方案](https://abp.io/get-started) 时, 此模块将被预安装 (作为 NuGet/NPM 包). 你可以继续用其作为包并轻松地获取更新, 也可以将其源代码包含在解决方案中(请参阅 `get-source` [CLI](../CLI.md))以开发自定义模块.
### 源代码
diff --git a/docs/zh-Hans/Modules/Tenant-Management.md b/docs/zh-Hans/Modules/Tenant-Management.md
new file mode 100644
index 0000000000..f8b0d60aa7
--- /dev/null
+++ b/docs/zh-Hans/Modules/Tenant-Management.md
@@ -0,0 +1,107 @@
+# 租户管理模块
+
+[多租户](../Multi-Tenancy.md) 是 ABP 框架的核心功能之一. 它提供了构建 SaaS (软件及服务) 解决方案的基础架构. ABP 的多租户系统通过提供 `ITenantStore` 接口来抽象租户存储的位置. 您需要做的就是实现该接口.
+
+**租户管理模块是 `ITenantStore` 接口的实现. 它将租户存储在数据库中. 它还提供了 UI 来管理租户及其 [功能](../Features.md).**
+
+> 请参阅 [多租户](../Multi-Tenancy.md) 文档,以了解 ABP 框架的多租户系统. 本文重点介绍租户管理模块.
+
+### 关于商业版 SaaS 模块
+
+[SaaS 模块](https://commercial.abp.io/modules/Volo.Saas) 是此模块的替代实现, 具有更多的功能和可能性. 它作为 [ABP 商业版](https://commercial.abp.io/) 订阅的一部分分发.
+
+## 如何安装
+
+当你使用 ABP 框架 [创建一个新的解决方案](https://abp.io/get-started) 时, 此模块将被预安装(作为 NuGet/NPM 包). 你可以继续用其作为包并轻松地获取更新, 也可以将其源代码包含在解决方案中(请参阅 `get-source` [CLI](../CLI.md))以开发自定义模块.
+
+### 源代码
+
+可以 [在此处](https://github.com/abpframework/abp/tree/dev/modules/identity) 访问源代码。源代码使用 [MIT](https://choosealicense.com/licenses/mit/) 许可, 所以你可以免费使用和自定义它.
+
+## 用户界面
+
+此模块将 "管理 -> 租户管理 -> 租户" 菜单项添加到应用程序的主菜单, 这将打开如下页面:
+
+
+
+在这个页面中, 你可以查看所有租户. 你可以按照如下所示创建一个新的租户:
+
+
+
+在这个模态框中;
+
+* **租户名称**: 租户唯一的名称. 如果为租户使用二级域名, 这将是二级域名名称.
+* **管理员电子邮件地址**: 租户管理员的电子邮件地址.
+* **管理员密码**: 租户管理员的密码.
+
+当您点击租户附近的 *操作* 按钮时, 您将看见可以执行的操作:
+
+
+
+### 管理租户功能
+
+功能操作会打开一个模态框, 来对相关租户进行启用/禁用/设置 [功能](../Features.md). 这有一个模态框的例子:
+
+
+
+### 管理主机功能
+
+如果您使用应用程序的功能也在主机端, *管理主机功能* 按钮则用于设置主机端的功能.
+
+## 内部结构
+
+如果要 [自定义](../Customizing-Application-Modules-Guide.md) 此模块而不更改 [其源代码](https://github.com/abpframework/abp/tree/dev/modules/tenant-management), 则可以将此节作为参考.
+
+### 领域层
+
+#### 聚合
+
+* `Tenant`
+
+#### 仓储
+
+* `ITenantRepository`
+
+#### 领域服务
+
+* `TenantManager`
+
+### 应用层
+
+#### 应用服务
+
+* `TenantAppService`
+
+#### 权限
+
+- `AbpTenantManagement.Tenants`: 租户管理.
+- `AbpTenantManagement.Tenants.Create`: 创建一个新的租户.
+- `AbpTenantManagement.Tenants.Update`: 编辑一个已存在的租户.
+- `AbpTenantManagement.Tenants.Delete`: 删除一个已存在的租户.
+- `AbpTenantManagement.Tenants.ManageFeatures`: 管理租户的功能.
+
+### EF Core 集成
+
+* `TenantManagementDbContext` (实现了 `ITenantManagementDbContext`)
+
+**数据库表:**
+
+* `AbpTenants`
+* `AbpTenantConnectionStrings`
+
+### MongoDB 集成
+
+* `TenantManagementMongoDbContext` (实现了 `ITenantManagementMongoDbContext`)
+
+**数据库集合:**
+
+* `AbpTenants` (也包括连接字符串)
+
+## 注意事项
+
+ABP 框架允许租户有专用的数据库, 以允许 *每个租户用一个数据库*. 此模块具有使该实现成为可能 (查阅其源代码) 的基础架构, 但是它没有实现应用程序层和 UI 功能以使其开箱即用. 您可以自己实现这些功能, 也可以考虑使用已完全实现这些功能并提供了更多业务功能的 [ABP 商业版 Saas 模块](https://docs.abp.io/en/commercial/latest/modules/saas).
+
+## 另请参见
+
+* [多租户](../Multi-Tenancy.md)
+* [ABP 商业版 SaaS 模块](https://docs.abp.io/en/commercial/latest/modules/saas)
diff --git a/docs/zh-Hans/Modules/Tentant-Management.md b/docs/zh-Hans/Modules/Tentant-Management.md
deleted file mode 100644
index fa999428dd..0000000000
--- a/docs/zh-Hans/Modules/Tentant-Management.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# 租户管理模块
-
-TODO
\ No newline at end of file
diff --git a/docs/zh-Hans/Tutorials/Part-8.md b/docs/zh-Hans/Tutorials/Part-8.md
index 602306c484..4727b8392e 100644
--- a/docs/zh-Hans/Tutorials/Part-8.md
+++ b/docs/zh-Hans/Tutorials/Part-8.md
@@ -1 +1,574 @@
-TODO..
\ No newline at end of file
+# Web应用程序开发教程 - 第八章: 作者: 应用服务层
+````json
+//[doc-params]
+{
+ "UI": ["MVC","Blazor","BlazorServer","NG"],
+ "DB": ["EF","Mongo"]
+}
+````
+## 关于本教程
+
+在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的:
+
+* **{{DB_Text}}** 做为ORM提供程序.
+* **{{UI_Value}}** 做为UI框架.
+
+本教程分为以下部分:
+
+- [Part 1: 创建服务端](Part-1.md)
+- [Part 2: 图书列表页面](Part-2.md)
+- [Part 3: 创建,更新和删除图书](Part-2.md)
+- [Part 4: 集成测试](Part-4.md)
+- [Part 5: 授权](Part-5.md)
+- [Part 6: 作者: 领域层](Part-6.md)
+- [Part 7: 作者: 数据库集成](Part-7.md)
+- [Part 8: 作者: 应用服务层](本章)
+- [Part 9: 作者: 用户页面](Part-9.md)
+- [Part 10: 图书到作者的关系](Part-10.md)
+
+## 下载源码
+
+本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合:
+
+* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore)
+* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
+* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
+
+> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
+
+> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
+> `git config --system core.longpaths true`
+
+## 简介
+
+这章阐述如何为前一章介绍的 `作者` 实体创建应用服务层.
+
+## IAuthorAppService
+
+我们首先创建 [应用服务](../Application-Services.md) 接口和相关的 [DTO](../Data-Transfer-Objects.md)s. 在 `Acme.BookStore.Application.Contracts` 项目的 `Authors` 命名空间 (文件夹) 创建一个新接口 `IAuthorAppService`:
+
+````csharp
+using System;
+using System.Threading.Tasks;
+using Volo.Abp.Application.Dtos;
+using Volo.Abp.Application.Services;
+
+namespace Acme.BookStore.Authors
+{
+ public interface IAuthorAppService : IApplicationService
+ {
+ Task GetAsync(Guid id);
+
+ Task> GetListAsync(GetAuthorListDto input);
+
+ Task CreateAsync(CreateAuthorDto input);
+
+ Task UpdateAsync(Guid id, UpdateAuthorDto input);
+
+ Task DeleteAsync(Guid id);
+ }
+}
+````
+
+* `IApplicationService` 是一个常规接口, 所有应用服务都继承自它, 所以 ABP 框架可以识别它们.
+* 在 `Author` 实体中定义标准方法用于CRUD操作.
+* `PagedResultDto` 是一个ABP框架中预定义的 DTO 类. 它拥有一个 `Items` 集合 和一个 `TotalCount` 属性, 用于返回分页结果.
+* 优先从 `CreateAsync` 方法返回 `AuthorDto` (新创建的作者), 虽然在这个程序中没有这么做 - 这里只是展示一种不同用法.
+
+这个类使用下面定义的DTOs (为你的项目创建它们).
+
+### AuthorDto
+
+````csharp
+using System;
+using Volo.Abp.Application.Dtos;
+
+namespace Acme.BookStore.Authors
+{
+ public class AuthorDto : EntityDto
+ {
+ public string Name { get; set; }
+
+ public DateTime BirthDate { get; set; }
+
+ public string ShortBio { get; set; }
+ }
+}
+````
+
+* `EntityDto` 只有一个类型为指定泛型参数的 `Id` 属性. 你可以自己创建 `Id` 属性, 而不是继承自 `EntityDto`.
+
+### GetAuthorListDto
+
+````csharp
+using Volo.Abp.Application.Dtos;
+
+namespace Acme.BookStore.Authors
+{
+ public class GetAuthorListDto : PagedAndSortedResultRequestDto
+ {
+ public string Filter { get; set; }
+ }
+}
+````
+
+* `Filter` 用于搜索作者. 它可以是 `null` (或空字符串) 以获得所有用户.
+* `PagedAndSortedResultRequestDto` 具有标准分页和排序属性: `int MaxResultCount`, `int SkipCount` 和 `string Sorting`.
+
+> ABP 框架拥有这些基本的DTO类以简化并标准化你的DTOs. 参阅 [DTO 文档](../Data-Transfer-Objects.md) 获得所有DTO类的详细信息.
+
+### CreateAuthorDto
+
+````csharp
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Acme.BookStore.Authors
+{
+ public class CreateAuthorDto
+ {
+ [Required]
+ [StringLength(AuthorConsts.MaxNameLength)]
+ public string Name { get; set; }
+
+ [Required]
+ public DateTime BirthDate { get; set; }
+
+ public string ShortBio { get; set; }
+ }
+}
+````
+
+数据标记特性可以用来验证DTO. 参阅 [验证文档](../Validation.md) 获得详细信息.
+
+### UpdateAuthorDto
+
+````csharp
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Acme.BookStore.Authors
+{
+ public class UpdateAuthorDto
+ {
+ [Required]
+ [StringLength(AuthorConsts.MaxNameLength)]
+ public string Name { get; set; }
+
+ [Required]
+ public DateTime BirthDate { get; set; }
+
+ public string ShortBio { get; set; }
+ }
+}
+````
+
+> 我们可以在创建和更新操作间分享 (重用) 相同的DTO. 虽然可以这么做, 但我们推荐为这些操作创建不同的DTOs, 因为我们发现随着时间的推移, 它们通常会变得有差异. 所以, 与紧耦合相比, 代码重复也是合理的.
+
+## AuthorAppService
+
+是时候实现 `IAuthorAppService` 接口了. 在 `Acme.BookStore.Application` 项目的 `Authors` 命名空间 (文件夹) 中创建一个新类 `AuthorAppService` :
+
+````csharp
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Acme.BookStore.Permissions;
+using Microsoft.AspNetCore.Authorization;
+using Volo.Abp.Application.Dtos;
+using Volo.Abp.Domain.Repositories;
+
+namespace Acme.BookStore.Authors
+{
+ [Authorize(BookStorePermissions.Authors.Default)]
+ public class AuthorAppService : BookStoreAppService, IAuthorAppService
+ {
+ private readonly IAuthorRepository _authorRepository;
+ private readonly AuthorManager _authorManager;
+
+ public AuthorAppService(
+ IAuthorRepository authorRepository,
+ AuthorManager authorManager)
+ {
+ _authorRepository = authorRepository;
+ _authorManager = authorManager;
+ }
+
+ //...SERVICE METHODS WILL COME HERE...
+ }
+}
+````
+
+* `[Authorize(BookStorePermissions.Authors.Default)]` 是一个检查权限(策略)的声明式方法, 用来给当前用户授权. 参阅 [授权文档](../Authorization.md) 获得详细信息. `BookStorePermissions` 类在后文会被更新, 现在不需要担心编译错误.
+* 由 `BookStoreAppService` 派生, 这个类是一个简单基类, 可以做为模板. 它继承自标准的 `ApplicationService` 类.
+* 实现上面定义的 `IAuthorAppService` .
+* 注入 `IAuthorRepository` 和 `AuthorManager` 以使用服务方法.
+
+现在, 我们逐个介绍服务方法. 复制这些方法到 `AuthorAppService` 类.
+
+### GetAsync
+
+````csharp
+public async Task GetAsync(Guid id)
+{
+ var author = await _authorRepository.GetAsync(id);
+ return ObjectMapper.Map(author);
+}
+````
+
+这个方法根据 `Id` 获得 `Author` 实体, 使用 [对象到对象映射](../Object-To-Object-Mapping.md) 转换为 `AuthorDto`. 这需要配置AutoMapper, 后面会介绍.
+
+### GetListAsync
+
+````csharp
+public async Task> GetListAsync(GetAuthorListDto input)
+{
+ if (input.Sorting.IsNullOrWhiteSpace())
+ {
+ input.Sorting = nameof(Author.Name);
+ }
+
+ var authors = await _authorRepository.GetListAsync(
+ input.SkipCount,
+ input.MaxResultCount,
+ input.Sorting,
+ input.Filter
+ );
+
+ var totalCount = input.Filter == null
+ ? await _authorRepository.CountAsync()
+ : await _authorRepository.CountAsync(
+ author => author.Name.Contains(input.Filter));
+
+ return new PagedResultDto(
+ totalCount,
+ ObjectMapper.Map, List>(authors)
+ );
+}
+````
+
+* 为处理客户端没有设置的情况, 在方法的开头设置默认排序是 "根据作者名".
+* 使用 `IAuthorRepository.GetListAsync` 从数据库中获得分页的, 排序的和过滤的作者列表. 我们已经在教程的前一章中实现了它. 再一次强调, 实际上不需要创建这个方法, 因为我们可以从数据库中直接查询, 这里只是演示如何创建自定义repository方法.
+* 直接查询 `AuthorRepository` , 得到作者的数量. 如果客户端发送了过滤条件, 会得到过滤后的作者数量.
+* 最后, 通过映射 `Author` 列表到 `AuthorDto` 列表, 返回分页后的结果.
+
+### CreateAsync
+
+````csharp
+[Authorize(BookStorePermissions.Authors.Create)]
+public async Task CreateAsync(CreateAuthorDto input)
+{
+ var author = await _authorManager.CreateAsync(
+ input.Name,
+ input.BirthDate,
+ input.ShortBio
+ );
+
+ await _authorRepository.InsertAsync(author);
+
+ return ObjectMapper.Map(author);
+}
+````
+
+* `CreateAsync` 需要 `BookStorePermissions.Authors.Create` 权限 (另外包括 `AuthorAppService` 类声明的 `BookStorePermissions.Authors.Default` 权限).
+* 使用 `AuthorManager` (领域服务) 创建新作者.
+* 使用 `IAuthorRepository.InsertAsync` 插入新作者到数据库.
+* 使用 `ObjectMapper` 返回 `AuthorDto` , 代表新创建的作者.
+
+> **DDD提示**: 一些开发者可能会发现可以在 `_authorManager.CreateAsync` 插入新实体. 我们认为把它留给应用层是更好的设计, 因为应用层更了解应该何时插入实体到数据库(在插入实体前可能需要额外的工作. 如果在领域层插入, 可能需要额外的更新操作). 但是, 你拥有最终的决定权.
+
+### UpdateAsync
+
+````csharp
+[Authorize(BookStorePermissions.Authors.Edit)]
+public async Task UpdateAsync(Guid id, UpdateAuthorDto input)
+{
+ var author = await _authorRepository.GetAsync(id);
+
+ if (author.Name != input.Name)
+ {
+ await _authorManager.ChangeNameAsync(author, input.Name);
+ }
+
+ author.BirthDate = input.BirthDate;
+ author.ShortBio = input.ShortBio;
+
+ await _authorRepository.UpdateAsync(author);
+}
+````
+
+* `UpdateAsync` 需要额外的 `BookStorePermissions.Authors.Edit` 权限.
+* 使用 `IAuthorRepository.GetAsync` 从数据库中获得作者实体. 如果给定的id没有找到作者, `GetAsync` 抛出 `EntityNotFoundException`, 这在web应用程序中导致一个 `404` HTTP 状态码. 在更新操作中先获取实体再更新它, 是一个好的实践.
+* 如果客户端请求, 使用 `AuthorManager.ChangeNameAsync` (领域服务方法) 修改作者姓名.
+* 因为没有任何业务逻辑, 直接更新 `BirthDate` 和 `ShortBio`, 它们可以接受任何值.
+* 最后, 调用 `IAuthorRepository.UpdateAsync` 更新实体到数据库.
+
+{{if DB == "EF"}}
+
+> **EF Core 提示**: Entity Framework Core 拥有 **change tracking** 系统并在unit of work 结束时 **自动保存** 任何修改到实体 (你可以简单地认为APB框架在方法结束时自动调用 `SaveChanges`). 所以, 即使你在方法结束时没有调用 `_authorRepository.UpdateAsync(...)` , 它依然可以工作. 如果你不考虑以后修改EF Core, 你可以移除这一行.
+
+{{end}}
+
+### DeleteAsync
+
+````csharp
+[Authorize(BookStorePermissions.Authors.Delete)]
+public async Task DeleteAsync(Guid id)
+{
+ await _authorRepository.DeleteAsync(id);
+}
+````
+
+* `DeleteAsync` 需要额外的 `BookStorePermissions.Authors.Delete` 权限.
+* 直接使用repository的 `DeleteAsync` 方法.
+
+## 权限定义
+
+你还不能编译代码, 因为它需要 `BookStorePermissions` 类定义中一些常数.
+
+打开 `Acme.BookStore.Application.Contracts` 项目中的 `BookStorePermissions` 类 (在 `Permissions` 文件夹中), 修改为如下代码:
+
+````csharp
+namespace Acme.BookStore.Permissions
+{
+ public static class BookStorePermissions
+ {
+ public const string GroupName = "BookStore";
+
+ public static class Books
+ {
+ public const string Default = GroupName + ".Books";
+ public const string Create = Default + ".Create";
+ public const string Edit = Default + ".Edit";
+ public const string Delete = Default + ".Delete";
+ }
+
+ // *** ADDED a NEW NESTED CLASS ***
+ public static class Authors
+ {
+ public const string Default = GroupName + ".Authors";
+ public const string Create = Default + ".Create";
+ public const string Edit = Default + ".Edit";
+ public const string Delete = Default + ".Delete";
+ }
+ }
+}
+````
+
+然后打开同一项目中的 `BookStorePermissionDefinitionProvider`, 在 `Define` 方法的结尾加入以下行:
+
+````csharp
+var authorsPermission = bookStoreGroup.AddPermission(
+ BookStorePermissions.Authors.Default, L("Permission:Authors"));
+
+authorsPermission.AddChild(
+ BookStorePermissions.Authors.Create, L("Permission:Authors.Create"));
+
+authorsPermission.AddChild(
+ BookStorePermissions.Authors.Edit, L("Permission:Authors.Edit"));
+
+authorsPermission.AddChild(
+ BookStorePermissions.Authors.Delete, L("Permission:Authors.Delete"));
+````
+
+最后, 在 `Acme.BookStore.Domain.Shared` 项目中的 `Localization/BookStore/en.json` 加入以下项, 用以本地化权限名称:
+
+````csharp
+"Permission:Authors": "Author Management",
+"Permission:Authors.Create": "Creating new authors",
+"Permission:Authors.Edit": "Editing the authors",
+"Permission:Authors.Delete": "Deleting the authors"
+````
+
+## 对象到对象映射
+
+`AuthorAppService` 使用 `ObjectMapper` 将 `Author` 对象 转换为 `AuthorDto` 对象. 所以, 我们需要在 AutoMapper 配置中定义映射.
+
+打开 `Acme.BookStore.Application` 项目中的 `BookStoreApplicationAutoMapperProfile` 类, 加入以下行到构造函数:
+
+````csharp
+CreateMap();
+````
+
+## 种子数据
+
+如同图书管理部分所做的, 在数据库中生成一些初始作者实体. 不仅当第一次运行应用程序时是有用的, 对自动化测试也是很有用的.
+
+打开 `Acme.BookStore.Domain` 项目中的 `BookStoreDataSeederContributor`, 修改文件内容如下:
+
+````csharp
+using System;
+using System.Threading.Tasks;
+using Acme.BookStore.Authors;
+using Acme.BookStore.Books;
+using Volo.Abp.Data;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Domain.Repositories;
+
+namespace Acme.BookStore
+{
+ public class BookStoreDataSeederContributor
+ : IDataSeedContributor, ITransientDependency
+ {
+ private readonly IRepository _bookRepository;
+ private readonly IAuthorRepository _authorRepository;
+ private readonly AuthorManager _authorManager;
+
+ public BookStoreDataSeederContributor(
+ IRepository bookRepository,
+ IAuthorRepository authorRepository,
+ AuthorManager authorManager)
+ {
+ _bookRepository = bookRepository;
+ _authorRepository = authorRepository;
+ _authorManager = authorManager;
+ }
+
+ public async Task SeedAsync(DataSeedContext context)
+ {
+ if (await _bookRepository.GetCountAsync() <= 0)
+ {
+ await _bookRepository.InsertAsync(
+ new Book
+ {
+ Name = "1984",
+ Type = BookType.Dystopia,
+ PublishDate = new DateTime(1949, 6, 8),
+ Price = 19.84f
+ },
+ autoSave: true
+ );
+
+ await _bookRepository.InsertAsync(
+ new Book
+ {
+ Name = "The Hitchhiker's Guide to the Galaxy",
+ Type = BookType.ScienceFiction,
+ PublishDate = new DateTime(1995, 9, 27),
+ Price = 42.0f
+ },
+ autoSave: true
+ );
+ }
+
+ // ADDED SEED DATA FOR AUTHORS
+
+ if (await _authorRepository.GetCountAsync() <= 0)
+ {
+ await _authorRepository.InsertAsync(
+ await _authorManager.CreateAsync(
+ "George Orwell",
+ new DateTime(1903, 06, 25),
+ "Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)."
+ )
+ );
+
+ await _authorRepository.InsertAsync(
+ await _authorManager.CreateAsync(
+ "Douglas Adams",
+ new DateTime(1952, 03, 11),
+ "Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'."
+ )
+ );
+ }
+ }
+ }
+}
+````
+
+{{if DB=="EF"}}
+
+你现在可以运行 `.DbMigrator` 控制台应用程序, **迁移** **数据库 schema** 并生成 **种子** 初始数据.
+
+{{else if DB=="Mongo"}}
+
+你现在可以运行 `.DbMigrator` 控制台应用程序, **迁移** **数据库 schema** 并生成 **种子** 初始数据.
+
+{{end}}
+
+## 测试作者应用服务
+
+最后, 你可以为 `IAuthorAppService` 写一些测试. 在 `Acme.BookStore.Application.Tests` 项目的 `Authors` 命名空间(文件夹)中加入一个名为 `AuthorAppService_Tests` 新类:
+
+````csharp
+using System;
+using System.Threading.Tasks;
+using Shouldly;
+using Xunit;
+
+namespace Acme.BookStore.Authors
+{ {{if DB=="Mongo"}}
+ [Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}}
+ public class AuthorAppService_Tests : BookStoreApplicationTestBase
+ {
+ private readonly IAuthorAppService _authorAppService;
+
+ public AuthorAppService_Tests()
+ {
+ _authorAppService = GetRequiredService();
+ }
+
+ [Fact]
+ public async Task Should_Get_All_Authors_Without_Any_Filter()
+ {
+ var result = await _authorAppService.GetListAsync(new GetAuthorListDto());
+
+ result.TotalCount.ShouldBeGreaterThanOrEqualTo(2);
+ result.Items.ShouldContain(author => author.Name == "George Orwell");
+ result.Items.ShouldContain(author => author.Name == "Douglas Adams");
+ }
+
+ [Fact]
+ public async Task Should_Get_Filtered_Authors()
+ {
+ var result = await _authorAppService.GetListAsync(
+ new GetAuthorListDto {Filter = "George"});
+
+ result.TotalCount.ShouldBeGreaterThanOrEqualTo(1);
+ result.Items.ShouldContain(author => author.Name == "George Orwell");
+ result.Items.ShouldNotContain(author => author.Name == "Douglas Adams");
+ }
+
+ [Fact]
+ public async Task Should_Create_A_New_Author()
+ {
+ var authorDto = await _authorAppService.CreateAsync(
+ new CreateAuthorDto
+ {
+ Name = "Edward Bellamy",
+ BirthDate = new DateTime(1850, 05, 22),
+ ShortBio = "Edward Bellamy was an American author..."
+ }
+ );
+
+ authorDto.Id.ShouldNotBe(Guid.Empty);
+ authorDto.Name.ShouldBe("Edward Bellamy");
+ }
+
+ [Fact]
+ public async Task Should_Not_Allow_To_Create_Duplicate_Author()
+ {
+ await Assert.ThrowsAsync(async () =>
+ {
+ await _authorAppService.CreateAsync(
+ new CreateAuthorDto
+ {
+ Name = "Douglas Adams",
+ BirthDate = DateTime.Now,
+ ShortBio = "..."
+ }
+ );
+ });
+ }
+
+ //TODO: Test other methods...
+ }
+}
+````
+
+完成应用服务方法的测试, 它们应该很容易理解.
+
+## 下一章
+
+查看本教程的[下一章](Part-9.md).
diff --git a/docs/zh-Hans/Tutorials/Part-9.md b/docs/zh-Hans/Tutorials/Part-9.md
index 602306c484..2fbd4dcd55 100644
--- a/docs/zh-Hans/Tutorials/Part-9.md
+++ b/docs/zh-Hans/Tutorials/Part-9.md
@@ -1 +1,1249 @@
-TODO..
\ No newline at end of file
+# Web应用程序开发教程 - 第九章: 作者: 用户页面
+````json
+//[doc-params]
+{
+ "UI": ["MVC","Blazor","BlazorServer","NG"],
+ "DB": ["EF","Mongo"]
+}
+````
+## 关于本教程
+
+在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的:
+
+* **{{DB_Text}}** 做为ORM提供程序.
+* **{{UI_Value}}** 做为UI框架.
+
+本教程分为以下部分:
+
+- [Part 1: 创建服务端](Part-1.md)
+- [Part 2: 图书列表页面](Part-2.md)
+- [Part 3: 创建,更新和删除图书](Part-2.md)
+- [Part 4: 集成测试](Part-4.md)
+- [Part 5: 授权](Part-5.md)
+- [Part 6: 作者: 领域层](Part-6.md)
+- [Part 7: 作者: 数据库集成](Part-7.md)
+- [Part 8: 作者: 应用服务层](Part-8.md)
+- [Part 9: 作者: 用户页面](本章)
+- [Part 10: 图书到作者的关系](Part-10.md)
+
+## 下载源码
+
+本教程根据你的**UI** 和 **数据库**偏好有多个版本,我们准备了几种可供下载的源码组合:
+
+* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore)
+* [Blazor UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Blazor-EfCore)
+* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb)
+
+> 如果你在Windows中遇到 "文件名太长" or "解压错误", 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 [在Windows 10中启用长路径](https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later).
+
+> 如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path
+> `git config --system core.longpaths true`
+
+## 简介
+
+这章阐述如何为前一章介绍的 `作者` 实体创建CRUD页面.
+
+{{if UI == "MVC"}}
+
+## 作者列表页面
+
+在 `Acme.BookStore.Web` 项目的 `Pages/Authors` 文件夹下创建一个新的razor页面, `Index.cshtml`, 修改文件内容如下.
+
+### Index.cshtml
+
+````html
+@page
+@using Acme.BookStore.Localization
+@using Acme.BookStore.Permissions
+@using Acme.BookStore.Web.Pages.Authors
+@using Microsoft.AspNetCore.Authorization
+@using Microsoft.Extensions.Localization
+@inject IStringLocalizer L
+@inject IAuthorizationService AuthorizationService
+@model IndexModel
+
+@section scripts
+{
+
+}
+
+
+
+
+
+ @L["Authors"]
+
+
+ @if (await AuthorizationService
+ .IsGrantedAsync(BookStorePermissions.Authors.Create))
+ {
+
+ }
+
+
+
+
+
+
+
+````
+
+这是一个简单的页面, 和我们以前创建的图书页面一样. 它导入了一个JavaScript文件, 我们后面会进行介绍这个文件.
+
+### IndexModel.cshtml.cs
+
+````csharp
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace Acme.BookStore.Web.Pages.Authors
+{
+ public class IndexModel : PageModel
+ {
+ public void OnGet()
+ {
+
+ }
+ }
+}
+````
+
+### Index.js
+
+````js
+$(function () {
+ var l = abp.localization.getResource('BookStore');
+ var createModal = new abp.ModalManager(abp.appPath + 'Authors/CreateModal');
+ var editModal = new abp.ModalManager(abp.appPath + 'Authors/EditModal');
+
+ var dataTable = $('#AuthorsTable').DataTable(
+ abp.libs.datatables.normalizeConfiguration({
+ serverSide: true,
+ paging: true,
+ order: [[1, "asc"]],
+ searching: false,
+ scrollX: true,
+ ajax: abp.libs.datatables.createAjax(acme.bookStore.authors.author.getList),
+ columnDefs: [
+ {
+ title: l('Actions'),
+ rowAction: {
+ items:
+ [
+ {
+ text: l('Edit'),
+ visible:
+ abp.auth.isGranted('BookStore.Authors.Edit'),
+ action: function (data) {
+ editModal.open({ id: data.record.id });
+ }
+ },
+ {
+ text: l('Delete'),
+ visible:
+ abp.auth.isGranted('BookStore.Authors.Delete'),
+ confirmMessage: function (data) {
+ return l(
+ 'AuthorDeletionConfirmationMessage',
+ data.record.name
+ );
+ },
+ action: function (data) {
+ acme.bookStore.authors.author
+ .delete(data.record.id)
+ .then(function() {
+ abp.notify.info(
+ l('SuccessfullyDeleted')
+ );
+ dataTable.ajax.reload();
+ });
+ }
+ }
+ ]
+ }
+ },
+ {
+ title: l('Name'),
+ data: "name"
+ },
+ {
+ title: l('BirthDate'),
+ data: "birthDate",
+ render: function (data) {
+ return luxon
+ .DateTime
+ .fromISO(data, {
+ locale: abp.localization.currentCulture.name
+ }).toLocaleString();
+ }
+ }
+ ]
+ })
+ );
+
+ createModal.onResult(function () {
+ dataTable.ajax.reload();
+ });
+
+ editModal.onResult(function () {
+ dataTable.ajax.reload();
+ });
+
+ $('#NewAuthorButton').click(function (e) {
+ e.preventDefault();
+ createModal.open();
+ });
+});
+````
+
+简单来说, 这个JavaScript页面:
+
+* 创建了一个具有 `操作`, `姓名` 和 `生日` 列的数据表格.
+ * `Actions` 列用来添加 *编辑* 和 *删除* 操作.
+ * `生日` 提供了一个 `render` 函数, 使用 [luxon](https://moment.github.io/luxon/) 库格式化 `DateTime` 值.
+* 使用 `abp.ModalManager` 打开 *新建* 和 *编辑* 模态表单.
+
+这块代码与以前创建的图书页面非常相似, 所以我们不再赘述.
+
+### 本地化
+
+这个页面使用了一些需要声明的本地化键. 打开 `Acme.BookStore.Domain.Shared` 项目中 `Localization/BookStore` 文件夹下的 `en.json` 文件, 加入以下条目:
+
+````json
+"Menu:Authors": "Authors",
+"Authors": "Authors",
+"AuthorDeletionConfirmationMessage": "Are you sure to delete the author '{0}'?",
+"BirthDate": "Birth date",
+"NewAuthor": "New author"
+````
+
+注意我们加入了额外的键. 它们会在下面的小节中被使用.
+
+### 加入主菜单
+
+打开 `Acme.BookStore.Web` 项目的 `Menus` 文件夹中的 `BookStoreMenuContributor.cs` , 在 `ConfigureMainMenuAsync` 方法的结尾加入以下代码:
+
+````csharp
+if (await context.IsGrantedAsync(BookStorePermissions.Authors.Default))
+{
+ bookStoreMenu.AddItem(new ApplicationMenuItem(
+ "BooksStore.Authors",
+ l["Menu:Authors"],
+ url: "/Authors"
+ ));
+}
+````
+
+### 运行应用程序
+
+运行并登录应用程序. **因为你还没有权限, 所以不能看见菜单项**. 转到 `Identity/Roles` 页面, 点击 *操作* 按钮并选择**管理员角色**的*权限*操作:
+
+
+
+如你所见, 管理员角色还没有*作者管理*权限. 单击复选框并保存, 赋予权限. **刷新页面**后, 你会在主菜单中的*图书商店*下看到*作者*菜单项:
+
+
+
+页面是完全可以工作的, 除了 *新建作者* 和 *操作/编辑*, 因为它们还没有实现 .
+
+> **提示**: 如果你在定义一个新权限后运行 `.DbMigrator` 控制台程序, 它会自动将这些权限赋予管理员角色, 你不需要手工赋予权限.
+
+## 新建模态窗口
+
+在 `Acme.BookStore.Web` 项目的 `Pages/Authors` 文件夹下创建一个 razor 页面 `CreateModal.cshtml`, 修改它的内容如下:
+
+### CreateModal.cshtml
+
+```html
+@page
+@using Acme.BookStore.Localization
+@using Acme.BookStore.Web.Pages.Authors
+@using Microsoft.Extensions.Localization
+@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
+@model CreateModalModel
+@inject IStringLocalizer L
+@{
+ Layout = null;
+}
+
+```
+
+之前我们已经使用ABP框架的 [动态表单](../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md)开发了图书页面. 这里可以使用相同的方法, 但我们希望展示如何手工完成它. 实际上, 没有那么手工化, 因为在这个例子中我们使用了 `abp-input` 标签简化了表单元素的创建.
+
+你当然可以使用标准Bootstrap HTML结构, 但是这需要写很多代码. `abp-input` 自动添加验证, 本地化和根据数据类型生成标准元素.
+
+### CreateModal.cshtml.cs
+
+```csharp
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Threading.Tasks;
+using Acme.BookStore.Authors;
+using Microsoft.AspNetCore.Mvc;
+using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form;
+
+namespace Acme.BookStore.Web.Pages.Authors
+{
+ public class CreateModalModel : BookStorePageModel
+ {
+ [BindProperty]
+ public CreateAuthorViewModel Author { get; set; }
+
+ private readonly IAuthorAppService _authorAppService;
+
+ public CreateModalModel(IAuthorAppService authorAppService)
+ {
+ _authorAppService = authorAppService;
+ }
+
+ public void OnGet()
+ {
+ Author = new CreateAuthorViewModel();
+ }
+
+ public async Task OnPostAsync()
+ {
+ var dto = ObjectMapper.Map(Author);
+ await _authorAppService.CreateAsync(dto);
+ return NoContent();
+ }
+
+ public class CreateAuthorViewModel
+ {
+ [Required]
+ [StringLength(AuthorConsts.MaxNameLength)]
+ public string Name { get; set; }
+
+ [Required]
+ [DataType(DataType.Date)]
+ public DateTime BirthDate { get; set; }
+
+ [TextArea]
+ public string ShortBio { get; set; }
+ }
+ }
+}
+```
+
+这个页面模型类注入和使用 `IAuthorAppService` 创建新作者. 它和图书创建模型类之间主要的区别是这个模型类为视图模型声明了一个新类 `CreateAuthorViewModel`, 而不是重用 `CreateAuthorDto`.
+
+这么做的主要原因是展示在页面中如何使用不同的模型. 但还有一个好处: 我们为类成员添加了两个不存在于 `CreateAuthorDto` 中的特性:
+
+* 为 `BirthDate` 添加 `[DataType(DataType.Date)]` 特性, 这会在UI为这个属性显示一个日期选择控件.
+* 为 `ShortBio` 添加 `[TextArea]` 特性, 这会显示一个多行文本框, 而不是标准文本框.
+
+通过这种方式, 可以根据UI需求定制视图模型类, 而无需修改DTO. 这么做的一个结果是: 使用 `ObjectMapper` 将 `CreateAuthorViewModel` 映射到 `CreateAuthorDto`. 为了完成映射, 需要在 `BookStoreWebAutoMapperProfile` 构造函数中加入新的映射代码:
+
+````csharp
+using Acme.BookStore.Authors; // ADDED NAMESPACE IMPORT
+using Acme.BookStore.Books;
+using AutoMapper;
+
+namespace Acme.BookStore.Web
+{
+ public class BookStoreWebAutoMapperProfile : Profile
+ {
+ public BookStoreWebAutoMapperProfile()
+ {
+ CreateMap();
+
+ // ADD a NEW MAPPING
+ CreateMap();
+ }
+ }
+}
+````
+
+当你重新运行应用程序后, 点击"新建作者" 按钮会打开一个新的模态窗口.
+
+
+
+## 编辑模态窗口
+
+在 `Acme.BookStore.Web` 项目的 `Pages/Authors` 文件夹下创建一个 razor 页面 `EditModal.cshtml`, 修改它的内容如下:
+
+### EditModal.cshtml
+
+````html
+@page
+@using Acme.BookStore.Localization
+@using Acme.BookStore.Web.Pages.Authors
+@using Microsoft.Extensions.Localization
+@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
+@model EditModalModel
+@inject IStringLocalizer L
+@{
+ Layout = null;
+}
+
+````
+
+### EditModal.cshtml.cs
+
+```csharp
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Threading.Tasks;
+using Acme.BookStore.Authors;
+using Microsoft.AspNetCore.Mvc;
+using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form;
+
+namespace Acme.BookStore.Web.Pages.Authors
+{
+ public class EditModalModel : BookStorePageModel
+ {
+ [BindProperty]
+ public EditAuthorViewModel Author { get; set; }
+
+ private readonly IAuthorAppService _authorAppService;
+
+ public EditModalModel(IAuthorAppService authorAppService)
+ {
+ _authorAppService = authorAppService;
+ }
+
+ public async Task OnGetAsync(Guid id)
+ {
+ var authorDto = await _authorAppService.GetAsync(id);
+ Author = ObjectMapper.Map(authorDto);
+ }
+
+ public async Task OnPostAsync()
+ {
+ await _authorAppService.UpdateAsync(
+ Author.Id,
+ ObjectMapper.Map(Author)
+ );
+
+ return NoContent();
+ }
+
+ public class EditAuthorViewModel
+ {
+ [HiddenInput]
+ public Guid Id { get; set; }
+
+ [Required]
+ [StringLength(AuthorConsts.MaxNameLength)]
+ public string Name { get; set; }
+
+ [Required]
+ [DataType(DataType.Date)]
+ public DateTime BirthDate { get; set; }
+
+ [TextArea]
+ public string ShortBio { get; set; }
+ }
+ }
+}
+```
+
+这个类与 `CreateModal.cshtml.cs` 类似, 主要不同是:
+
+* 使用 `IAuthorAppService.GetAsync(...)` 方法从应用层获取正在编辑的作者.
+* `EditAuthorViewModel` 拥有一个额外的 `Id` 属性, 它被 `[HiddenInput]` 特性标记, 会为这个属性在页面上创建一个隐藏输入框.
+
+这个类要求在 `BookStoreWebAutoMapperProfile` 类中添加两个对象映射声明:
+
+```csharp
+using Acme.BookStore.Authors;
+using Acme.BookStore.Books;
+using AutoMapper;
+
+namespace Acme.BookStore.Web
+{
+ public class BookStoreWebAutoMapperProfile : Profile
+ {
+ public BookStoreWebAutoMapperProfile()
+ {
+ CreateMap();
+
+ CreateMap();
+
+ // ADD THESE NEW MAPPINGS
+ CreateMap();
+ CreateMap();
+ }
+ }
+}
+```
+
+这就是全部了! 你可以运行应用程序并尝试编辑一个作者.
+
+{{else if UI == "NG"}}
+
+## 作者管理页面
+
+运行下面的命令行在angular应用程序的根目录创建一个名为 `AuthorModule` 新模块:
+
+```bash
+yarn ng generate module author --module app --routing --route authors
+```
+
+这上命令应该产生以下输出:
+
+```bash
+> yarn ng generate module author --module app --routing --route authors
+
+yarn run v1.19.1
+$ ng generate module author --module app --routing --route authors
+CREATE src/app/author/author-routing.module.ts (344 bytes)
+CREATE src/app/author/author.module.ts (349 bytes)
+CREATE src/app/author/author.component.html (21 bytes)
+CREATE src/app/author/author.component.spec.ts (628 bytes)
+CREATE src/app/author/author.component.ts (276 bytes)
+CREATE src/app/author/author.component.scss (0 bytes)
+UPDATE src/app/app-routing.module.ts (1396 bytes)
+Done in 2.22s.
+```
+
+### AuthorModule
+
+打开 `/src/app/author/author.module.ts,` 替换成以下内容:
+
+```js
+import { NgModule } from '@angular/core';
+import { SharedModule } from '../shared/shared.module';
+import { AuthorRoutingModule } from './author-routing.module';
+import { AuthorComponent } from './author.component';
+import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap';
+
+@NgModule({
+ declarations: [AuthorComponent],
+ imports: [SharedModule, AuthorRoutingModule, NgbDatepickerModule],
+})
+export class AuthorModule {}
+```
+
+- 添加 `SharedModule`. `SharedModule` 导出一些创建用户页面需要的通用模块.
+- `SharedModule` 已经导出 `CommonModule`, 所以移除 `CommonModule`.
+- 添加 `NgbDatepickerModule`, 后面用于作者创建和编辑表单.
+
+### 菜单定义
+
+打开 `src/app/route.provider.ts` 文件, 加入以下的菜单定义:
+
+````js
+{
+ path: '/authors',
+ name: '::Menu:Authors',
+ parentName: '::Menu:BookStore',
+ layout: eLayoutType.application,
+ requiredPolicy: 'BookStore.Authors',
+}
+````
+
+最终的 `configureRoutes` 函数声明应该如下:
+
+```js
+function configureRoutes(routes: RoutesService) {
+ return () => {
+ routes.add([
+ {
+ path: '/',
+ name: '::Menu:Home',
+ iconClass: 'fas fa-home',
+ order: 1,
+ layout: eLayoutType.application,
+ },
+ {
+ path: '/book-store',
+ name: '::Menu:BookStore',
+ iconClass: 'fas fa-book',
+ order: 2,
+ layout: eLayoutType.application,
+ },
+ {
+ path: '/books',
+ name: '::Menu:Books',
+ parentName: '::Menu:BookStore',
+ layout: eLayoutType.application,
+ requiredPolicy: 'BookStore.Books',
+ },
+ {
+ path: '/authors',
+ name: '::Menu:Authors',
+ parentName: '::Menu:BookStore',
+ layout: eLayoutType.application,
+ requiredPolicy: 'BookStore.Authors',
+ },
+ ]);
+ };
+}
+```
+
+### 生成服务代理
+
+[ABP CLI](https://docs.abp.io/en/abp/latest/CLI) 提供 `generate-proxy` 命令为HTTP APIs生成客户端代理, 使得使用HTTP APIs更容易. `generate-proxy` 命令运行前, 应用程序必须启动运行.
+
+在 `angular` 文件夹运行以下命令:
+
+```bash
+abp generate-proxy -t ng
+```
+
+这个命令为作者服务和相关模型(DTO)类生成服务代理:
+
+
+
+### AuthorComponent
+
+打开 `/src/app/author/author.component.ts` 文件, 替换成以下内容:
+
+```js
+import { Component, OnInit } from '@angular/core';
+import { ListService, PagedResultDto } from '@abp/ng.core';
+import { AuthorService, AuthorDto } from '@proxy/authors';
+import { FormGroup, FormBuilder, Validators } from '@angular/forms';
+import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
+import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared';
+
+@Component({
+ selector: 'app-author',
+ templateUrl: './author.component.html',
+ styleUrls: ['./author.component.scss'],
+ providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
+})
+export class AuthorComponent implements OnInit {
+ author = { items: [], totalCount: 0 } as PagedResultDto;
+
+ isModalOpen = false;
+
+ form: FormGroup;
+
+ selectedAuthor = {} as AuthorDto;
+
+ constructor(
+ public readonly list: ListService,
+ private authorService: AuthorService,
+ private fb: FormBuilder,
+ private confirmation: ConfirmationService
+ ) {}
+
+ ngOnInit(): void {
+ const authorStreamCreator = (query) => this.authorService.getList(query);
+
+ this.list.hookToQuery(authorStreamCreator).subscribe((response) => {
+ this.author = response;
+ });
+ }
+
+ createAuthor() {
+ this.selectedAuthor = {} as AuthorDto;
+ this.buildForm();
+ this.isModalOpen = true;
+ }
+
+ editAuthor(id: string) {
+ this.authorService.get(id).subscribe((author) => {
+ this.selectedAuthor = author;
+ this.buildForm();
+ this.isModalOpen = true;
+ });
+ }
+
+ buildForm() {
+ this.form = this.fb.group({
+ name: [this.selectedAuthor.name || '', Validators.required],
+ birthDate: [
+ this.selectedAuthor.birthDate ? new Date(this.selectedAuthor.birthDate) : null,
+ Validators.required,
+ ],
+ });
+ }
+
+ save() {
+ if (this.form.invalid) {
+ return;
+ }
+
+ if (this.selectedAuthor.id) {
+ this.authorService
+ .update(this.selectedAuthor.id, this.form.value)
+ .subscribe(() => {
+ this.isModalOpen = false;
+ this.form.reset();
+ this.list.get();
+ });
+ } else {
+ this.authorService.create(this.form.value).subscribe(() => {
+ this.isModalOpen = false;
+ this.form.reset();
+ this.list.get();
+ });
+ }
+ }
+
+ delete(id: string) {
+ this.confirmation.warn('::AreYouSureToDelete', '::AreYouSure')
+ .subscribe((status) => {
+ if (status === Confirmation.Status.confirm) {
+ this.authorService.delete(id).subscribe(() => this.list.get());
+ }
+ });
+ }
+}
+```
+
+打开 `/src/app/author/author.component.html` 文件, 替换成以下内容:
+
+````html
+
+
+
+
+
+
+
+
+ {%{{{ '::Actions' | abpLocalization }}}%}
+
+
+
+ {%{{{ '::Edit' | abpLocalization }}}%}
+
+
+ {%{{{ '::Delete' | abpLocalization }}}%}
+
+
+
+
+
+
+
+
+ {%{{{ row.birthDate | date }}}%}
+
+
+
+
+
+
+
+
+ {%{{{ (selectedAuthor.id ? '::Edit' : '::NewAuthor') | abpLocalization }}}%}
+
+
+
+
+
+
+
+
+ {%{{{ '::Close' | abpLocalization }}}%}
+
+
+
+
+ {%{{{ '::Save' | abpLocalization }}}%}
+
+
+
+````
+
+### 本地化
+
+这个页面使用了一些需要声明的本地化键. 打开 `Acme.BookStore.Domain.Shared` 项目中 `Localization/BookStore` 文件夹下的 `en.json` 文件, 加入以下条目:
+
+````json
+"Menu:Authors": "Authors",
+"Authors": "Authors",
+"AuthorDeletionConfirmationMessage": "Are you sure to delete the author '{0}'?",
+"BirthDate": "Birth date",
+"NewAuthor": "New author"
+````
+
+### 运行应用程序
+
+运行并登录应用程序. **因为你还没有权限, 所以不能看见菜单项**. 转到 `Identity/Roles` 页面, 点击 *操作* 按钮并选择**管理员角色**的*权限*操作:
+
+
+
+如你所见, 管理员角色还没有*作者管理*权限. 单击复选框并保存, 赋予权限. **刷新页面**后, 你会在主菜单中的*图书商店*下看到*作者*菜单项:
+
+
+
+这就是全部了! 这是一个完整的, 可以工作的页面. 你可以新建, 编辑和删除作者.
+
+> **提示**: 如果你在定义一个新权限后运行 `.DbMigrator` 控制台程序, 它会自动将这些权限赋予管理员角色, 你不需要手工赋予权限.
+
+{{end}}
+
+{{if UI == "Blazor" || UI == "BlazorServer"}}
+
+## 作者管理页面
+
+### 作者Razor组件
+
+在 `Acme.BookStore.Blazor` 项目中新建一个 Razor组件页面 `/Pages/Authors.razor`, 替换成以下内容:
+
+````xml
+@page "/authors"
+@using Acme.BookStore.Authors
+@using Acme.BookStore.Localization
+@using Volo.Abp.AspNetCore.Components.Web
+@inherits BookStoreComponentBase
+@inject IAuthorAppService AuthorAppService
+@inject AbpBlazorMessageLocalizerHelper LH
+
+
+
+
+ @L["Authors"]
+
+
+
+ @if (CanCreateAuthor)
+ {
+
+ @L["NewAuthor"]
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+ @L["Actions"]
+
+
+ @if (CanEditAuthor)
+ {
+
+ @L["Edit"]
+
+ }
+ @if (CanDeleteAuthor)
+ {
+
+ @L["Delete"]
+
+ }
+
+
+
+
+
+
+
+ @context.BirthDate.ToShortDateString()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+````
+
+* 这些代码类似 `Books.razor`, 除了不继承自 `AbpCrudPageBase`, 它使用自己的实现.
+* 注入 `IAuthorAppService` , 从UI使用服务器端的HTTP APIs . 我们可以直接注入应用服务接口并在 [动态 C# HTTP API 客户端代理系统](../API/Dynamic-CSharp-API-Clients.md)的帮助下像使用普通的方法一样使用它们, 动态 C# HTTP API 客户端代理系统会为我们调用REST API. 参考下面的 `Authors` 类获得使用方法.
+* 注入 `IAuthorizationService` 检查 [权限](../Authorization.md).
+* 注入 `IObjectMapper` 进行 [对象到对象映射](../Object-To-Object-Mapping.md).
+
+在 `Pages` 文件夹下新建一个代码后置文件 `Authors.razor.cs`, 使用以下代码:
+
+````csharp
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Acme.BookStore.Authors;
+using Acme.BookStore.Permissions;
+using Blazorise;
+using Blazorise.DataGrid;
+using Microsoft.AspNetCore.Authorization;
+using Volo.Abp.Application.Dtos;
+
+namespace Acme.BookStore.Blazor.Pages
+{
+ public partial class Authors
+ {
+ private IReadOnlyList AuthorList { get; set; }
+
+ private int PageSize { get; } = LimitedResultRequestDto.DefaultMaxResultCount;
+ private int CurrentPage { get; set; }
+ private string CurrentSorting { get; set; }
+ private int TotalCount { get; set; }
+
+ private bool CanCreateAuthor { get; set; }
+ private bool CanEditAuthor { get; set; }
+ private bool CanDeleteAuthor { get; set; }
+
+ private CreateAuthorDto NewAuthor { get; set; }
+
+ private Guid EditingAuthorId { get; set; }
+ private UpdateAuthorDto EditingAuthor { get; set; }
+
+ private Modal CreateAuthorModal { get; set; }
+ private Modal EditAuthorModal { get; set; }
+
+ private Validations CreateValidationsRef;
+
+ private Validations EditValidationsRef;
+
+ public Authors()
+ {
+ NewAuthor = new CreateAuthorDto();
+ EditingAuthor = new UpdateAuthorDto();
+ }
+
+ protected override async Task OnInitializedAsync()
+ {
+ await SetPermissionsAsync();
+ await GetAuthorsAsync();
+ }
+
+ private async Task SetPermissionsAsync()
+ {
+ CanCreateAuthor = await AuthorizationService
+ .IsGrantedAsync(BookStorePermissions.Authors.Create);
+
+ CanEditAuthor = await AuthorizationService
+ .IsGrantedAsync(BookStorePermissions.Authors.Edit);
+
+ CanDeleteAuthor = await AuthorizationService
+ .IsGrantedAsync(BookStorePermissions.Authors.Delete);
+ }
+
+ private async Task GetAuthorsAsync()
+ {
+ var result = await AuthorAppService.GetListAsync(
+ new GetAuthorListDto
+ {
+ MaxResultCount = PageSize,
+ SkipCount = CurrentPage * PageSize,
+ Sorting = CurrentSorting
+ }
+ );
+
+ AuthorList = result.Items;
+ TotalCount = (int)result.TotalCount;
+ }
+
+ private async Task OnDataGridReadAsync(DataGridReadDataEventArgs e)
+ {
+ CurrentSorting = e.Columns
+ .Where(c => c.Direction != SortDirection.None)
+ .Select(c => c.Field + (c.Direction == SortDirection.Descending ? " DESC" : ""))
+ .JoinAsString(",");
+ CurrentPage = e.Page - 1;
+
+ await GetAuthorsAsync();
+
+ await InvokeAsync(StateHasChanged);
+ }
+
+ private void OpenCreateAuthorModal()
+ {
+ CreateValidationsRef.ClearAll();
+
+ NewAuthor = new CreateAuthorDto();
+ CreateAuthorModal.Show();
+ }
+
+ private void CloseCreateAuthorModal()
+ {
+ CreateAuthorModal.Hide();
+ }
+
+ private void OpenEditAuthorModal(AuthorDto author)
+ {
+ EditValidationsRef.ClearAll();
+
+ EditingAuthorId = author.Id;
+ EditingAuthor = ObjectMapper.Map(author);
+ EditAuthorModal.Show();
+ }
+
+ private async Task DeleteAuthorAsync(AuthorDto author)
+ {
+ var confirmMessage = L["AuthorDeletionConfirmationMessage", author.Name];
+ if (!await Message.Confirm(confirmMessage))
+ {
+ return;
+ }
+
+ await AuthorAppService.DeleteAsync(author.Id);
+ await GetAuthorsAsync();
+ }
+
+ private void CloseEditAuthorModal()
+ {
+ EditAuthorModal.Hide();
+ }
+
+ private async Task CreateAuthorAsync()
+ {
+ if (CreateValidationsRef.ValidateAll())
+ {
+ await AuthorAppService.CreateAsync(NewAuthor);
+ await GetAuthorsAsync();
+ CreateAuthorModal.Hide();
+ }
+ }
+
+ private async Task UpdateAuthorAsync()
+ {
+ if (EditValidationsRef.ValidateAll())
+ {
+ await AuthorAppService.UpdateAsync(EditingAuthorId, EditingAuthor);
+ await GetAuthorsAsync();
+ EditAuthorModal.Hide();
+ }
+ }
+ }
+}
+````
+
+这个类定义了 `Authors.razor` 页面使用的属性和方法.
+
+### 对象映射
+
+`Authors` 类使用 `OpenEditAuthorModal` 方法中的 `IObjectMapper`. 所以需要定义这个映射.
+
+打开 `Acme.BookStore.Blazor` 项目中的 `BookStoreBlazorAutoMapperProfile.cs`, 在构造函数中加入一个映射:
+
+````csharp
+CreateMap();
+````
+
+你在文件开头需要 `using Acme.BookStore.Authors;` 声明语句.
+
+### 加入主菜单
+
+打开 `Acme.BookStore.Blazor` 项目的 `Menus` 文件夹中的 `BookStoreMenuContributor.cs` , 在 `ConfigureMainMenuAsync` 方法的结尾加入以下代码:
+
+````csharp
+if (await context.IsGrantedAsync(BookStorePermissions.Authors.Default))
+{
+ bookStoreMenu.AddItem(new ApplicationMenuItem(
+ "BooksStore.Authors",
+ l["Menu:Authors"],
+ url: "/authors"
+ ));
+}
+````
+
+### 本地化
+
+我们需要本地化上面的代码. 打开 `Acme.BookStore.Domain.Shared` 项目中 `Localization/BookStore` 文件夹下的 `en.json` 文件, 加入以下条目:
+
+````json
+"Menu:Authors": "Authors",
+"Authors": "Authors",
+"AuthorDeletionConfirmationMessage": "Are you sure to delete the author '{0}'?",
+"BirthDate": "Birth date",
+"NewAuthor": "New author"
+````
+
+### 运行应用程序
+
+运行并登录应用程序. **因为你还没有权限, 所以不能看见菜单项**. 转到 `Identity/Roles` 页面, 点击 *操作* 按钮并选择**管理员角色**的*权限*操作:
+
+
+
+如你所见, 管理员角色还没有*作者管理*权限. 单击复选框并保存, 赋予权限. **刷新页面**后, 你会在主菜单中的*图书商店*下看到*作者*菜单项:
+
+
+
+这就是全部了! 这是一个完整的, 可以工作的页面. 你可以新建, 编辑和删除作者.
+
+> **提示**: 如果你在定义一个新权限后运行 `.DbMigrator` 控制台程序, 它会自动将这些权限赋予管理员角色, 你不需要手工赋予权限.
+
+{{end}}
+
+## 下一章
+
+查看本教程的[下一章](Part-10.md).
diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json
index 74a44c8445..f63354c194 100644
--- a/docs/zh-Hans/docs-nav.json
+++ b/docs/zh-Hans/docs-nav.json
@@ -692,7 +692,7 @@
},
{
"text": "CMS Kit",
- "path": "Modules/Cms-Kit.md"
+ "path": "Modules/Cms-Kit/Index.md"
},
{
"text": "文档",
diff --git a/docs/zh-Hans/images/features-modal.png b/docs/zh-Hans/images/features-modal.png
index 74ee8aabc3..0fce7ecf5c 100644
Binary files a/docs/zh-Hans/images/features-modal.png and b/docs/zh-Hans/images/features-modal.png differ
diff --git a/docs/zh-Hans/images/module-tenant-management-actions.png b/docs/zh-Hans/images/module-tenant-management-actions.png
new file mode 100644
index 0000000000..899e731f8c
Binary files /dev/null and b/docs/zh-Hans/images/module-tenant-management-actions.png differ
diff --git a/docs/zh-Hans/images/module-tenant-management-new-tenant.png b/docs/zh-Hans/images/module-tenant-management-new-tenant.png
new file mode 100644
index 0000000000..7f0f50b42a
Binary files /dev/null and b/docs/zh-Hans/images/module-tenant-management-new-tenant.png differ
diff --git a/docs/zh-Hans/images/module-tenant-management-page.png b/docs/zh-Hans/images/module-tenant-management-page.png
new file mode 100644
index 0000000000..5ed6cc63bd
Binary files /dev/null and b/docs/zh-Hans/images/module-tenant-management-page.png differ
diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageHeader.razor.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageHeader.razor.cs
index 594909cd52..fd6bd50942 100644
--- a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageHeader.razor.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageHeader.razor.cs
@@ -37,7 +37,7 @@ public partial class PageHeader : ComponentBase
ToolbarItemRenders = new List();
}
- protected override async Task OnParametersSetAsync()
+ protected async override Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
if (Toolbar != null)
@@ -65,7 +65,7 @@ public partial class PageHeader : ComponentBase
}
}
- protected override async Task OnInitializedAsync()
+ protected async override Task OnInitializedAsync()
{
await base.OnInitializedAsync();
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Bootstrap/BootstrapScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Bootstrap/BootstrapScriptContributor.cs
index b79506352b..01850ccaf9 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Bootstrap/BootstrapScriptContributor.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Bootstrap/BootstrapScriptContributor.cs
@@ -12,5 +12,10 @@ public class BootstrapScriptContributor : BundleContributor
{
context.Files.AddIfNotContains("/libs/bootstrap/js/bootstrap.bundle.js");
context.Files.AddIfNotContains("/libs/bootstrap/js/bootstrap.enable.tooltips.everywhere.js");
+
+ if (context.FileProvider.GetFileInfo("/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js").Exists)
+ {
+ context.Files.AddIfNotContains("/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js");
+ }
}
}
diff --git a/framework/src/Volo.Abp.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperConventionalRegistrar.cs b/framework/src/Volo.Abp.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperConventionalRegistrar.cs
index 6c4a88d999..39c21e2ca7 100644
--- a/framework/src/Volo.Abp.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperConventionalRegistrar.cs
+++ b/framework/src/Volo.Abp.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperConventionalRegistrar.cs
@@ -1,7 +1,6 @@
using System;
using System.Linq;
using AutoMapper;
-using AutoMapper.Internal;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.DependencyInjection;
@@ -19,7 +18,8 @@ public class AbpAutoMapperConventionalRegistrar : DefaultConventionalRegistrar
protected override bool IsConventionalRegistrationDisabled(Type type)
{
- return !OpenTypes.Any(type.ImplementsGenericInterface) || base.IsConventionalRegistrationDisabled(type);
+ return !type.GetInterfaces().Any(x => x.IsGenericType && OpenTypes.Contains(x.GetGenericTypeDefinition())) ||
+ base.IsConventionalRegistrationDisabled(type);
}
protected override ServiceLifetime? GetDefaultLifeTimeOrNull(Type type)
diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs
index 33e0ebde4c..8449dc3df0 100644
--- a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs
+++ b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs
@@ -4,6 +4,7 @@ using System.Threading;
using System.Threading.Tasks;
using Hangfire;
using Volo.Abp.DependencyInjection;
+using Volo.Abp.DynamicProxy;
using Volo.Abp.Threading;
namespace Volo.Abp.BackgroundWorkers.Hangfire;
@@ -25,14 +26,14 @@ public class HangfireBackgroundWorkerManager : IBackgroundWorkerManager, ISingle
{
if (worker is IHangfireBackgroundWorker hangfireBackgroundWorker)
{
+ var unProxyWorker = ProxyHelper.UnProxy(hangfireBackgroundWorker);
if (hangfireBackgroundWorker.RecurringJobId.IsNullOrWhiteSpace())
{
- RecurringJob.AddOrUpdate(() => hangfireBackgroundWorker.DoWorkAsync(),
- hangfireBackgroundWorker.CronExpression);
+ RecurringJob.AddOrUpdate(() => ((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(),hangfireBackgroundWorker.CronExpression);
}
else
{
- RecurringJob.AddOrUpdate(hangfireBackgroundWorker.RecurringJobId,() => hangfireBackgroundWorker.DoWorkAsync(),
+ RecurringJob.AddOrUpdate(hangfireBackgroundWorker.RecurringJobId,() => ((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(),
hangfireBackgroundWorker.CronExpression);
}
}
@@ -64,7 +65,7 @@ public class HangfireBackgroundWorkerManager : IBackgroundWorkerManager, ISingle
return Task.CompletedTask;
}
- var adapterType = typeof(HangfirePeriodicBackgroundWorkerAdapter<>).MakeGenericType(worker.GetType());
+ var adapterType = typeof(HangfirePeriodicBackgroundWorkerAdapter<>).MakeGenericType(ProxyHelper.GetUnProxiedType(worker));
var workerAdapter = Activator.CreateInstance(adapterType) as IHangfireBackgroundWorker;
RecurringJob.AddOrUpdate(() => workerAdapter.DoWorkAsync(), GetCron(period.Value));
diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs
index 954f21bcda..060afd3ac6 100644
--- a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs
+++ b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs
@@ -1,8 +1,9 @@
-using System;
+using System;
using System.Threading;
using System.Threading.Tasks;
using Quartz;
using Volo.Abp.DependencyInjection;
+using Volo.Abp.DynamicProxy;
using Volo.Abp.Threading;
namespace Volo.Abp.BackgroundWorkers.Quartz;
@@ -56,7 +57,7 @@ public class QuartzBackgroundWorkerManager : IBackgroundWorkerManager, ISingleto
}
else
{
- var adapterType = typeof(QuartzPeriodicBackgroundWorkerAdapter<>).MakeGenericType(worker.GetType());
+ var adapterType = typeof(QuartzPeriodicBackgroundWorkerAdapter<>).MakeGenericType(ProxyHelper.GetUnProxiedType(worker));
var workerAdapter = Activator.CreateInstance(adapterType) as IQuartzBackgroundWorkerAdapter;
diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzPeriodicBackgroundWorkerAdapter.cs b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzPeriodicBackgroundWorkerAdapter.cs
index 7361fdfeb9..e0477f7106 100644
--- a/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzPeriodicBackgroundWorkerAdapter.cs
+++ b/framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzPeriodicBackgroundWorkerAdapter.cs
@@ -1,7 +1,8 @@
-using System;
+using System;
using System.Reflection;
using System.Threading.Tasks;
using Quartz;
+using Volo.Abp.DynamicProxy;
using Volo.Abp.Threading;
namespace Volo.Abp.BackgroundWorkers.Quartz;
@@ -26,16 +27,16 @@ public class QuartzPeriodicBackgroundWorkerAdapter : QuartzBackgroundWo
public void BuildWorker(IBackgroundWorker worker)
{
int? period;
- var workerType = worker.GetType();
+ var workerType = ProxyHelper.GetUnProxiedType(worker);
if (worker is AsyncPeriodicBackgroundWorkerBase or PeriodicBackgroundWorkerBase)
{
- if (typeof(TWorker) != worker.GetType())
+ if (typeof(TWorker) != workerType)
{
throw new ArgumentException($"{nameof(worker)} type is different from the generic type");
}
- var timer = worker.GetType().GetProperty("Timer", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(worker);
+ var timer = workerType.GetProperty("Timer", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(worker);
if (worker is AsyncPeriodicBackgroundWorkerBase)
{
diff --git a/framework/src/Volo.Abp.BlazoriseUI/AbpCrudPageBase.cs b/framework/src/Volo.Abp.BlazoriseUI/AbpCrudPageBase.cs
index e2b188eec3..f5d06f5ba1 100644
--- a/framework/src/Volo.Abp.BlazoriseUI/AbpCrudPageBase.cs
+++ b/framework/src/Volo.Abp.BlazoriseUI/AbpCrudPageBase.cs
@@ -212,7 +212,7 @@ public abstract class AbpCrudPageBase<
EntityActions = new EntityActionDictionary();
}
- protected override async Task OnInitializedAsync()
+ protected async override Task OnInitializedAsync()
{
await SetPermissionsAsync();
await SetEntityActionsAsync();
@@ -220,7 +220,7 @@ public abstract class AbpCrudPageBase<
await InvokeAsync(StateHasChanged);
}
- protected override async Task OnAfterRenderAsync(bool firstRender)
+ protected async override Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
@@ -350,6 +350,7 @@ public abstract class AbpCrudPageBase<
protected virtual Task CloseCreateModalAsync()
{
+ NewEntity = new TCreateViewModel();
return InvokeAsync(CreateModal.Hide);
}
@@ -464,6 +465,7 @@ public abstract class AbpCrudPageBase<
protected virtual async Task OnCreatedEntityAsync()
{
+ NewEntity = new TCreateViewModel();
await GetEntitiesAsync();
await InvokeAsync(CreateModal.Hide);
diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/CheckExtensionProperty.razor b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/CheckExtensionProperty.razor
index ed9f1664fe..eebff3970d 100644
--- a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/CheckExtensionProperty.razor
+++ b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/CheckExtensionProperty.razor
@@ -2,10 +2,20 @@
@typeparam TResourceType
@using Volo.Abp.BlazoriseUI
@using Volo.Abp.Localization
+@inherits ExtensionPropertyComponentBase
@if (PropertyInfo != null && Entity != null)
{
-
- @PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)
-
-}
\ No newline at end of file
+
+
+
+
+ @PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)
+
+
+
+
+
+
+
+}
diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/CheckExtensionProperty.razor.cs b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/CheckExtensionProperty.razor.cs
index 4f8a06a146..79662ab96c 100644
--- a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/CheckExtensionProperty.razor.cs
+++ b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/CheckExtensionProperty.razor.cs
@@ -1,22 +1,10 @@
-using Microsoft.AspNetCore.Components;
-using Microsoft.Extensions.Localization;
-using Volo.Abp.Data;
-using Volo.Abp.ObjectExtending;
+using Volo.Abp.Data;
namespace Volo.Abp.BlazoriseUI.Components.ObjectExtending;
-public partial class CheckExtensionProperty : ComponentBase
+public partial class CheckExtensionProperty
where TEntity : IHasExtraProperties
{
- [Inject]
- public IStringLocalizerFactory StringLocalizerFactory { get; set; }
-
- [Parameter]
- public TEntity Entity { get; set; }
-
- [Parameter]
- public ObjectExtensionPropertyInfo PropertyInfo { get; set; }
-
protected bool Value {
get {
return PropertyInfo.GetInputValueOrDefault(Entity.GetProperty(PropertyInfo.Name));
diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/DateTimeExtensionProperty.razor b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/DateTimeExtensionProperty.razor
index a9847d8e33..8fa7f0d777 100644
--- a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/DateTimeExtensionProperty.razor
+++ b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/DateTimeExtensionProperty.razor
@@ -3,15 +3,21 @@
@using Volo.Abp.BlazoriseUI
@using Volo.Abp.Localization
@using Volo.Abp.ObjectExtending
+@inherits ExtensionPropertyComponentBase
@if (PropertyInfo != null && Entity != null)
{
-
- @PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)
-
-
-
+
+
+ @PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)
+
+
+
+
+
+
+
}
diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/DateTimeExtensionProperty.razor.cs b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/DateTimeExtensionProperty.razor.cs
index 30a8ecc1a1..9a39b7a02d 100644
--- a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/DateTimeExtensionProperty.razor.cs
+++ b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/DateTimeExtensionProperty.razor.cs
@@ -5,23 +5,16 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
+using Blazorise;
+using Volo.Abp.AspNetCore.Components.Web;
using Volo.Abp.Data;
using Volo.Abp.ObjectExtending;
namespace Volo.Abp.BlazoriseUI.Components.ObjectExtending;
-public partial class DateTimeExtensionProperty : ComponentBase
+public partial class DateTimeExtensionProperty
where TEntity : IHasExtraProperties
{
- [Inject]
- public IStringLocalizerFactory StringLocalizerFactory { get; set; }
-
- [Parameter]
- public TEntity Entity { get; set; }
-
- [Parameter]
- public ObjectExtensionPropertyInfo PropertyInfo { get; set; }
-
protected DateTime? Value {
get {
return PropertyInfo.GetInputValueOrDefault(Entity.GetProperty(PropertyInfo.Name));
diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/ExtensionProperties.razor b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/ExtensionProperties.razor
index f8aad7b5cd..9fe96c7a20 100644
--- a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/ExtensionProperties.razor
+++ b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/ExtensionProperties.razor
@@ -1,7 +1,6 @@
@typeparam TEntityType
@typeparam TResourceType
@using Volo.Abp.ObjectExtending
-@using Volo.Abp.Localization
@using Volo.Abp.Data
@{
@@ -13,11 +12,11 @@
{
if (propertyInfo.Type.IsEnum)
{
-
+
}
else if (!propertyInfo.Lookup.Url.IsNullOrEmpty())
{
-
+
}
else
{
@@ -25,6 +24,7 @@
__builder.OpenComponent(0, inputType.MakeGenericType(new[] { typeof(TEntityType), typeof(TResourceType) }));
__builder.AddAttribute(1, "PropertyInfo", propertyInfo);
__builder.AddAttribute(2, "Entity", Entity);
+ __builder.AddAttribute(3, "LH", LH);
__builder.CloseComponent();
}
}
diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/ExtensionPropertyComponentBase.cs b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/ExtensionPropertyComponentBase.cs
new file mode 100644
index 0000000000..4408741fef
--- /dev/null
+++ b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/ExtensionPropertyComponentBase.cs
@@ -0,0 +1,83 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Blazorise;
+using Blazorise.Utilities;
+using Microsoft.AspNetCore.Components;
+using Microsoft.Extensions.Localization;
+using Volo.Abp.AspNetCore.Components.Web;
+using Volo.Abp.Data;
+using Volo.Abp.ObjectExtending;
+
+namespace Volo.Abp.BlazoriseUI.Components.ObjectExtending;
+
+public abstract class ExtensionPropertyComponentBase : OwningComponentBase
+ where TEntity : IHasExtraProperties
+{
+ [Inject]
+ public IStringLocalizerFactory StringLocalizerFactory { get; set; }
+
+ [Inject]
+ public IValidationMessageLocalizerAttributeFinder ValidationMessageLocalizerAttributeFinder { get; set; }
+
+ [Parameter]
+ public TEntity Entity { get; set; }
+
+ [Parameter]
+ public ObjectExtensionPropertyInfo PropertyInfo { get; set; }
+
+ [Parameter]
+ public AbpBlazorMessageLocalizerHelper LH { get; set; }
+
+ protected virtual void Validate(ValidatorEventArgs e)
+ {
+ e.Status = ValidationStatus.Success;
+
+ var validationAttributes = PropertyInfo.GetValidationAttributes();
+ var validationContext = new ValidationContext(Entity)
+ {
+ DisplayName = PropertyInfo.Name,
+ MemberName = PropertyInfo.Name
+ };
+
+ foreach (var validationAttribute in validationAttributes)
+ {
+ var result = validationAttribute.GetValidationResult(e.Value, validationContext);
+ if (result == ValidationResult.Success || result == null)
+ {
+ continue;
+ }
+
+ var errorMessage = result.ErrorMessage;
+ if (LH != null)
+ {
+ var formattedErrorMessage = GetDefaultErrorMessage(validationAttribute);
+ var errorMessageString = ValidationAttributeHelper.RevertErrorMessagePlaceholders(formattedErrorMessage);
+ var errorMessageArguments = ValidationMessageLocalizerAttributeFinder.FindAll(errorMessage, errorMessageString)
+ ?.OrderBy(x => x.Index)
+ ?.Select(x => x.Argument);
+
+ errorMessage = LH.Localize(errorMessageString, errorMessageArguments);
+ }
+
+ e.MemberNames = result.MemberNames;
+ e.Status = ValidationStatus.Error;
+ e.ErrorText = errorMessage;
+ break;
+ }
+ }
+
+ private static string GetDefaultErrorMessage(ValidationAttribute validationAttribute)
+ {
+ if (validationAttribute is StringLengthAttribute stringLengthAttribute && stringLengthAttribute.MinimumLength != 0)
+ {
+ var nullable = ValidationAttributeHelper.ValidationAttributeCustomErrorMessageSetProperty.GetValue((object) validationAttribute) as bool?;
+ var flag = true;
+ if (!(nullable.GetValueOrDefault() == flag & nullable.HasValue))
+ {
+ return ValidationAttributeHelper.SetErrorMessagePlaceholders("The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.");
+ }
+ }
+ return ValidationAttributeHelper.SetErrorMessagePlaceholders(ValidationAttributeHelper.ValidationAttributeErrorMessageStringProperty.GetValue((object) validationAttribute) as string);
+ }
+}
diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/LookupExtensionProperty.razor b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/LookupExtensionProperty.razor
index 4a305d606d..52369d6b46 100644
--- a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/LookupExtensionProperty.razor
+++ b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/LookupExtensionProperty.razor
@@ -2,18 +2,20 @@
@typeparam TResourceType
@using Abp.Localization
@using Blazorise.Components
+@inherits ExtensionPropertyComponentBase
@PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)
-
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/LookupExtensionProperty.razor.cs b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/LookupExtensionProperty.razor.cs
index 725edf8e99..c21db1352d 100644
--- a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/LookupExtensionProperty.razor.cs
+++ b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/LookupExtensionProperty.razor.cs
@@ -9,6 +9,8 @@ using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
+using Blazorise;
+using Volo.Abp.AspNetCore.Components.Web;
using Volo.Abp.AspNetCore.Components.Web.Extensibility;
using Volo.Abp.Data;
using Volo.Abp.Http;
@@ -18,18 +20,11 @@ using Volo.Abp.ObjectExtending;
namespace Volo.Abp.BlazoriseUI.Components.ObjectExtending;
-public partial class LookupExtensionProperty : ComponentBase
+public partial class LookupExtensionProperty
where TEntity : IHasExtraProperties
{
protected List> lookupItems;
- [Inject] public IStringLocalizerFactory StringLocalizerFactory { get; set; }
-
- [Parameter] public TEntity Entity { get; set; }
-
- [Parameter] public ObjectExtensionPropertyInfo PropertyInfo { get; set; }
-
-
[Inject] public ILookupApiRequestService LookupApiService { get; set; }
public string TextPropertyName => PropertyInfo.Name + "_Text";
@@ -42,28 +37,22 @@ public partial class LookupExtensionProperty : Component
}
}
+ public string SelectedText => Entity.GetProperty(TextPropertyName);
+
public LookupExtensionProperty()
{
lookupItems = new List>();
}
- protected override void OnParametersSet()
+ protected async override Task OnInitializedAsync()
{
- var value = Entity.GetProperty(PropertyInfo.Name);
- var text = Entity.GetProperty(TextPropertyName);
- if (value != null && text != null)
- {
- lookupItems.Add(new SelectItem
- {
- Text = Entity.GetProperty(TextPropertyName).ToString(),
- Value = value
- });
- }
+ await base.OnInitializedAsync();
+ await SearchFilterChangedAsync(string.Empty);
}
protected virtual void UpdateLookupTextProperty(object value)
{
- var selectedItemText = lookupItems.SingleOrDefault(t => t.Value.Equals(value)).Text;
+ var selectedItemText = lookupItems.SingleOrDefault(t => t.Value.Equals(value))?.Text;
Entity.SetProperty(TextPropertyName, selectedItemText);
}
@@ -86,8 +75,7 @@ public partial class LookupExtensionProperty : Component
selectItems.Add(new SelectItem
{
Text = item.GetProperty(PropertyInfo.Lookup.DisplayPropertyName).GetString(),
- Value = JsonSerializer.Deserialize(
- item.GetProperty(PropertyInfo.Lookup.ValuePropertyName).GetRawText(), PropertyInfo.Type)
+ Value = JsonSerializer.Deserialize(item.GetProperty(PropertyInfo.Lookup.ValuePropertyName).GetRawText(), PropertyInfo.Type)
});
}
@@ -97,22 +85,11 @@ public partial class LookupExtensionProperty : Component
protected virtual Task SelectedValueChanged(object selectedItem)
{
SelectedValue = selectedItem;
-
return Task.CompletedTask;
}
- protected async Task SearchFilterChangedAsync(string filter)
+ protected virtual async Task SearchFilterChangedAsync(string filter)
{
lookupItems = await GetLookupItemsAsync(filter);
}
-
- protected override async Task OnAfterRenderAsync(bool firstRender)
- {
- await base.OnAfterRenderAsync(firstRender);
-
- if (firstRender)
- {
- await SearchFilterChangedAsync(string.Empty);
- }
- }
}
diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/SelectExtensionProperty.razor b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/SelectExtensionProperty.razor
index c70dc11237..cc1735c26a 100644
--- a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/SelectExtensionProperty.razor
+++ b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/SelectExtensionProperty.razor
@@ -1,13 +1,21 @@
@typeparam TEntity
@typeparam TResourceType
@using Abp.Localization
+@inherits ExtensionPropertyComponentBase
-
- @PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)
-
- @foreach (var item in SelectItems)
- {
- @item.Text
- }
-
-
+
+
+ @PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)
+
+
+ @foreach (var item in SelectItems)
+ {
+ @item.Text
+ }
+
+
+
+
+
+
+
diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/SelectExtensionProperty.razor.cs b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/SelectExtensionProperty.razor.cs
index e873ec27d1..ddc00a4c8b 100644
--- a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/SelectExtensionProperty.razor.cs
+++ b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/SelectExtensionProperty.razor.cs
@@ -2,23 +2,20 @@
using Microsoft.Extensions.Localization;
using System;
using System.Collections.Generic;
+using System.Linq;
+using Blazorise;
+using Volo.Abp.AspNetCore.Components.Web;
using Volo.Abp.Data;
using Volo.Abp.Localization;
using Volo.Abp.ObjectExtending;
namespace Volo.Abp.BlazoriseUI.Components.ObjectExtending;
-public partial class SelectExtensionProperty : ComponentBase
+public partial class SelectExtensionProperty
where TEntity : IHasExtraProperties
{
protected List> SelectItems = new();
- [Inject] public IStringLocalizerFactory StringLocalizerFactory { get; set; }
-
- [Parameter] public TEntity Entity { get; set; }
-
- [Parameter] public ObjectExtensionPropertyInfo PropertyInfo { get; set; }
-
public int SelectedValue {
get { return Entity.GetProperty(PropertyInfo.Name); }
set { Entity.SetProperty(PropertyInfo.Name, value, false); }
diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/TextExtensionProperty.razor b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/TextExtensionProperty.razor
index ade78ee6d2..fc8b7b5398 100644
--- a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/TextExtensionProperty.razor
+++ b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/TextExtensionProperty.razor
@@ -2,13 +2,18 @@
@typeparam TResourceType
@using Volo.Abp.BlazoriseUI
@using Volo.Abp.Localization
+@inherits ExtensionPropertyComponentBase
@if (PropertyInfo != null && Entity != null)
{
-
- @PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)
-
-
-
-
-}
\ No newline at end of file
+
+
+ @PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)
+
+
+
+
+
+
+
+}
diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/TextExtensionProperty.razor.cs b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/TextExtensionProperty.razor.cs
index fd017d2472..6fe9c9fb71 100644
--- a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/TextExtensionProperty.razor.cs
+++ b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/TextExtensionProperty.razor.cs
@@ -1,23 +1,10 @@
-using Microsoft.AspNetCore.Components;
-using Microsoft.Extensions.Localization;
-using Volo.Abp.Data;
-using Volo.Abp.ObjectExtending;
+using Volo.Abp.Data;
namespace Volo.Abp.BlazoriseUI.Components.ObjectExtending;
-public partial class TextExtensionProperty : ComponentBase
+public partial class TextExtensionProperty
where TEntity : IHasExtraProperties
{
- [Inject]
- public IStringLocalizerFactory StringLocalizerFactory { get; set; }
-
- [Parameter]
- public TEntity Entity { get; set; }
-
- [Parameter]
- public ObjectExtensionPropertyInfo PropertyInfo { get; set; }
-
-
protected string Value {
get {
return PropertyInfo.GetTextInputValueOrNull(Entity.GetProperty(PropertyInfo.Name));
diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/TimeExtensionProperty.razor b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/TimeExtensionProperty.razor
index ce2215e621..6ad15b2cb5 100644
--- a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/TimeExtensionProperty.razor
+++ b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/TimeExtensionProperty.razor
@@ -2,12 +2,18 @@
@typeparam TResourceType
@using Volo.Abp.BlazoriseUI
@using Volo.Abp.Localization
+@inherits ExtensionPropertyComponentBase
@if (PropertyInfo != null && Entity != null)
{
-
- @PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory) -->
-
-
-
-}
\ No newline at end of file
+
+
+ @PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory) -->
+
+
+
+
+
+
+
+}
diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/TimeExtensionProperty.razor.cs b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/TimeExtensionProperty.razor.cs
index 6e582ef698..038283850c 100644
--- a/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/TimeExtensionProperty.razor.cs
+++ b/framework/src/Volo.Abp.BlazoriseUI/Components/ObjectExtending/TimeExtensionProperty.razor.cs
@@ -5,23 +5,16 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
+using Blazorise;
+using Volo.Abp.AspNetCore.Components.Web;
using Volo.Abp.Data;
using Volo.Abp.ObjectExtending;
namespace Volo.Abp.BlazoriseUI.Components.ObjectExtending;
-public partial class TimeExtensionProperty : ComponentBase
+public partial class TimeExtensionProperty
where TEntity : IHasExtraProperties
{
- [Inject]
- public IStringLocalizerFactory StringLocalizerFactory { get; set; }
-
- [Parameter]
- public TEntity Entity { get; set; }
-
- [Parameter]
- public ObjectExtensionPropertyInfo PropertyInfo { get; set; }
-
protected TimeSpan? Value {
get {
return PropertyInfo.GetInputValueOrDefault(Entity.GetProperty(PropertyInfo.Name));
diff --git a/framework/src/Volo.Abp.BlazoriseUI/Volo.Abp.BlazoriseUI.csproj b/framework/src/Volo.Abp.BlazoriseUI/Volo.Abp.BlazoriseUI.csproj
index 3ef6aa04bd..a9fe114ecc 100644
--- a/framework/src/Volo.Abp.BlazoriseUI/Volo.Abp.BlazoriseUI.csproj
+++ b/framework/src/Volo.Abp.BlazoriseUI/Volo.Abp.BlazoriseUI.csproj
@@ -14,10 +14,10 @@
-
-
-
-
+
+
+
+
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs
index f9dab2f3af..52e6c85206 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs
@@ -88,7 +88,7 @@ public class NpmPackagesUpdater : ITransientDependency
RunYarn(fileDirectory);
}
- if (!IsAngularProject(fileDirectory))
+ if (!IsAngularProject(fileDirectory) && GulpFileExistAsync(fileDirectory))
{
Thread.Sleep(1000);
RunGulp(fileDirectory);
@@ -108,6 +108,11 @@ public class NpmPackagesUpdater : ITransientDependency
return await Task.FromResult(File.Exists(Path.Combine(directoryName, ".npmrc")));
}
+ private static bool GulpFileExistAsync(string directoryName)
+ {
+ return File.Exists(Path.Combine(directoryName, "gulpfile.js"));
+ }
+
private async Task CreateNpmrcFileAsync(string directoryName)
{
var fileName = Path.Combine(directoryName, ".npmrc");
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DynamicProxy/ProxyHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DynamicProxy/ProxyHelper.cs
index c35fc0bd2f..2bf147573f 100644
--- a/framework/src/Volo.Abp.Core/Volo/Abp/DynamicProxy/ProxyHelper.cs
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/DynamicProxy/ProxyHelper.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Linq;
using System.Reflection;
@@ -9,7 +9,7 @@ public static class ProxyHelper
private const string ProxyNamespace = "Castle.Proxies";
///
- /// Returns dynamic proxy target object if this is a proxied object, otherwise returns the given object.
+ /// Returns dynamic proxy target object if this is a proxied object, otherwise returns the given object.
/// It supports Castle Dynamic Proxies.
///
public static object UnProxy(object obj)
@@ -33,6 +33,20 @@ public static class ProxyHelper
public static Type GetUnProxiedType(object obj)
{
- return UnProxy(obj).GetType();
+ if (obj.GetType().Namespace == ProxyNamespace)
+ {
+ var target = UnProxy(obj);
+ if (target != null)
+ {
+ if (target == obj)
+ {
+ return obj.GetType().GetTypeInfo().BaseType;
+ }
+
+ return target.GetType();
+ }
+ }
+
+ return obj.GetType();
}
}
diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/LimitedResultRequestDto.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/LimitedResultRequestDto.cs
index dae8bae0fb..d72f7b0a77 100644
--- a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/LimitedResultRequestDto.cs
+++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/LimitedResultRequestDto.cs
@@ -49,3 +49,51 @@ public class LimitedResultRequestDto : ILimitedResultRequest, IValidatableObject
}
}
}
+
+///
+/// Simply implements .
+///
+[Serializable]
+public class ExtensibleLimitedResultRequestDto : ExtensibleEntityDto, ILimitedResultRequest, IValidatableObject
+{
+ ///
+ /// Default value: 10.
+ ///
+ public static int DefaultMaxResultCount { get; set; } = 10;
+
+ ///
+ /// Maximum possible value of the .
+ /// Default value: 1,000.
+ ///
+ public static int MaxMaxResultCount { get; set; } = 1000;
+
+ ///
+ /// Maximum result count should be returned.
+ /// This is generally used to limit result count on paging.
+ ///
+ [Range(1, int.MaxValue)]
+ public virtual int MaxResultCount { get; set; } = DefaultMaxResultCount;
+
+ public override IEnumerable Validate(ValidationContext validationContext)
+ {
+ foreach(var result in base.Validate(validationContext))
+ {
+ yield return result;
+ }
+
+ if (MaxResultCount > MaxMaxResultCount)
+ {
+ var localizer = validationContext.GetRequiredService>();
+
+ yield return new ValidationResult(
+ localizer[
+ "MaxResultCountExceededExceptionMessage",
+ nameof(MaxResultCount),
+ MaxMaxResultCount,
+ typeof(ExtensibleLimitedResultRequestDto).FullName,
+ nameof(MaxMaxResultCount)
+ ],
+ new[] { nameof(MaxResultCount) });
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ListResultDto.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ListResultDto.cs
index e5bb81aa2e..361b27faac 100644
--- a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ListResultDto.cs
+++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ListResultDto.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Volo.Abp.ObjectExtending;
namespace Volo.Abp.Application.Dtos;
@@ -7,7 +8,8 @@ namespace Volo.Abp.Application.Dtos;
public class ListResultDto : IListResult
{
///
- public IReadOnlyList Items {
+ public IReadOnlyList Items
+ {
get { return _items ?? (_items = new List()); }
set { _items = value; }
}
@@ -30,3 +32,32 @@ public class ListResultDto : IListResult
Items = items;
}
}
+
+[Serializable]
+public class ExtensibleListResultDto : ExtensibleObject, IListResult
+{
+ ///
+ public IReadOnlyList Items
+ {
+ get { return _items ?? (_items = new List()); }
+ set { _items = value; }
+ }
+ private IReadOnlyList _items;
+
+ ///
+ /// Creates a new object.
+ ///
+ public ExtensibleListResultDto()
+ {
+
+ }
+
+ ///
+ /// Creates a new object.
+ ///
+ /// List of items
+ public ExtensibleListResultDto(IReadOnlyList items)
+ {
+ Items = items;
+ }
+}
diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/PagedAndSortedResultRequestDto.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/PagedAndSortedResultRequestDto.cs
index c48aacd938..229178adbf 100644
--- a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/PagedAndSortedResultRequestDto.cs
+++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/PagedAndSortedResultRequestDto.cs
@@ -10,3 +10,12 @@ public class PagedAndSortedResultRequestDto : PagedResultRequestDto, IPagedAndSo
{
public virtual string Sorting { get; set; }
}
+
+///
+/// Simply implements .
+///
+[Serializable]
+public class ExtensiblePagedAndSortedResultRequestDto : ExtensiblePagedResultRequestDto, IPagedAndSortedResultRequest
+{
+ public virtual string Sorting { get; set; }
+}
diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/PagedResultDto.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/PagedResultDto.cs
index 58ce4117cb..5860a47846 100644
--- a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/PagedResultDto.cs
+++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/PagedResultDto.cs
@@ -32,3 +32,33 @@ public class PagedResultDto : ListResultDto, IPagedResult
TotalCount = totalCount;
}
}
+
+///
+/// Implements .
+///
+/// Type of the items in the list
+[Serializable]
+public class ExtensiblePagedResultDto : ExtensibleListResultDto, IPagedResult
+{
+ ///
+ public long TotalCount { get; set; } //TODO: Can be a long value..?
+
+ ///
+ /// Creates a new object.
+ ///
+ public ExtensiblePagedResultDto()
+ {
+
+ }
+
+ ///
+ /// Creates a new object.
+ ///
+ /// Total count of Items
+ /// List of items in current page
+ public ExtensiblePagedResultDto(long totalCount, IReadOnlyList items)
+ : base(items)
+ {
+ TotalCount = totalCount;
+ }
+}
diff --git a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/PagedResultRequestDto.cs b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/PagedResultRequestDto.cs
index 12fe39ea6e..cfbbc31939 100644
--- a/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/PagedResultRequestDto.cs
+++ b/framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/PagedResultRequestDto.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel.DataAnnotations;
namespace Volo.Abp.Application.Dtos;
@@ -12,3 +12,13 @@ public class PagedResultRequestDto : LimitedResultRequestDto, IPagedResultReques
[Range(0, int.MaxValue)]
public virtual int SkipCount { get; set; }
}
+
+///
+/// Simply implements .
+///
+[Serializable]
+public class ExtensiblePagedResultRequestDto : ExtensibleLimitedResultRequestDto, IPagedResultRequest
+{
+ [Range(0, int.MaxValue)]
+ public virtual int SkipCount { get; set; }
+}
diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj
index 918ac8ddf3..3bb22e24d8 100644
--- a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj
+++ b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj
@@ -20,7 +20,6 @@
-
diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo.Abp.EntityFrameworkCore.Oracle.csproj b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo.Abp.EntityFrameworkCore.Oracle.csproj
index abfb6e41a1..5a0a11b6ad 100644
--- a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo.Abp.EntityFrameworkCore.Oracle.csproj
+++ b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo.Abp.EntityFrameworkCore.Oracle.csproj
@@ -19,8 +19,7 @@
-
-
+
diff --git a/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js
new file mode 100644
index 0000000000..a02a2a7a5e
--- /dev/null
+++ b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js
@@ -0,0 +1,5 @@
+(function () {
+ [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
+ return new bootstrap.Popover(popoverTriggerEl)
+ })
+})();
diff --git a/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js
new file mode 100644
index 0000000000..a02a2a7a5e
--- /dev/null
+++ b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js
@@ -0,0 +1,5 @@
+(function () {
+ [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
+ return new bootstrap.Popover(popoverTriggerEl)
+ })
+})();
diff --git a/modules/blogging/app/Volo.BloggingTestApp/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js b/modules/blogging/app/Volo.BloggingTestApp/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js
new file mode 100644
index 0000000000..a02a2a7a5e
--- /dev/null
+++ b/modules/blogging/app/Volo.BloggingTestApp/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js
@@ -0,0 +1,5 @@
+(function () {
+ [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
+ return new bootstrap.Popover(popoverTriggerEl)
+ })
+})();
diff --git a/modules/client-simulation/demo/Volo.ClientSimulation.Demo/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js b/modules/client-simulation/demo/Volo.ClientSimulation.Demo/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js
new file mode 100644
index 0000000000..a02a2a7a5e
--- /dev/null
+++ b/modules/client-simulation/demo/Volo.ClientSimulation.Demo/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js
@@ -0,0 +1,5 @@
+(function () {
+ [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
+ return new bootstrap.Popover(popoverTriggerEl)
+ })
+})();
diff --git a/modules/cms-kit/host/Volo.CmsKit.IdentityServer/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js b/modules/cms-kit/host/Volo.CmsKit.IdentityServer/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js
new file mode 100644
index 0000000000..a02a2a7a5e
--- /dev/null
+++ b/modules/cms-kit/host/Volo.CmsKit.IdentityServer/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js
@@ -0,0 +1,5 @@
+(function () {
+ [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
+ return new bootstrap.Popover(popoverTriggerEl)
+ })
+})();
diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Host/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js b/modules/cms-kit/host/Volo.CmsKit.Web.Host/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js
new file mode 100644
index 0000000000..a02a2a7a5e
--- /dev/null
+++ b/modules/cms-kit/host/Volo.CmsKit.Web.Host/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js
@@ -0,0 +1,5 @@
+(function () {
+ [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
+ return new bootstrap.Popover(popoverTriggerEl)
+ })
+})();
diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js
new file mode 100644
index 0000000000..a02a2a7a5e
--- /dev/null
+++ b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js
@@ -0,0 +1,5 @@
+(function () {
+ [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
+ return new bootstrap.Popover(popoverTriggerEl)
+ })
+})();
diff --git a/modules/docs/app/VoloDocs.Web/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js b/modules/docs/app/VoloDocs.Web/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js
new file mode 100644
index 0000000000..a02a2a7a5e
--- /dev/null
+++ b/modules/docs/app/VoloDocs.Web/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js
@@ -0,0 +1,5 @@
+(function () {
+ [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
+ return new bootstrap.Popover(popoverTriggerEl)
+ })
+})();
diff --git a/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js b/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js
new file mode 100644
index 0000000000..a02a2a7a5e
--- /dev/null
+++ b/modules/setting-management/app/Volo.Abp.SettingManagement.DemoApp/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js
@@ -0,0 +1,5 @@
+(function () {
+ [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
+ return new bootstrap.Popover(popoverTriggerEl)
+ })
+})();
diff --git a/modules/virtual-file-explorer/app/Volo.Abp.VirtualFileExplorer.DemoApp/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js b/modules/virtual-file-explorer/app/Volo.Abp.VirtualFileExplorer.DemoApp/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js
new file mode 100644
index 0000000000..a02a2a7a5e
--- /dev/null
+++ b/modules/virtual-file-explorer/app/Volo.Abp.VirtualFileExplorer.DemoApp/wwwroot/libs/bootstrap/js/bootstrap.enable.popovers.everywhere.js
@@ -0,0 +1,5 @@
+(function () {
+ [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')).map(function (popoverTriggerEl) {
+ return new bootstrap.Popover(popoverTriggerEl)
+ })
+})();
diff --git a/npm/ng-packs/packages/core/src/lib/core.module.ts b/npm/ng-packs/packages/core/src/lib/core.module.ts
index c34b4a2fab..270ab7dcbd 100644
--- a/npm/ng-packs/packages/core/src/lib/core.module.ts
+++ b/npm/ng-packs/packages/core/src/lib/core.module.ts
@@ -23,6 +23,7 @@ import { LocalizationModule } from './localization.module';
import { ABP } from './models/common';
import { LocalizationPipe } from './pipes/localization.pipe';
import { SortPipe } from './pipes/sort.pipe';
+import { ToInjectorPipe } from './pipes/to-injector.pipe';
import { CookieLanguageProvider } from './providers/cookie-language.provider';
import { LocaleProvider } from './providers/locale.provider';
import { LocalizationService } from './services/localization.service';
@@ -65,6 +66,7 @@ export function storageFactory(): OAuthStorage {
RouterOutletComponent,
SortPipe,
StopPropagationDirective,
+ ToInjectorPipe,
],
imports: [
OAuthModule,
@@ -89,6 +91,7 @@ export function storageFactory(): OAuthStorage {
RouterOutletComponent,
SortPipe,
StopPropagationDirective,
+ ToInjectorPipe,
],
providers: [LocalizationPipe],
entryComponents: [
diff --git a/npm/ng-packs/packages/core/src/lib/pipes/index.ts b/npm/ng-packs/packages/core/src/lib/pipes/index.ts
index eb028eee4f..31e0a76593 100644
--- a/npm/ng-packs/packages/core/src/lib/pipes/index.ts
+++ b/npm/ng-packs/packages/core/src/lib/pipes/index.ts
@@ -1,2 +1,3 @@
export * from './localization.pipe';
export * from './sort.pipe';
+export * from './to-injector.pipe';
diff --git a/npm/ng-packs/packages/core/src/lib/pipes/to-injector.pipe.ts b/npm/ng-packs/packages/core/src/lib/pipes/to-injector.pipe.ts
new file mode 100644
index 0000000000..5a1ac4df29
--- /dev/null
+++ b/npm/ng-packs/packages/core/src/lib/pipes/to-injector.pipe.ts
@@ -0,0 +1,28 @@
+import { InjectionToken, Injector, Pipe, PipeTransform } from '@angular/core';
+
+export const INJECTOR_PIPE_DATA_TOKEN = new InjectionToken(
+ 'INJECTOR_PIPE_DATA_TOKEN',
+);
+
+@Pipe({
+ name: 'toInjector',
+})
+export class ToInjectorPipe implements PipeTransform {
+ constructor(private injector: Injector) {}
+ transform(
+ value: any,
+ token: InjectionToken = INJECTOR_PIPE_DATA_TOKEN,
+ name = 'ToInjectorPipe',
+ ): Injector {
+ return Injector.create({
+ providers: [
+ {
+ provide: token,
+ useValue: value,
+ },
+ ],
+ parent: this.injector,
+ name,
+ });
+ }
+}
diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/index.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/index.ts
index 7daf7f2c1a..b5e0d52629 100644
--- a/npm/ng-packs/packages/theme-basic/src/lib/components/index.ts
+++ b/npm/ng-packs/packages/theme-basic/src/lib/components/index.ts
@@ -7,6 +7,6 @@ export * from './logo/logo.component';
export * from './nav-items/current-user.component';
export * from './nav-items/languages.component';
export * from './nav-items/nav-items.component';
+export * from './page-alert-container/page-alert-container.component';
export * from './routes/routes.component';
export * from './validation-error/validation-error.component';
-export * from './page-alert-container/page-alert-container.component';
diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/current-user.component.html b/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/current-user.component.html
index 64a1bb4d6a..3484c452a0 100644
--- a/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/current-user.component.html
+++ b/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/current-user.component.html
@@ -31,11 +31,35 @@
aria-labelledby="dropdownMenuLink"
[class.d-block]="smallScreen && currentUserDropdown.isOpen()"
>
- {{ 'AbpAccount::MyAccount' | abpLocalization }}
- {{ 'AbpUi::Logout' | abpLocalization }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.textTemplate.text | abpLocalization }}
+
+
+
+
diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/current-user.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/current-user.component.ts
index fbfa01882a..9619845837 100644
--- a/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/current-user.component.ts
+++ b/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/current-user.component.ts
@@ -5,7 +5,8 @@ import {
NAVIGATE_TO_MANAGE_PROFILE,
SessionStateService,
} from '@abp/ng.core';
-import { Component, Inject } from '@angular/core';
+import { UserMenu, UserMenuService } from '@abp/ng.theme.shared';
+import { Component, Inject, TrackByFunction } from '@angular/core';
import { Observable } from 'rxjs';
@Component({
@@ -16,12 +17,15 @@ export class CurrentUserComponent {
currentUser$: Observable