diff --git a/.gitignore b/.gitignore index eb4dbedfae..9a855fd898 100644 --- a/.gitignore +++ b/.gitignore @@ -293,3 +293,10 @@ samples/MicroserviceDemo/applications/ConsoleClientDemo/Logs/logs.txt modules/docs/app/Volo.DocsTestApp/Logs/logs.txt framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Logs/logs.txt samples/MicroserviceDemo/microservices/TenantManagementService.Host/Logs/logs.txt +/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package-lock.json +/samples/BookStore/src/Acme.BookStore.Web/package-lock.json +/samples/DashboardDemo/src/DashboardDemo.Web/package-lock.json +/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/package-lock.json +/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/package-lock.json +/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/package-lock.json +/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.IdentityServer/package-lock.json diff --git a/docs/en/Samples/Index.md b/docs/en/Samples/Index.md index 0c9ce68886..12095bb2c9 100644 --- a/docs/en/Samples/Index.md +++ b/docs/en/Samples/Index.md @@ -35,6 +35,9 @@ While there is no Razor Pages & MongoDB combination, you can check both document * **Entity Framework Migrations**: A solution to demonstrate how to split your application into multiple databases each database contains different modules. * [Source code](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) * [EF Core database migrations document](../Entity-Framework-Core-Migrations.md) +* **SignalR Demo**: A simple chat application that allows to send and receive messages among authenticated users. + * [Source code](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) + * [Signal Integration document](../SignalR-Integration.md) * **Dashboard Demo**: A simple application to show how to use the widget system for the ASP.NET Core MVC UI. * [Source code](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) * [Widget documentation](../UI/AspNetCore/Widgets.md) diff --git a/docs/en/SignalR-Integration.md b/docs/en/SignalR-Integration.md new file mode 100644 index 0000000000..415b585f3f --- /dev/null +++ b/docs/en/SignalR-Integration.md @@ -0,0 +1,227 @@ +# SignalR Integration + +> It is already possible to follow [the standard Microsoft tutorial](https://docs.microsoft.com/en-us/aspnet/core/tutorials/signalr) to add [SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) to your application. However, ABP provides a SignalR integration packages that simplify the integration and usage. + +## Installation + +### Server Side + +It is suggested to use the [ABP CLI](CLI.md) to install this package. + +#### Using the ABP CLI + +Open a command line window in the folder of your project (.csproj file) and type the following command: + +```bash +abp add-package Volo.Abp.AspNetCore.SignalR +``` + +> You typically want to add this package to the web or API layer of your application, depending on your architecture. + +#### Manual Installation + +If you want to manually install; + +1. Add the [Volo.Abp.AspNetCore.SignalR](https://www.nuget.org/packages/Volo.Abp.AspNetCore.SignalR) NuGet package to your project: + + ``` + Install-Package Volo.Abp.BackgroundJobs.HangFire + ``` + + Or use the Visual Studio NuGet package management UI to install it. + +2. Add the `AbpAspNetCoreSignalRModule` to the dependency list of your module: + +```csharp +[DependsOn( + //...other dependencies + typeof(AbpAspNetCoreSignalRModule) //Add the new module dependency + )] +public class YourModule : AbpModule +{ +} +``` + +> You don't need to use the `services.AddSignalR()` and the `app.UseEndpoints(...)`, it's done by the `AbpAspNetCoreSignalRModule`. + +### Client Side + +Client side installation depends on your UI framework / client type. + +#### ASP.NET Core MVC / Razor Pages UI + +Run the following command in the root folder of your web project: + +````bash +yarn add @abp/signalr +```` + +> This requires to [install yarn](https://yarnpkg.com/) if you haven't install before. + +This will add the `@abp/signalr` to the dependencies in the `package.json` of your project: + +````json +{ + ... + "dependencies": { + ... + "@abp/signalr": "~2.7.0" + } +} +```` + +Run the `gulp` in the root folder of your web project: + +````bash +gulp +```` + +This will copy the SignalR JavaScript files into your project: + +![signal-js-file](images/signal-js-file.png) + +Finally, add the following code to your page/view to include the `signalr.js` file + +````xml +@section scripts { + +} +```` + +It requires to add `@using Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR` to your page/view. + +> You could add the `signalr.js` file in a standard way. But using the `SignalRBrowserScriptContributor` has additional benefits. See the [Client Side Package Management](UI/AspNetCore/Client-Side-Package-Management.md) and [Bundling & Minification](UI/AspNetCore/Bundling-Minification.md) documents for details. + +That's all. you can use the [SignalR JavaScript API](https://docs.microsoft.com/en-us/aspnet/core/signalr/javascript-client) in your page. + +#### Other UI Frameworks / Clients + +Please refer to [Microsoft's documentation](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) for other type of clients. + +## The ABP Framework Integration + +This section covers the additional benefits when you use the ABP Framework integration packages. + +### Hub Route & Mapping + +ABP automatically registers all the hubs to the [dependency injection](Dependency-Injection.md) (as transient) and maps the hub endpoint. So, you don't have to use the ` app.UseEndpoints(...)` to map your hubs. Hub route (URL) is determined conventionally based on your hub name. + +Example: + +````csharp +public class MessagingHub : Hub +{ + //... +} +```` + +The hub route will be `/signalr-hubs/messasing` for the `MessasingHub`: + +* Adding a standard `/signalr-hubs/` prefix +* Continue with the **camel case** hub name, without the `Hub` suffix. + +If you want to specify the route, you can use the `HubRoute` attribute: + +````csharp +[HubRoute("/my-messasing-hub")] +public class MessagingHub : Hub +{ + //... +} +```` + +### AbpHub Base Classes + +Instead of the standard `Hub` and `Hub` classes, you can inherit from the `AbpHub` or `AbpHub` which hve useful base properties like `CurrentUser`. + +Example: + +````csharp +public class MessagingHub : AbpHub +{ + public async Task SendMessage(string targetUserName, string message) + { + var currentUserName = CurrentUser.UserName; //Access to the current user info + var txt = L["MyText"]; //Localization + } +} +```` + +> While you could inject the same properties into your hub constructor, this way simplifies your hub class. + +### Manual Registration / Mapping + +ABP automatically registers all the hubs to the [dependency injection](Dependency-Injection.md) as a **transient service**. If you want to **disable auto dependency injection** registration for your hub class, just add a `DisableConventionalRegistration` attribute. You can still register your hub class to dependency injection in the `ConfigureServices` method of your module if you like: + +````csharp +context.Services.AddTransient(); +```` + +When **you or ABP** register the class to the dependency injection, it is automatically mapped to the endpoint route configuration just as described in the previous sections. You can use `DisableAutoHubMap` attribute if you want to manually map your hub class. + +For manual mapping, you have two options: + +1. Use the `AbpSignalROptions` to add your map configuration (in the `ConfigureServices` method of your [module](Module-Development-Basics.md)), so ABP still performs the endpoint mapping for your hub: + +````csharp +Configure(options => +{ + options.Hubs.Add( + new HubConfig( + typeof(MessagingHub), //Hub type + "/my-messaging/route", //Hub route (URL) + hubOptions => + { + //Additional options + hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); + } + ) + ); +}); +```` + +This is a good way to provide additional SignalR options. + +If you don't want to disable auto hub map, but still want to perform additional SignalR configuration, use the `options.Hubs.AddOrUpdate(...)` method: + +````csharp +Configure(options => +{ + options.Hubs.AddOrUpdate( + typeof(MessagingHub), //Hub type + config => //Additional configuration + { + config.RoutePattern = "/my-messaging-hub"; //override the default route + config.ConfigureActions.Add(hubOptions => + { + //Additional options + hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); + }); + } + ); +}); +```` + +This is the way you can modify the options of a hub class defined in a depended module (where you don't have the source code access). + +2. Change `app.UseConfiguredEndpoints` in the `OnApplicationInitialization` method of your [module](Module-Development-Basics.md) as shown below (added a lambda method as the parameter). + +````csharp +app.UseConfiguredEndpoints(endpoints => +{ + endpoints.MapHub("/my-messaging-hub", options => + { + options.LongPolling.PollTimeout = TimeSpan.FromSeconds(30); + }); +}); +```` + +### UserIdProvider + +ABP implements SignalR's `IUserIdProvider` interface to provide the current user id from the `ICurrentUser` service of the ABP framework (see [the current user service](CurrentUser.md)), so it will be integrated to the authentication system of your application. The implementing class is the `AbpSignalRUserIdProvider`, if you want to change/override it. + +## Example Application + +See the [SignalR Integration Demo](https://github.com/abpframework/abp-samples/tree/master/SignalRDemo) as a sample application. It has a simple Chat page to send messages between (authenticated) users. + +![signalr-demo-chat](images/signalr-demo-chat.png) \ No newline at end of file diff --git a/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md b/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md index 0cc63d49e8..9969bcf3cc 100644 --- a/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md +++ b/docs/en/UI/AspNetCore/Tag-Helpers/Paginator.md @@ -24,7 +24,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components public void OnGet(int currentPage, string sort) { - PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort); + PagerModel = new PagerModel(100, 10, currentPage, 10, "/Components/Paginator", sort); } } } diff --git a/docs/en/images/signal-js-file.png b/docs/en/images/signal-js-file.png new file mode 100644 index 0000000000..19fc6cefe3 Binary files /dev/null and b/docs/en/images/signal-js-file.png differ diff --git a/docs/en/images/signalr-demo-chat.png b/docs/en/images/signalr-demo-chat.png new file mode 100644 index 0000000000..120d7eda6e Binary files /dev/null and b/docs/en/images/signalr-demo-chat.png differ diff --git a/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md b/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md index caa448a14a..94a2120059 100644 --- a/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md +++ b/docs/zh-Hans/Customizing-Application-Modules-Overriding-Services.md @@ -107,22 +107,25 @@ public class MyIdentityUserAppService : IdentityUserAppService public class MyIdentityUserManager : IdentityUserManager { public MyIdentityUserManager( - IdentityUserStore store, + IdentityUserStore store, + IIdentityRoleRepository roleRepository, + IIdentityUserRepository userRepository, IOptions optionsAccessor, IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, - ILookupNormalizer keyNormalizer, - IdentityErrorDescriber errors, - IServiceProvider services, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + IServiceProvider services, ILogger logger, - ICancellationTokenProvider cancellationTokenProvider - ) : base( - store, + ICancellationTokenProvider cancellationTokenProvider) : + base(store, + roleRepository, + userRepository, optionsAccessor, passwordHasher, userValidators, - passwordValidators, + passwordValidators, keyNormalizer, errors, services, diff --git a/docs/zh-Hans/Getting-Started-AspNetCore-Application.md b/docs/zh-Hans/Getting-Started-AspNetCore-Application.md index 9d195562fd..0334609bd3 100644 --- a/docs/zh-Hans/Getting-Started-AspNetCore-Application.md +++ b/docs/zh-Hans/Getting-Started-AspNetCore-Application.md @@ -30,10 +30,8 @@ ABP是一个模块化框架,它需要一个**启动 (根) 模块**继承自``Abp ````C# using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Volo.Abp; -using Volo.Abp.AspNetCore.Modularity; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.Modularity; @@ -42,7 +40,8 @@ namespace BasicAspNetCoreApplication [DependsOn(typeof(AbpAspNetCoreMvcModule))] public class AppModule : AbpModule { - public override void OnApplicationInitialization(ApplicationInitializationContext context) + public override void OnApplicationInitialization( + ApplicationInitializationContext context) { var app = context.GetApplicationBuilder(); var env = context.GetEnvironment(); @@ -51,8 +50,14 @@ namespace BasicAspNetCoreApplication { app.UseDeveloperExceptionPage(); } + else + { + app.UseExceptionHandler("/Error"); + } - app.UseMvcWithDefaultRoute(); + app.UseStaticFiles(); + app.UseRouting(); + app.UseConfiguredEndpoints(); } } } diff --git a/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md b/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md index 8b282bbe56..34aab9fa46 100644 --- a/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md +++ b/docs/zh-Hans/How-To/Azure-Active-Directory-Authentication-MVC.md @@ -157,6 +157,16 @@ private void ConfigureAuthentication(ServiceConfigurationContext context, IConfi JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier); ```` +* Help! 我一直得到 ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** 错误! + + * 当你使用 Azure Authority **v2.0 端点** 而不请求 `email` 域, 会发生这些情况. [Abp 创建用户检查了唯一的邮箱](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). 只需添加 + + ````csharp + options.Scope.Add("email"); + ```` + + 到你的 openid 配置. + * Help! 我一直得到 ***AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application*** 错误! * 如果你在appsettings设置 **CallbackPath** 为: @@ -170,15 +180,17 @@ private void ConfigureAuthentication(ServiceConfigurationContext context, IConfi 你在azure门户的应用程序**重定向URI**必须具有之类 `https://localhost:44320/signin-azuread-oidc` 的, 而不仅是 `/signin-azuread-oidc`. -* Help! 我一直得到 ***System.ArgumentNullException: Value cannot be null. (Parameter 'userName')*** 错误! +* Help! 我一直得到 ***AADSTS700051: The response_type 'token' is not enabled for the application.*** 错误! - * 当你使用 Azure Authority **v2.0 端点** 而不请求 `email` 域, 会发生这些情况. [Abp 创建用户检查了唯一的邮箱](https://github.com/abpframework/abp/blob/037ef9abe024c03c1f89ab6c933710bcfe3f5c93/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L208). 只需添加 + * 当你请求**token**(访问令牌)和**id_token**时没有在Azure门户应用程序启用访问令牌时会发生这个错误,只需勾选ID令牌顶部的**访问令牌**复选框即可同时请求令牌. - ````csharp - options.Scope.Add("email"); - ```` +* Help! 我一直得到 ***AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret*** 错误! - 到你的 openid 配置. + * 当你与 **id_token** 一起请求 **code**时,你需要在Azure门户的**证书和机密**菜单下添加**证书和机密**. 然后你需要添加openid配置选项: + + ````csharp + options.ClientSecret = "Value of your secret on azure portal"; + ```` * 如何**调试/监视**在映射之前获得的声明? diff --git a/docs/zh-Hans/Samples/Index.md b/docs/zh-Hans/Samples/Index.md new file mode 100644 index 0000000000..774d6467ef --- /dev/null +++ b/docs/zh-Hans/Samples/Index.md @@ -0,0 +1,56 @@ +# 示例应用 + +这些是ABP框架创建的官方示例. 这些示例大部分在[abpframework/abp-samples](https://github.com/abpframework/abp-samples) GitHub 仓库. + +### 微服务示例 + +演示如何基于微服务体系结构构建系统的完整解决方案. + +* [示例的文档](Microservice-Demo.md) +* [源码](https://github.com/abpframework/abp/tree/dev/samples/MicroserviceDemo) +* [微服务架构文档](../Microservice-Architecture.md) + +### Book Store + +一个简单的CRUD应用程序,展示了使用ABP框架开发应用程序的基本原理. 使用不同的技术实现了相同的示例: + +* **Book Store: Razor Pages UI & Entity Framework Core** + + * [教程](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC) + * [源码](https://github.com/abpframework/abp-samples/tree/master/BookStore) + +* **Book Store: Angular UI & MongoDB** + + * [教程](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=NG) + * [源码](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) + +* **Book Store: Modular application (Razor Pages UI & EF Core)** + + * [源码](https://github.com/abpframework/abp-samples/tree/master/BookStore-Modular) + +如果没有Razor Pages & MongoDB 结合,但你可以检查两个文档来理解它,因为DB和UI不会互相影响. + +### 其他示例 + +* **Entity Framework 迁移**: 演示如何将应用程序拆分为多个数据库的解决方案. 每个数据库包含不同的模块. + * [源码](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) + * [EF Core数据库迁移文档](../Entity-Framework-Core-Migrations.md) +* **Dashboard Demo**: 一个简单的应用程序,展示了如何在ASP.NET Core MVC UI中使用widget系统. + * [源码](https://github.com/abpframework/abp-samples/tree/master/DashboardDemo) + * [Widget 文档](../UI/AspNetCore/Widgets.md) +* **RabbitMQ 事件总线 Demo**: 由两个通过RabbitMQ集成的分布式事件相互通信的应用程序组成的解决方案. + * [源码](https://github.com/abpframework/abp-samples/tree/master/RabbitMqEventBus) + * [分布式事件总线文档](../Distributed-Event-Bus.md) + * [RabbitMQ 分布式事件总线集成文档](../Distributed-Event-Bus-RabbitMQ-Integration.md) +* **自定义认证**: 如何为ASP.NET Core MVC / Razor Pages应用程序自定义身份验证的解决方案. + * [源码](https://github.com/abpframework/abp-samples/tree/master/aspnet-core/Authentication-Customization) + * 相关 "[How To](../How-To/Index.md)" 文档: + * [Azure Active Directory 认证](../How-To/Azure-Active-Directory-Authentication-MVC.md) + * [自定义登录页面](../How-To/Customize-Login-Page-MVC.md) + * [自定义 SignIn Manager](../How-To/Customize-SignIn-Manager.md) +* **空的ASP.NET Core应用程序**: 从基本的ASP.NET Core应用程序使用ABP框架. + * [源码](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication) + * [文档](../Getting-Started-AspNetCore-Application.md) +* **空的控制台应用程序**: 从基本的控制台应用程序安装ABP框架. + * [源码](https://github.com/abpframework/abp-samples/tree/master/BasicConsoleApplication) + * [文档](../Getting-Started-Console-Application.md) \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-II.md b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-II.md index 2d4b344afa..e981259db8 100644 --- a/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-II.md +++ b/docs/zh-Hans/Tutorials/AspNetCore-Mvc/Part-II.md @@ -1,432 +1,8 @@ -## ASP.NET Core MVC 教程 - 第二章 +# 教程 -### 关于本教程 +## 应用程序开发 -这是ASP.NET Core MVC教程系列的第二章. 查看其它章节 +* [ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) +* [Angular UI](../Part-1?UI=NG) -* [Part I: 创建项目和书籍列表页面](Part-I.md) -* **Part II: 创建,编辑,删除书籍(本章)** -* [Part III: 集成测试](Part-III.md) - -你可以从[GitHub存储库](https://github.com/volosoft/abp/tree/master/samples/BookStore)访问应用程序的**源代码**. - -> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). - -### 新增 Book 实体 - -通过本节, 你将会了解如何创建一个 modal form 来实现新增书籍的功能. 最终成果如下图所示: - -![bookstore-create-dialog](images/bookstore-create-dialog-2.png) - -#### 新建 modal form - -在 `Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个 `CreateModal.cshtml` Razor页面: - -![bookstore-add-create-dialog](images/bookstore-add-create-dialog-v2.png) - -##### CreateModal.cshtml.cs - -展开 `CreateModal.cshtml`,打开 `CreateModal.cshtml.cs` 代码文件,用如下代码替换 `CreateModalModel` 类的实现: - -````C# -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; - -namespace Acme.BookStore.Web.Pages.Books -{ - public class CreateModalModel : BookStorePageModel - { - [BindProperty] - public CreateUpdateBookDto Book { get; set; } - - private readonly IBookAppService _bookAppService; - - public CreateModalModel(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - public async Task OnPostAsync() - { - await _bookAppService.CreateAsync(Book); - return NoContent(); - } - } -} -```` - -* 该类派生于 `BookStorePageModel` 而非默认的 `PageModel`. `BookStorePageModel` 继承了 `PageModel` 并且添加了一些可以被你的page model类使用的通用属性和方法. -* `Book` 属性上的 `[BindProperty]` 特性将post请求提交上来的数据绑定到该属性上. -* 该类通过构造函数注入了 `IBookAppService` 应用服务,并且在 `OnPostAsync` 处理程序中调用了服务的 `CreateAsync` 方法. - -##### CreateModal.cshtml - -打开 `CreateModal.cshtml` 文件并粘贴如下代码: - -````html -@page -@inherits Acme.BookStore.Web.Pages.BookStorePage -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model Acme.BookStore.Web.Pages.Books.CreateModalModel -@{ - Layout = null; -} - - - - - - - - - -```` - -* 这个 modal 使用 `abp-dynamic-form` Tag Helper 根据 `CreateBookViewModel` 类自动构建了表单. - * `abp-model` 指定了 `Book` 属性为模型对象. - * `data-ajaxForm` 设置了表单通过AJAX提交,而不是经典的页面回发. - * `abp-form-content` tag helper 作为表单控件渲染位置的占位符 (这是可选的,只有你在 `abp-dynamic-form` 中像本示例这样添加了其他内容才需要). - -#### 添加 "New book" 按钮 - -打开 `Pages/Books/Index.cshtml` 并按如下代码修改 `abp-card-header` : - -````html - - - -

@L["Books"]

-
- - - -
-
-```` - -如下图所示,只是在表格 **右上方** 添加了 **New book** 按钮: - -![bookstore-new-book-button](images/bookstore-new-book-button.png) - -打开 `Pages/books/index.js` 在datatable配置代码后面添加如下代码: - -````js -var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - -createModal.onResult(function () { - dataTable.ajax.reload(); -}); - -$('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); -}); -```` - -* `abp.ModalManager` 是一个在客户端打开和管理modal的辅助类.它基于Twitter Bootstrap的标准modal组件通过简化的API抽象隐藏了许多细节. - -现在,你可以 **运行程序** 通过新的 modal form 来创建书籍了. - -### 编辑更新已存在的 Book 实体 - -在 `Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个名叫 `EditModal.cshtml` 的Razor页面: - -![bookstore-add-edit-dialog](images/bookstore-add-edit-dialog.png) - -#### EditModal.cshtml.cs - -展开 `EditModal.cshtml`,打开 `EditModal.cshtml.cs` 文件( `EditModalModel` 类) 并替换成以下代码: - -````csharp -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; - -namespace Acme.BookStore.Web.Pages.Books -{ - public class EditModalModel : BookStorePageModel - { - [HiddenInput] - [BindProperty(SupportsGet = true)] - public Guid Id { get; set; } - - [BindProperty] - public CreateUpdateBookDto Book { get; set; } - - private readonly IBookAppService _bookAppService; - - public EditModalModel(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - public async Task OnGetAsync() - { - var bookDto = await _bookAppService.GetAsync(Id); - Book = ObjectMapper.Map(bookDto); - } - - public async Task OnPostAsync() - { - await _bookAppService.UpdateAsync(Id, Book); - return NoContent(); - } - } -} -```` - -* `[HiddenInput]` 和 `[BindProperty]` 是标准的 ASP.NET Core MVC 特性.这里启用 `SupportsGet` 从Http请求的查询字符串中获取Id的值. -* 在 `OnGetAsync` 方法中,将 `BookAppService.GetAsync` 方法返回的 `BookDto` 映射成 `CreateUpdateBookDto` 并赋值给Book属性. -* `OnPostAsync` 方法直接使用 `BookAppService.UpdateAsync` 来更新实体. - -#### BookDto到CreateUpdateBookDto对象映射 - -为了执行`BookDto`到`CreateUpdateBookDto`对象映射,请打开`Acme.BookStore.Web`项目中的`BookStoreWebAutoMapperProfile.cs`并更改它,如下所示: - -````csharp -using AutoMapper; - -namespace Acme.BookStore.Web -{ - public class BookStoreWebAutoMapperProfile : Profile - { - public BookStoreWebAutoMapperProfile() - { - CreateMap(); - } - } -} -```` - -* 刚刚添加了`CreateMap();`作为映射定义. - -#### EditModal.cshtml - -将 `EditModal.cshtml` 页面内容替换成如下代码: - -````html -@page -@inherits Acme.BookStore.Web.Pages.BookStorePage -@using Acme.BookStore.Web.Pages.Books -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model EditModalModel -@{ - Layout = null; -} - - - - - - - - - - -```` - -这个页面内容和 `CreateModal.cshtml` 非常相似,除了以下几点: - -* 它包含`id`属性的`abp-input`, 用于存储编辑书的id(它是隐藏的Input) -* 此页面指定的post地址是`Books/EditModal`, 并用文本 *Update* 作为 modal 标题. - -#### 为表格添加 "操作(Actions)" 下拉菜单 - -我们将为表格每行添加下拉按钮 ("Actions") . 最终效果如下: - -![bookstore-books-table-actions](images/bookstore-books-table-actions.png) - -打开 `Pages/Books/Index.cshtml` 页面,并按下方所示修改表格部分的代码: - -````html - - - - @L["Actions"] - @L["Name"] - @L["Type"] - @L["PublishDate"] - @L["Price"] - @L["CreationTime"] - - - -```` - -* 只是为"Actions"增加了一个 `th` 标签. - -打开 `Pages/books/index.js` 并用以下内容进行替换: - -````js -$(function () { - - var l = abp.localization.getResource('BookStore'); - - var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); - - var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ - processing: true, - serverSide: true, - paging: true, - searching: false, - autoWidth: false, - scrollCollapse: true, - order: [[1, "asc"]], - ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), - columnDefs: [ - { - rowAction: { - items: - [ - { - text: l('Edit'), - action: function (data) { - editModal.open({ id: data.record.id }); - } - } - ] - } - }, - { data: "name" }, - { data: "type" }, - { data: "publishDate" }, - { data: "price" }, - { data: "creationTime" } - ] - })); - - createModal.onResult(function () { - dataTable.ajax.reload(); - }); - - editModal.onResult(function () { - dataTable.ajax.reload(); - }); - - $('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); - }); -}); -```` - -* 通过 `abp.localization.getResource('BookStore')` 可以在客户端使用服务器端定义的相同的本地化语言文本. -* 添加了一个名为 `createModal` 的新的 `ModalManager` 来打开创建用的 modal 对话框. -* 添加了一个名为 `editModal` 的新的 `ModalManager` 来打开编辑用的 modal 对话框. -* 在 `columnDefs` 起始处新增一列用于显示 "Actions" 下拉按钮. -* "New Book"动作只需调用`createModal.open`来打开创建对话框. -* "Edit" 操作只是简单调用 `editModal.open` 来打开编辑对话框. - -现在,你可以运行程序,通过编辑操作来更新任一个book实体. - -### 删除一个已有的Book实体 - -打开 `Pages/books/index.js` 文件,在 `rowAction` `items` 下新增一项: - -````js -{ - text: l('Delete'), - confirmMessage: function (data) { - return l('BookDeletionConfirmationMessage', data.record.name); - }, - action: function (data) { - acme.bookStore.book - .delete(data.record.id) - .then(function() { - abp.notify.info(l('SuccessfullyDeleted')); - dataTable.ajax.reload(); - }); - } -} -```` - -* `confirmMessage` 用来在实际执行 `action` 之前向用户进行确认. -* 通过javascript代理方法 `acme.bookStore.book.delete` 执行一个AJAX请求来删除一个book实体. -* `abp.notify.info` 用来在执行删除操作后显示一个toastr通知信息. - -最终的 `index.js` 文件内容如下所示: - -````js -$(function () { - - var l = abp.localization.getResource('BookStore'); - - var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); - - var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ - processing: true, - serverSide: true, - paging: true, - searching: false, - autoWidth: false, - scrollCollapse: true, - order: [[1, "asc"]], - ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), - columnDefs: [ - { - rowAction: { - items: - [ - { - text: l('Edit'), - action: function (data) { - editModal.open({ id: data.record.id }); - } - }, - { - text: l('Delete'), - confirmMessage: function (data) { - return l('BookDeletionConfirmationMessage', data.record.name); - }, - action: function (data) { - acme.bookStore.book - .delete(data.record.id) - .then(function() { - abp.notify.info(l('SuccessfullyDeleted')); - dataTable.ajax.reload(); - }); - } - } - ] - } - }, - { data: "name" }, - { data: "type" }, - { data: "publishDate" }, - { data: "price" }, - { data: "creationTime" } - ] - })); - - createModal.onResult(function () { - dataTable.ajax.reload(); - }); - - editModal.onResult(function () { - dataTable.ajax.reload(); - }); - - $('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); - }); -}); -```` - -打开`Acme.BookStore.Domain.Shared`项目中的`en.json`并添加以下行: - -````json -"BookDeletionConfirmationMessage": "Are you sure to delete the book {0}?", -"SuccessfullyDeleted": "Successfully deleted" -```` - -运行程序并尝试删除一个book实体. - -### 下一章 - -查看本教程的 [下一章](Part-III.md) . + \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Paginator.md b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Paginator.md index 7a9b3de126..945da6fa67 100644 --- a/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Paginator.md +++ b/docs/zh-Hans/UI/AspNetCore/Tag-Helpers/Paginator.md @@ -24,7 +24,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components public void OnGet(int currentPage, string sort) { - PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort); + PagerModel = new PagerModel(100, 10, currentPage, 10, "/Components/Paginator", sort); } } } diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json index 9f7d69a784..d61aa63ce6 100644 --- a/docs/zh-Hans/docs-nav.json +++ b/docs/zh-Hans/docs-nav.json @@ -495,6 +495,10 @@ { "text": "示例", "items": [ + { + "text": "所有示例", + "path": "Samples/Index.md" + }, { "text": "微服务示例", "path": "Samples/Microservice-Demo.md" diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index 3ff151439f..2db0442389 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -287,6 +287,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Validation.Abstrac EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.SignalR", "src\Volo.Abp.AspNetCore.SignalR\Volo.Abp.AspNetCore.SignalR.csproj", "{B64FCE08-E9D2-4984-BF12-FE199F257416}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.AspNetCore.SignalR.Tests", "test\Volo.Abp.AspNetCore.SignalR.Tests\Volo.Abp.AspNetCore.SignalR.Tests.csproj", "{8B758716-DCC9-4223-8421-5588D1597487}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -853,6 +855,10 @@ Global {B64FCE08-E9D2-4984-BF12-FE199F257416}.Debug|Any CPU.Build.0 = Debug|Any CPU {B64FCE08-E9D2-4984-BF12-FE199F257416}.Release|Any CPU.ActiveCfg = Release|Any CPU {B64FCE08-E9D2-4984-BF12-FE199F257416}.Release|Any CPU.Build.0 = Release|Any CPU + {8B758716-DCC9-4223-8421-5588D1597487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B758716-DCC9-4223-8421-5588D1597487}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B758716-DCC9-4223-8421-5588D1597487}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B758716-DCC9-4223-8421-5588D1597487}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -998,6 +1004,7 @@ Global {251C7FD3-D313-4BCE-8068-352EC7EEA275} = {447C8A77-E5F0-4538-8687-7383196D04EA} {FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {B64FCE08-E9D2-4984-BF12-FE199F257416} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {8B758716-DCC9-4223-8421-5588D1597487} = {447C8A77-E5F0-4538-8687-7383196D04EA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/AbpPaginationTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/AbpPaginationTagHelperService.cs index 2ab9ccef96..76bfcb3ef1 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/AbpPaginationTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/AbpPaginationTagHelperService.cs @@ -1,4 +1,6 @@ -using System.Text; +using System; +using System.Linq; +using System.Text; using System.Text.Encodings.Web; using System.Threading.Tasks; using Localization.Resources.AbpUi; @@ -123,6 +125,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination var tagHelperOutput = await anchorTagHelper.ProcessAndGetOutputAsync(attributeList, context, "a", TagMode.StartTagAndEndTag); + SetHrefAttribute(currentPage, attributeList); + tagHelperOutput.Content.SetHtmlContent(localizer[localizationKey]); var renderedHtml = tagHelperOutput.Render(_encoder); @@ -172,5 +176,20 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination " \r\n" + " \r\n"; } + + protected virtual void SetHrefAttribute(string currentPage, TagHelperAttributeList attributeList) + { + var hrefAttribute = attributeList.FirstOrDefault(x => x.Name.Equals("href", StringComparison.OrdinalIgnoreCase)); + + if (hrefAttribute != null) + { + var pageUrl = TagHelper.Model.PageUrl; + var routeValue = $"currentPage={currentPage}{(TagHelper.Model.Sort.IsNullOrWhiteSpace()? "" : "&sort="+TagHelper.Model.Sort)}"; + pageUrl += pageUrl.Contains("?") ? "&" + routeValue : "?" + routeValue; + + attributeList.Remove(hrefAttribute); + attributeList.Add(new TagHelperAttribute("href", pageUrl, hrefAttribute.ValueStyle)); + } + } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/PagerModel.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/PagerModel.cs index 8c6eeb34a4..db74266f38 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/PagerModel.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/PagerModel.cs @@ -38,7 +38,8 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination PageSize = pageSize; TotalPageCount = (int)Math.Ceiling(Convert.ToDouble((decimal)TotalItemsCount / PageSize)); Sort = sort; - PageUrl = pageUrl; + + PageUrl = pageUrl?.EnsureStartsWith('/') ?? "/"; if (currentPage > TotalPageCount) { diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRModule.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRModule.cs index e26de184af..b89e5e9158 100644 --- a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRModule.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; using Volo.Abp.Modularity; namespace Volo.Abp.AspNetCore.SignalR @@ -30,7 +31,7 @@ namespace Volo.Abp.AspNetCore.SignalR public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddSignalR(); - + Configure(options => { options.EndpointConfigureActions.Add(endpointContext => @@ -65,7 +66,7 @@ namespace Volo.Abp.AspNetCore.SignalR services.OnRegistred(context => { - if (typeof(Hub).IsAssignableFrom(context.ImplementationType)) + if (IsHubClass(context) && !IsDisabledForAutoMap(context)) { hubTypes.Add(context.ImplementationType); } @@ -80,7 +81,17 @@ namespace Volo.Abp.AspNetCore.SignalR }); } - private void MapHubType( + private static bool IsHubClass(IOnServiceRegistredContext context) + { + return typeof(Hub).IsAssignableFrom(context.ImplementationType); + } + + private static bool IsDisabledForAutoMap(IOnServiceRegistredContext context) + { + return context.ImplementationType.IsDefined(typeof(DisableAutoHubMapAttribute), true); + } + + private void MapHubType( Type hubType, IEndpointRouteBuilder endpoints, string pattern, @@ -101,8 +112,8 @@ namespace Volo.Abp.AspNetCore.SignalR // ReSharper disable once UnusedMember.Local (used via reflection) private static void MapHub( - IEndpointRouteBuilder endpoints, - string pattern, + IEndpointRouteBuilder endpoints, + string pattern, Action configureOptions) where THub : Hub { diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions.cs index c0a07a2ea3..9adbe8d7a6 100644 --- a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions.cs +++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions.cs @@ -1,14 +1,12 @@ -using System.Collections.Generic; - -namespace Volo.Abp.AspNetCore.SignalR +namespace Volo.Abp.AspNetCore.SignalR { public class AbpSignalROptions { - public List Hubs { get; } + public HubConfigList Hubs { get; } public AbpSignalROptions() { - Hubs = new List(); + Hubs = new HubConfigList(); } } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/DisableAutoHubMapAttribute.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/DisableAutoHubMapAttribute.cs new file mode 100644 index 0000000000..2d2489bda9 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/DisableAutoHubMapAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace Volo.Abp.AspNetCore.SignalR +{ + public class DisableAutoHubMapAttribute : Attribute + { + + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfig.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfig.cs index 59c9342fce..be2e2a6877 100644 --- a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfig.cs +++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfig.cs @@ -19,11 +19,17 @@ namespace Volo.Abp.AspNetCore.SignalR public HubConfig( [NotNull] Type hubType, - [NotNull] string routePattern) + [NotNull] string routePattern, + [CanBeNull] Action configureAction = null) { HubType = Check.NotNull(hubType, nameof(hubType)); RoutePattern = Check.NotNullOrWhiteSpace(routePattern, nameof(routePattern)); ConfigureActions = new List>(); + + if (configureAction != null) + { + ConfigureActions.Add(configureAction); + } } public static HubConfig Create() diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfigList.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfigList.cs new file mode 100644 index 0000000000..434e50e515 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfigList.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Volo.Abp.AspNetCore.SignalR +{ + public class HubConfigList : List + { + public void AddOrUpdate(Action configAction = null) + { + AddOrUpdate(typeof(THub)); + } + + public void AddOrUpdate(Type hubType, Action configAction = null) + { + var hubConfig = this.GetOrAdd( + c => c.HubType == hubType, + () => HubConfig.Create(hubType) + ); + + configAction?.Invoke(hubConfig); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubRouteAttribute.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubRouteAttribute.cs index 5de916a89d..0373fbf691 100644 --- a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubRouteAttribute.cs +++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubRouteAttribute.cs @@ -13,6 +13,11 @@ namespace Volo.Abp.AspNetCore.SignalR RoutePattern = routePattern; } + public virtual string GetRoutePatternForType(Type hubType) + { + return RoutePattern; + } + public static string GetRoutePattern() where THub : Hub { @@ -24,7 +29,7 @@ namespace Volo.Abp.AspNetCore.SignalR var routeAttribute = hubType.GetSingleAttributeOrNull(); if (routeAttribute != null) { - return routeAttribute.RoutePattern; + return routeAttribute.GetRoutePatternForType(hubType); } return "/signalr-hubs/" + hubType.Name.RemovePostFix("Hub").ToKebabCase(); diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/es.json b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/es.json new file mode 100644 index 0000000000..33c59be8b5 --- /dev/null +++ b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/es.json @@ -0,0 +1,23 @@ +{ + "culture": "es", + "texts": { + "DisplayName:Abp.Mailing.DefaultFromAddress": "Direccin de envo", + "DisplayName:Abp.Mailing.DefaultFromDisplayName": "Nombre de envo", + "DisplayName:Abp.Mailing.Smtp.Host": "Host", + "DisplayName:Abp.Mailing.Smtp.Port": "Puerto", + "DisplayName:Abp.Mailing.Smtp.UserName": "Nombre de usuario", + "DisplayName:Abp.Mailing.Smtp.Password": "Contrasea", + "DisplayName:Abp.Mailing.Smtp.Domain": "Dominio", + "DisplayName:Abp.Mailing.Smtp.EnableSsl": "Habilitar SSL", + "DisplayName:Abp.Mailing.Smtp.UseDefaultCredentials": "Usar las credenciales por defecto", + "Description:Abp.Mailing.DefaultFromAddress": "La direccin de envo por defecto", + "Description:Abp.Mailing.DefaultFromDisplayName": "El nombre de envo por defecto", + "Description:Abp.Mailing.Smtp.Host": "El nombre o direccin del host para transacciones SMTP.", + "Description:Abp.Mailing.Smtp.Port": "El puerto usado para transacciones SMTP.", + "Description:Abp.Mailing.Smtp.UserName": "Nombre de usuario asociado a las credenciales.", + "Description:Abp.Mailing.Smtp.Password": "La contrasea del ususario asociado a las credenciales.", + "Description:Abp.Mailing.Smtp.Domain": "El dominio o equipo que valida las credenciales.", + "Description:Abp.Mailing.Smtp.EnableSsl": "Si SmtpClient usa Secure Sockets Layer (SSL) para encriptar la coneccin.", + "Description:Abp.Mailing.Smtp.UseDefaultCredentials": "Si las credenciales por defecto se envan con los request." + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Localization/Volo.Abp.Localization.csproj b/framework/src/Volo.Abp.Localization/Volo.Abp.Localization.csproj index 0fd10d94ca..88c3bf0e81 100644 --- a/framework/src/Volo.Abp.Localization/Volo.Abp.Localization.csproj +++ b/framework/src/Volo.Abp.Localization/Volo.Abp.Localization.csproj @@ -28,5 +28,4 @@ - diff --git a/framework/src/Volo.Abp.TextTemplating/Volo/Abp/TextTemplating/VirtualFiles/LocalizedTemplateContentReaderFactory.cs b/framework/src/Volo.Abp.TextTemplating/Volo/Abp/TextTemplating/VirtualFiles/LocalizedTemplateContentReaderFactory.cs index b30e5771f4..02b857bf5d 100644 --- a/framework/src/Volo.Abp.TextTemplating/Volo/Abp/TextTemplating/VirtualFiles/LocalizedTemplateContentReaderFactory.cs +++ b/framework/src/Volo.Abp.TextTemplating/Volo/Abp/TextTemplating/VirtualFiles/LocalizedTemplateContentReaderFactory.cs @@ -1,7 +1,7 @@ -using System; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; +using Nito.AsyncEx; using Volo.Abp.DependencyInjection; using Volo.Abp.VirtualFileSystem; @@ -9,45 +9,34 @@ namespace Volo.Abp.TextTemplating.VirtualFiles { public class LocalizedTemplateContentReaderFactory : ILocalizedTemplateContentReaderFactory, ISingletonDependency { - private readonly IVirtualFileProvider _virtualFileProvider; - private readonly Dictionary _readerCache; - private readonly ReaderWriterLockSlim _lock; + protected IVirtualFileProvider VirtualFileProvider { get; } + protected ConcurrentDictionary ReaderCache { get; } + protected SemaphoreSlim SyncObj; public LocalizedTemplateContentReaderFactory(IVirtualFileProvider virtualFileProvider) { - _virtualFileProvider = virtualFileProvider; - _readerCache = new Dictionary(); - _lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); + VirtualFileProvider = virtualFileProvider; + ReaderCache = new ConcurrentDictionary(); + SyncObj = new SemaphoreSlim(1, 1); } - public async Task CreateAsync(TemplateDefinition templateDefinition) + public virtual async Task CreateAsync(TemplateDefinition templateDefinition) { - _lock.EnterUpgradeableReadLock(); + if (ReaderCache.TryGetValue(templateDefinition.Name, out var reader)) + { + return reader; + } - try + using (await SyncObj.LockAsync()) { - var reader = _readerCache.GetOrDefault(templateDefinition.Name); - if (reader != null) + if (ReaderCache.TryGetValue(templateDefinition.Name, out reader)) { return reader; } - _lock.EnterWriteLock(); - - try - { - reader = await CreateInternalAsync(templateDefinition); - _readerCache[templateDefinition.Name] = reader; - return reader; - } - finally - { - _lock.ExitWriteLock(); - } - } - finally - { - _lock.ExitUpgradeableReadLock(); + reader = await CreateInternalAsync(templateDefinition); + ReaderCache[templateDefinition.Name] = reader; + return reader; } } @@ -60,7 +49,7 @@ namespace Volo.Abp.TextTemplating.VirtualFiles return NullLocalizedTemplateContentReader.Instance; } - var fileInfo = _virtualFileProvider.GetFileInfo(virtualPath); + var fileInfo = VirtualFileProvider.GetFileInfo(virtualPath); if (!fileInfo.Exists) { throw new AbpException("Could not find a file/folder at the location: " + virtualPath); @@ -69,7 +58,7 @@ namespace Volo.Abp.TextTemplating.VirtualFiles if (fileInfo.IsDirectory) { var folderReader = new VirtualFolderLocalizedTemplateContentReader(); - await folderReader.ReadContentsAsync(_virtualFileProvider, virtualPath); + await folderReader.ReadContentsAsync(VirtualFileProvider, virtualPath); return folderReader; } else //File diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/es.json b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/es.json index ba27a9193c..0fc6660fac 100644 --- a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/es.json +++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/Localization/Resource/es.json @@ -1,6 +1,6 @@ { "culture": "es", "texts": { - "Menu:Administration": "Administracin" + "Menu:Administration": "Administración" } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json index b52c56510b..be137f013b 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json @@ -1,20 +1,20 @@ { "culture": "es", "texts": { - "InternalServerErrorMessage": "Ocurri un error interno en su pedido!", - "ValidationErrorMessage": "Su pedido no es vlido!", - "ValidationNarrativeErrorMessageTitle": "Los siguientes errores se encontraton durante la validacin.", - "DefaultErrorMessage": "Ocurri un error!", - "DefaultErrorMessageDetail": "El servidor no envi el detalle del error.", - "DefaultErrorMessage401": "Usted no ha iniciado sessin!", - "DefaultErrorMessage401Detail": "Debe iniciar sessin para ejecutar esta operacin.", - "DefaultErrorMessage403": "Usted no est autorizado!", - "DefaultErrorMessage403Detail": "Usted no puede hacer esta operacin!", - "DefaultErrorMessage404": "No se encontr el recurso!", - "DefaultErrorMessage404Detail": "El recurso solicitado no se encontr en el servidor!", + "InternalServerErrorMessage": "Ocurrió un error interno en su pedido!", + "ValidationErrorMessage": "Su pedido no es válido!", + "ValidationNarrativeErrorMessageTitle": "Los siguientes errores se encontraton durante la validación.", + "DefaultErrorMessage": "Ocurrió un error!", + "DefaultErrorMessageDetail": "El servidor no envió el detalle del error.", + "DefaultErrorMessage401": "Usted no ha iniciado sessión!", + "DefaultErrorMessage401Detail": "Debe iniciar sessión para ejecutar esta operación.", + "DefaultErrorMessage403": "Usted no está autorizado!", + "DefaultErrorMessage403Detail": "Usted no puede hacer esta operación!", + "DefaultErrorMessage404": "No se encontró el recurso!", + "DefaultErrorMessage404Detail": "El recurso solicitado no se encontró en el servidor!", "EntityNotFoundErrorMessage": "No hay una entidad {0} con el id = {1}!", "Error": "Error", - "AreYouSure": "Est seguro?", + "AreYouSure": "¿Está seguro?", "Cancel": "Cancelar", "Yes": "Si", "No": "No", @@ -30,14 +30,14 @@ "Welcome": "Bienvenido", "Login": "Iniciar session", "Register": "Registrarse", - "Logout": "Cerrar sessin", + "Logout": "Cerrar sessión", "Submit": "Enviar", - "Back": "Atrs", + "Back": "Atrás", "PagerSearch": "Buscar", "PagerNext": "Siguiente", "PagerPrevious": "Anterior", "PagerFirst": "Primero", - "PagerLast": "ltimo", + "PagerLast": "Último", "PagerInfo": "Mostrando desde _START_ hasta _END_ total _TOTAL_", "PagerInfo{0}{1}{2}": "Mostrando desde {0} hasta {1} total {2}", "PagerInfoEmpty": "Mostrando desde 0 hasta 0 de 0 registros", @@ -45,15 +45,15 @@ "NoDataAvailableInDatatable": "No hay datos", "PagerShowMenuEntries": "Mostrar _MENU_ registros", "DatatableActionDropdownDefaultText": "Acciones", - "ChangePassword": "Cambiar contrasea", + "ChangePassword": "Cambiar contraseña", "PersonalInfo": "My perfil", "AreYouSureYouWantToCancelEditingWarningMessage": "Tiene cambios sin guardar.", - "UnhandledException": "Excepcin no controlada!", + "UnhandledException": "Excepción no controlada!", "401Message": "No autorizado", "403Message": "Prohibido", - "404Message": "La pgina no existe", + "404Message": "La página no existe", "500Message": "Error interno del servidor", - "GoHomePage": "Ir a la pgina principal", + "GoHomePage": "Ir a la página principal", "GoBack": "Atras" } } diff --git a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/es.json b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/es.json index 3f84a158aa..bb7cb733fe 100644 --- a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/es.json +++ b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/es.json @@ -1,4 +1,4 @@ -{ +{ "culture": "es", "texts": { "'{0}' and '{1}' do not match.": "'{0} \" y \"{1} \" no coinciden.", @@ -11,7 +11,7 @@ "The {0} field is not a valid phone number.": "{0} no es un número de teléfono válido.", "The field {0} must be between {1} and {2}.": "El campo {0} debe ser entre {1} y {2}.", "The field {0} must match the regular expression '{1}'.": "El campo {0} no coincide con el formato solicitado.", - "The {0} field is required.": "La {0} campo es obligatorio.", + "The {0} field is required.": "El {0} campo es obligatorio.", "The field {0} must be a string with a maximum length of {1}.": "El campo {0} debe ser una cadena con una longitud máxima de {1}.", "The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "El campo {0} debe ser una cadena con una longitud mínima de {2} y una longitud máxima de {1}.", "The {0} field is not a valid fully-qualified http, https, or ftp URL.": "El campo {0} no es una dirección URL válida http, https o ftp.", diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Paginator.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Paginator.cshtml index be26fb5251..b13e1bc4ef 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Paginator.cshtml +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Paginator.cshtml @@ -47,7 +47,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components public void OnGet(int currentPage, string sort) { - PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort); + PagerModel = new PagerModel(100, 10, currentPage, 10, "/Components/Paginator", sort); } } } @@ -60,7 +60,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components

-<div class="row mt-3">    
+<div class="row mt-3">
     <div class="col-sm-12 col-md-5">
         Showing 80 to 90 of 100 entries.
     </div>
@@ -105,4 +105,4 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components
             
         
     
-
\ No newline at end of file
+
diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Paginator.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Paginator.cshtml.cs
index 15b31db6da..3723631381 100644
--- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Paginator.cshtml.cs
+++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Pages/Components/Paginator.cshtml.cs
@@ -9,7 +9,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo.Pages.Components
 
         public void OnGet(int currentPage, string sort)
         {
-            PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort);
+            PagerModel = new PagerModel(100, 10, currentPage, 10, "/Components/Paginator", sort);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml
index 6eb48dcd43..d987bce207 100644
--- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml
+++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml
@@ -7,14 +7,9 @@
 @{
     PageLayout.Content.Title = "Paginator";
 }
-@section scripts {
-    
-        
-    
-}
 
 

Paginator

Check the ABP Documentation.

-@await Component.InvokeAsync(typeof(PaginatorDemoViewComponent), new { pagerModel = Model.PagerModel }) \ No newline at end of file +@await Component.InvokeAsync(typeof(PaginatorDemoViewComponent), new { pagerModel = Model.PagerModel }) diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml.cs index 50659a0ab4..bc51f80f0a 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.cshtml.cs @@ -9,7 +9,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo.Pages.Components.Paginator public void OnGet(int currentPage = 1, string sort = null) { - PagerModel = new PagerModel(100, 10, currentPage, 10, "Paginator", sort); + PagerModel = new PagerModel(100, 10, currentPage, 10, "/Components/Paginator", sort); } } -} \ No newline at end of file +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.js b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.js deleted file mode 100644 index 843e0dea00..0000000000 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/Pages/Components/Paginator/Index.js +++ /dev/null @@ -1,9 +0,0 @@ -$(function () { - var links = $("a.page-link"); - - $.each(links, function (key, value) { - var oldUrl = links[key].getAttribute("href"); - var value = Number(oldUrl.match(/currentPage=(\d+)&page/)[1]); - links[key].setAttribute("href", "/Components/Paginator?currentPage=" + value); - }) -}); \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo.Abp.AspNetCore.SignalR.Tests.csproj b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo.Abp.AspNetCore.SignalR.Tests.csproj new file mode 100644 index 0000000000..8e1ab29329 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo.Abp.AspNetCore.SignalR.Tests.csproj @@ -0,0 +1,20 @@ + + + + + + netcoreapp3.1 + + + + + + + + + + + + + + diff --git a/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRTestBase.cs b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRTestBase.cs new file mode 100644 index 0000000000..eb3cb0c265 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRTestBase.cs @@ -0,0 +1,12 @@ +using Volo.Abp.Testing; + +namespace Volo.Abp.AspNetCore.SignalR +{ + public abstract class AbpAspNetCoreSignalRTestBase : AbpIntegratedTest + { + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) + { + options.UseAutofac(); + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRTestModule.cs b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRTestModule.cs new file mode 100644 index 0000000000..eee1dd6ed3 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRTestModule.cs @@ -0,0 +1,15 @@ +using Volo.Abp.Autofac; +using Volo.Abp.Modularity; + +namespace Volo.Abp.AspNetCore.SignalR +{ + [DependsOn( + typeof(AbpAspNetCoreSignalRModule), + typeof(AbpTestBaseModule), + typeof(AbpAutofacModule) + )] + public class AbpAspNetCoreSignalRTestModule : AbpModule + { + + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions_Tests.cs b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions_Tests.cs new file mode 100644 index 0000000000..38e262abe3 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions_Tests.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Options; +using Shouldly; +using Volo.Abp.AspNetCore.SignalR.SampleHubs; +using Xunit; + +namespace Volo.Abp.AspNetCore.SignalR +{ + public class AbpSignalROptions_Tests : AbpAspNetCoreSignalRTestBase + { + private readonly AbpSignalROptions _options; + + public AbpSignalROptions_Tests() + { + _options = GetRequiredService>().Value; + } + + [Fact] + public void Should_Auto_Add_Maps() + { + _options.Hubs.ShouldContain(h => h.HubType == typeof(RegularHub)); + _options.Hubs.ShouldContain(h => h.HubType == typeof(RegularAbpHub)); + _options.Hubs.ShouldNotContain(h => h.HubType == typeof(DisableConventionalRegistrationHub)); + _options.Hubs.ShouldNotContain(h => h.HubType == typeof(DisableAutoHubMapHub)); + } + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/DisableAutoHubMapHub.cs b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/DisableAutoHubMapHub.cs new file mode 100644 index 0000000000..3ceae70af4 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/DisableAutoHubMapHub.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.SignalR; + +namespace Volo.Abp.AspNetCore.SignalR.SampleHubs +{ + [DisableAutoHubMap] + public class DisableAutoHubMapHub : Hub + { + + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/DisableConventionalRegistrationHub.cs b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/DisableConventionalRegistrationHub.cs new file mode 100644 index 0000000000..ff28b6eca9 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/DisableConventionalRegistrationHub.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.SignalR; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AspNetCore.SignalR.SampleHubs +{ + [DisableConventionalRegistration] + public class DisableConventionalRegistrationHub : Hub + { + + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/RegularAbpHub.cs b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/RegularAbpHub.cs new file mode 100644 index 0000000000..2afceffa8d --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/RegularAbpHub.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.AspNetCore.SignalR.SampleHubs +{ + public class RegularAbpHub : AbpHub + { + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/RegularHub.cs b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/RegularHub.cs new file mode 100644 index 0000000000..8d3987f314 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.SignalR.Tests/Volo/Abp/AspNetCore/SignalR/SampleHubs/RegularHub.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.SignalR; + +namespace Volo.Abp.AspNetCore.SignalR.SampleHubs +{ + public class RegularHub : Hub + { + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/CountryNames/es.json b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/CountryNames/es.json index d9b29895b8..13a6c2db61 100644 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/CountryNames/es.json +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/CountryNames/es.json @@ -1,4 +1,4 @@ -{ +{ "culture": "es", "texts": { "USA": "Estados unidos de América", diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/Validation/es.json b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/Validation/es.json index 04e85b5f12..a83091e2c8 100644 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/Validation/es.json +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Base/Validation/es.json @@ -1,4 +1,4 @@ -{ +{ "culture": "es", "texts": { "ThisFieldIsRequired": "El campo no puede estar vacío", diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/es.json b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/es.json index 7b68ff6f2c..f68ed052c3 100644 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/es.json +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/Source/es.json @@ -1,4 +1,4 @@ -{ +{ "culture": "es", "texts": { "Hello {0}.": "Hola {0}.", diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/es.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/es.json new file mode 100644 index 0000000000..43554b2311 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/es.json @@ -0,0 +1,45 @@ +{ + "culture": "es", + "texts": { + "UserName": "Nombre de usuario", + "EmailAddress": "Dirección de correo electrónico", + "UserNameOrEmailAddress": "Nombre de usuario o dirección de correo electrónico", + "Password": "Contraseña", + "RememberMe": "Recuérdame", + "UseAnotherServiceToLogin": "Usar otro servicio para iniciar sesión", + "UserLockedOutMessage": "La cuenta de usuario ha sido bloqueada debido a los intentos de inicio de sesión no válidos. Por favor, espere unos minutos y vuelve a intentarlo.", + "InvalidUserNameOrPassword": "El nombre de usuario o la contraseña no son válidos", + "LoginIsNotAllowed": "No está permitido el inicio de sesión! Usted tendrá que confirmar su correo electrónico o número de teléfono.", + "SelfRegistrationDisabledMessage": "El autoregistro de usuario está deshabilitado para esta aplicación. Póngase en contacto con el administrador de la aplicación para registrar un nuevo usuario.", + "LocalLoginDisabledMessage": "No está habilitado el login local para esta aplicación.", + "Login": "Iniciar sesión", + "Cancel": "Cancelar", + "Register": "Registrarse", + "AreYouANewUser": "¿Usted es un usuario nuevo?", + "AlreadyRegistered": "¿Está registrado en esta aplicación?", + "InvalidLoginRequest": "Solicitud de inicio de sesión no válido", + "ThereAreNoLoginSchemesConfiguredForThisClient": "No hay ningún esquema de inicio de sesión configurado para este cliente.", + "LogInUsingYourProviderAccount": "Inicia sesión con tu cuenta de {0} ", + "DisplayName:CurrentPassword": "Contraseña actual", + "DisplayName:NewPassword": "Nueva contraseña", + "DisplayName:NewPasswordConfirm": "Confirmar nueva contraseña", + "PasswordChangedMessage": "Su contraseña ha sido cambiada con éxito.", + "DisplayName:UserName": "Nombre de usuario", + "DisplayName:Email": "Correo electrónico", + "DisplayName:Name": "Nombre", + "DisplayName:Surname": "Apellido", + "DisplayName:Password": "Contraseña", + "DisplayName:EmailAddress": "Dirección de correo electrónico", + "DisplayName:PhoneNumber": "Número de teléfono", + "PersonalSettings": "Configuración Personal", + "PersonalSettingsSaved": "Ajustes personales guardados", + "PasswordChanged": "Cambiar la contraseña", + "NewPasswordConfirmFailed": "Por favor, confirme la nueva contraseña.", + "Manage": "Administrar", + "ManageYourProfile": "Gestionar su perfil", + "DisplayName:Abp.Account.IsSelfRegistrationEnabled": "Habilitar el registro de usuario", + "Description:Abp.Account.IsSelfRegistrationEnabled": "Si está habilitado los usuarios pueden crear una cuenta mediante el registro automático.", + "DisplayName:Abp.Account.EnableLocalLogin": "Habilitar cuenta local", + "Description:Abp.Account.EnableLocalLogin": "Indica que el servidor permite iniciar sessión con una cuenta local." + } +} \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/zh-Hant.json b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/zh-Hant.json index 240b7babdf..8ad18c7c1c 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/zh-Hant.json +++ b/modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/zh-Hant.json @@ -82,6 +82,7 @@ "DisplayName:Abp.Identity.Lockout.LockoutDuration": "鎖定時間(秒)", "DisplayName:Abp.Identity.Lockout.MaxFailedAccessAttempts": "最大失敗存取嘗試次數", "DisplayName:Abp.Identity.SignIn.RequireConfirmedEmail": "要求驗證的電子信箱", + "DisplayName:Abp.Identity.SignIn.EnablePhoneNumberConfirmation": "啟用手機號碼驗證", "DisplayName:Abp.Identity.SignIn.RequireConfirmedPhoneNumber": "要求驗證的手機號碼", "DisplayName:Abp.Identity.User.IsUserNameUpdateEnabled": "啟用使用者名稱更新", "DisplayName:Abp.Identity.User.IsEmailUpdateEnabled": "啟用電子信箱更新", @@ -95,9 +96,9 @@ "Description:Abp.Identity.Lockout.LockoutDuration": "當鎖定發生時使用者被鎖定的時間(秒).", "Description:Abp.Identity.Lockout.MaxFailedAccessAttempts": "如果啟用鎖定,當使用者被鎖定前失敗的存取嘗試次數.", "Description:Abp.Identity.SignIn.RequireConfirmedEmail": "登入時是否需要驗證電子信箱.", + "Description:Abp.Identity.SignIn.EnablePhoneNumberConfirmation": "使用者手機號碼是否需要驗證.", "Description:Abp.Identity.SignIn.RequireConfirmedPhoneNumber": "登入時是否需要驗證手機號碼.", "Description:Abp.Identity.User.IsUserNameUpdateEnabled": "是否允許使用者更新使用者名稱.", "Description:Abp.Identity.User.IsEmailUpdateEnabled": "是否允許使用者更新電子信箱." - } -} \ No newline at end of file +} diff --git a/npm/ng-packs/apps/dev-app/src/environments/environment.prod.ts b/npm/ng-packs/apps/dev-app/src/environments/environment.prod.ts index 5b42ebace3..ae0e6aba2c 100644 --- a/npm/ng-packs/apps/dev-app/src/environments/environment.prod.ts +++ b/npm/ng-packs/apps/dev-app/src/environments/environment.prod.ts @@ -19,7 +19,4 @@ export const environment = { url: 'https://localhost:44305', }, }, - localization: { - defaultResourceName: 'MyProjectName', - }, }; diff --git a/npm/ng-packs/apps/dev-app/src/environments/environment.ts b/npm/ng-packs/apps/dev-app/src/environments/environment.ts index ca462ff043..e678b55a74 100644 --- a/npm/ng-packs/apps/dev-app/src/environments/environment.ts +++ b/npm/ng-packs/apps/dev-app/src/environments/environment.ts @@ -19,7 +19,4 @@ export const environment = { url: 'https://localhost:44305', }, }, - localization: { - defaultResourceName: 'MyProjectName', - }, }; diff --git a/npm/ng-packs/packages/core/src/lib/actions/session.actions.ts b/npm/ng-packs/packages/core/src/lib/actions/session.actions.ts index 463e9a202d..a7d2cd9f3c 100644 --- a/npm/ng-packs/packages/core/src/lib/actions/session.actions.ts +++ b/npm/ng-packs/packages/core/src/lib/actions/session.actions.ts @@ -2,7 +2,7 @@ import { ABP } from '../models'; export class SetLanguage { static readonly type = '[Session] Set Language'; - constructor(public payload: string) {} + constructor(public payload: string, public dispatchAppConfiguration?: boolean) {} } export class SetTenant { static readonly type = '[Session] Set Tenant'; diff --git a/npm/ng-packs/packages/core/src/lib/models/application-configuration.ts b/npm/ng-packs/packages/core/src/lib/models/application-configuration.ts index 0a4377f4d2..b52afe9faf 100644 --- a/npm/ng-packs/packages/core/src/lib/models/application-configuration.ts +++ b/npm/ng-packs/packages/core/src/lib/models/application-configuration.ts @@ -66,5 +66,6 @@ export namespace ApplicationConfiguration { id: string; tenantId: string; userName: string; + email: string; } } diff --git a/npm/ng-packs/packages/core/src/lib/models/config.ts b/npm/ng-packs/packages/core/src/lib/models/config.ts index 0f7405380b..2169f75643 100644 --- a/npm/ng-packs/packages/core/src/lib/models/config.ts +++ b/npm/ng-packs/packages/core/src/lib/models/config.ts @@ -16,7 +16,7 @@ export namespace Config { hmr?: boolean; oAuthConfig: AuthConfig; apis: Apis; - localization: { defaultResourceName: string }; + localization?: { defaultResourceName?: string }; } export interface Application { diff --git a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts index 0e89732e56..8adef08f46 100644 --- a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts @@ -1,11 +1,12 @@ import { Injectable, NgZone, Optional, SkipSelf } from '@angular/core'; import { ActivatedRouteSnapshot, Router } from '@angular/router'; -import { Store, Actions, ofActionSuccessful } from '@ngxs/store'; +import { Actions, ofActionSuccessful, Store } from '@ngxs/store'; import { noop, Observable } from 'rxjs'; +import { SetLanguage } from '../actions/session.actions'; +import { ApplicationConfiguration } from '../models/application-configuration'; +import { Config } from '../models/config'; import { ConfigState } from '../states/config.state'; import { registerLocale } from '../utils/initial-utils'; -import { Config } from '../models/config'; -import { SetLanguage } from '../actions/session.actions'; type ShouldReuseRoute = (future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot) => boolean; @@ -75,4 +76,31 @@ export class LocalizationService { instant(key: string | Config.LocalizationWithDefault, ...interpolateParams: string[]): string { return this.store.selectSnapshot(ConfigState.getLocalization(key, ...interpolateParams)); } + + isLocalized(key, sourceName) { + if (sourceName === '_') { + // A convention to suppress the localization + return true; + } + + const localization = this.store.selectSnapshot( + ConfigState.getOne('localization'), + ) as ApplicationConfiguration.Localization; + sourceName = sourceName || localization.defaultResourceName; + if (!sourceName) { + return false; + } + + const source = localization.values[sourceName]; + if (!source) { + return false; + } + + const value = source[key]; + if (value === undefined) { + return false; + } + + return true; + } } diff --git a/npm/ng-packs/packages/core/src/lib/states/config.state.ts b/npm/ng-packs/packages/core/src/lib/states/config.state.ts index 546b3d0b1e..5c5338a948 100644 --- a/npm/ng-packs/packages/core/src/lib/states/config.state.ts +++ b/npm/ng-packs/packages/core/src/lib/states/config.state.ts @@ -155,33 +155,45 @@ export class ConfigState { } const keys = key.split('::') as string[]; - const selector = createSelector([ConfigState], (state: Config.State) => { - if (!state.localization) return defaultValue || key; - - const defaultResourceName = snq(() => state.environment.localization.defaultResourceName); - if (keys[0] === '') { - if (!defaultResourceName) { - throw new Error( - `Please check your environment. May you forget set defaultResourceName? - Here is the example: - { production: false, - localization: { - defaultResourceName: 'MyProjectName' - } - }`, - ); - } + const selector = createSelector([ConfigState], (state: Config.State): string => { + const warn = (message: string) => { + if (!state.environment.production) console.warn(message); + }; - keys[0] = defaultResourceName; + if (keys.length < 2) { + warn('The localization source separator (::) not found.'); + return defaultValue || (key as string); } + if (!state.localization) return defaultValue || keys[1]; - let localization = (keys as any).reduce((acc, val) => { - if (acc) { - return acc[val]; - } + const sourceName = + keys[0] || + snq(() => state.environment.localization.defaultResourceName) || + state.localization.defaultResourceName; + const sourceKey = keys[1]; - return undefined; - }, state.localization.values); + if (sourceName === '_') { + return defaultValue || sourceKey; + } + + if (!sourceName) { + warn( + 'Localization source name is not specified and the defaultResourceName was not defined!', + ); + + return defaultValue || sourceKey; + } + + const source = state.localization.values[sourceName]; + if (!source) { + warn('Could not find localization source: ' + sourceName); + return defaultValue || sourceKey; + } + + let localization = source[sourceKey]; + if (typeof localization === 'undefined') { + return defaultValue || sourceKey; + } interpolateParams = interpolateParams.filter(params => params != null); if (localization && interpolateParams && interpolateParams.length) { @@ -191,7 +203,7 @@ export class ConfigState { } if (typeof localization !== 'string') localization = ''; - return localization || defaultValue || key; + return localization || defaultValue || (key as string); }); return selector; @@ -219,9 +231,13 @@ export class ConfigState { defaultLang = defaultLang.split(';')[0]; } + document.documentElement.setAttribute( + 'lang', + configuration.localization.currentCulture.cultureName, + ); return this.store.selectSnapshot(SessionState.getLanguage) ? of(null) - : dispatch(new SetLanguage(defaultLang)); + : dispatch(new SetLanguage(defaultLang, false)); }), catchError(err => { dispatch(new RestOccurError(new HttpErrorResponse({ status: 0, error: err }))); diff --git a/npm/ng-packs/packages/core/src/lib/states/session.state.ts b/npm/ng-packs/packages/core/src/lib/states/session.state.ts index 6acbf1d60d..0c81a46c58 100644 --- a/npm/ng-packs/packages/core/src/lib/states/session.state.ts +++ b/npm/ng-packs/packages/core/src/lib/states/session.state.ts @@ -69,12 +69,15 @@ export class SessionState { } @Action(SetLanguage) - setLanguage({ patchState, dispatch }: StateContext, { payload }: SetLanguage) { + setLanguage( + { patchState, dispatch }: StateContext, + { payload, dispatchAppConfiguration = true }: SetLanguage, + ) { patchState({ language: payload, }); - return dispatch(new GetAppConfiguration()); + if (dispatchAppConfiguration) return dispatch(new GetAppConfiguration()); } @Action(SetTenant) diff --git a/npm/ng-packs/packages/core/src/lib/tests/config-state.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/config-state.service.spec.ts index d019bda84b..17de9efb9e 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/config-state.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/config-state.service.spec.ts @@ -122,6 +122,7 @@ const CONFIG_STATE_DATA = { id: null, tenantId: null, userName: null, + email: null, }, features: { values: {}, diff --git a/npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts index 3df3abc63e..e823328f73 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts @@ -146,6 +146,7 @@ export const CONFIG_STATE_DATA = { id: null, tenantId: null, userName: null, + email: null, }, features: { values: {}, @@ -271,7 +272,7 @@ describe('ConfigState', () => { ); expect(ConfigState.getLocalization('AbpIdentity::NoIdentity')(CONFIG_STATE_DATA)).toBe( - 'AbpIdentity::NoIdentity', + 'NoIdentity', ); expect( @@ -286,18 +287,15 @@ describe('ConfigState', () => { )(CONFIG_STATE_DATA), ).toBe('first and second do not match.'); - try { + expect( ConfigState.getLocalization('::Test')({ ...CONFIG_STATE_DATA, environment: { ...CONFIG_STATE_DATA.environment, localization: {} as any, }, - }); - expect(false).toBeTruthy(); // fail - } catch (error) { - expect((error as Error).message).toContain('Please check your environment'); - } + }), + ).toBe('Test'); }); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/localization.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/localization.service.spec.ts index 5600e3aa39..8c5b859f9f 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/localization.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/localization.service.spec.ts @@ -44,13 +44,7 @@ describe('LocalizationService', () => { describe('#instant', () => { it('should be return a localization', () => { - store.selectSnapshot.andCallFake( - (selector: (state: any, ...states: any[]) => Observable) => { - return selector({ - ConfigState: { getLocalization: (keys, ...interpolateParams) => keys }, - }); - }, - ); + store.selectSnapshot.andReturn('AbpTest'); expect(service.instant('AbpTest')).toBe('AbpTest'); }); diff --git a/npm/ng-packs/scripts/publish.ts b/npm/ng-packs/scripts/publish.ts index 5570602813..9228a752ca 100644 --- a/npm/ng-packs/scripts/publish.ts +++ b/npm/ng-packs/scripts/publish.ts @@ -28,7 +28,15 @@ const publish = async () => { await execa( 'yarn', - ['lerna', 'version', program.nextVersion, '--yes', '--no-commit-hooks', '--skip-git'], + [ + 'lerna', + 'version', + program.nextVersion, + '--yes', + '--no-commit-hooks', + '--skip-git', + '--force-publish', + ], { stdout: 'inherit', cwd: '../' }, ); diff --git a/templates/app/angular/src/environments/environment.prod.ts b/templates/app/angular/src/environments/environment.prod.ts index bf1c0ea488..ada2b74c97 100644 --- a/templates/app/angular/src/environments/environment.prod.ts +++ b/templates/app/angular/src/environments/environment.prod.ts @@ -2,7 +2,7 @@ export const environment = { production: true, application: { name: 'MyProjectName', - logoUrl: '' + logoUrl: '', }, oAuthConfig: { issuer: 'https://localhost:44305', @@ -11,14 +11,11 @@ export const environment = { scope: 'MyProjectName', showDebugInformation: true, oidc: false, - requireHttps: true + requireHttps: true, }, apis: { default: { - url: 'https://localhost:44305' - } + url: 'https://localhost:44305', + }, }, - localization: { - defaultResourceName: 'MyProjectName' - } }; diff --git a/templates/app/angular/src/environments/environment.ts b/templates/app/angular/src/environments/environment.ts index 6f2a182746..d3676e2d08 100644 --- a/templates/app/angular/src/environments/environment.ts +++ b/templates/app/angular/src/environments/environment.ts @@ -2,7 +2,7 @@ export const environment = { production: false, application: { name: 'MyProjectName', - logoUrl: '' + logoUrl: '', }, oAuthConfig: { issuer: 'https://localhost:44305', @@ -11,14 +11,11 @@ export const environment = { scope: 'MyProjectName', showDebugInformation: true, oidc: false, - requireHttps: true + requireHttps: true, }, apis: { default: { - url: 'https://localhost:44305' - } + url: 'https://localhost:44305', + }, }, - localization: { - defaultResourceName: 'MyProjectName' - } }; diff --git a/templates/module/angular/src/environments/environment.prod.ts b/templates/module/angular/src/environments/environment.prod.ts index ce43f45c24..23d41cf375 100644 --- a/templates/module/angular/src/environments/environment.prod.ts +++ b/templates/module/angular/src/environments/environment.prod.ts @@ -18,7 +18,4 @@ export const environment = { url: 'https://localhost:44300', }, }, - localization: { - defaultResourceName: 'MyProjectName', - }, }; diff --git a/templates/module/angular/src/environments/environment.ts b/templates/module/angular/src/environments/environment.ts index f1c21c40ac..32d3aa3d4c 100644 --- a/templates/module/angular/src/environments/environment.ts +++ b/templates/module/angular/src/environments/environment.ts @@ -18,7 +18,4 @@ export const environment = { url: 'https://localhost:44300', }, }, - localization: { - defaultResourceName: 'MyProjectName', - }, };