Browse Source

Merge remote-tracking branch 'origin/dev' into cms-kit/reaction-definitions

pull/7500/head
enisn 5 years ago
parent
commit
dfa5020071
  1. 23
      abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
  2. 18
      abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/tr.json
  3. 25
      abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/zh-Hans.json
  4. 3
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json
  5. 31
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json
  6. 7
      abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hans.json
  7. 2
      docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/How-To-Add-Custom-Property-To-The-User-Entity.md
  8. 101
      docs/en/UI/Angular/HTTP-Requests.md
  9. 2
      docs/en/UI/Angular/Router-Events.md
  10. BIN
      docs/en/UI/Angular/images/custom-error-handler-404-component.jpg
  11. BIN
      docs/en/UI/Angular/images/custom-error-handler-toaster-message.jpg
  12. 2
      docs/zh-Hans/Domain-Driven-Design-Implementation-Guide.md
  13. 6
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpController.cs
  14. 24
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Authentication/ChallengeAccountController.cs
  15. 78
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/MicroserviceServiceRandomPortStep.cs
  16. 54
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveProjectFromTyeStep.cs
  17. 6
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/Microservice/MicroserviceServiceTemplateBase.cs
  18. 10
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/Microservice/MicroserviceTemplateBase.cs
  19. 13
      framework/src/Volo.Abp.Core/Microsoft/Extensions/DependencyInjection/ServiceCollectionLoggingExtensions.cs
  20. 21
      framework/src/Volo.Abp.Core/System/AbpStringExtensions.cs
  21. 27
      framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationBase.cs
  22. 6
      framework/src/Volo.Abp.Core/Volo/Abp/Internal/InternalServiceCollectionExtensions.cs
  23. 20
      framework/src/Volo.Abp.Core/Volo/Abp/Logging/AbpInitLogEntry.cs
  24. 38
      framework/src/Volo.Abp.Core/Volo/Abp/Logging/DefaultInitLogger.cs
  25. 15
      framework/src/Volo.Abp.Core/Volo/Abp/Logging/DefaultInitLoggerFactory.cs
  26. 10
      framework/src/Volo.Abp.Core/Volo/Abp/Logging/IInitLogger.cs
  27. 7
      framework/src/Volo.Abp.Core/Volo/Abp/Logging/IInitLoggerFactory.cs
  28. 15
      framework/src/Volo.Abp.Core/Volo/Abp/Modularity/AbpModuleHelper.cs
  29. 10
      framework/src/Volo.Abp.Core/Volo/Abp/Modularity/ModuleLoader.cs
  30. 12
      framework/src/Volo.Abp.Core/Volo/Abp/Modularity/ModuleManager.cs
  31. 8
      framework/src/Volo.Abp.Core/Volo/Abp/Modularity/PlugIns/PlugInSourceExtensions.cs
  32. 7
      framework/src/Volo.Abp.Core/Volo/Abp/Modularity/PlugIns/PlugInSourceList.cs
  33. 6
      framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/zh-Hans.json
  34. 10
      framework/test/Volo.Abp.Core.Tests/Volo/Abp/Modularity/ModuleLoader_Tests.cs
  35. 1951
      modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20210305122554_BlogPost_CoverImage.Designer.cs
  36. 56
      modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20210305122554_BlogPost_CoverImage.cs
  37. 75
      modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/UnifiedDbContextModelSnapshot.cs
  38. 4
      modules/cms-kit/host/Volo.CmsKit.Web.Unified/Volo.CmsKit.Web.Unified.csproj
  39. 7
      modules/cms-kit/host/Volo.CmsKit.Web.Unified/wwwroot/libs/tui-editor/tui-editor-jquery-patch.js
  40. 2
      modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Blogs/BlogPostDto.cs
  41. 2
      modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Blogs/CreateBlogPostDto.cs
  42. 3
      modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Blogs/IBlogPostAdminAppService.cs
  43. 2
      modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Blogs/UpdateBlogPostDto.cs
  44. 33
      modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Blogs/BlogPostAdminAppService.cs
  45. 3
      modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/MediaDescriptors/MediaDescriptorAdminAppService.cs
  46. 44
      modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi/Volo/CmsKit/Admin/Blogs/BlogPostAdminController.cs
  47. 3
      modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/Create.cshtml.cs
  48. 2
      modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/Update.cshtml
  49. 3
      modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/Update.cshtml.cs
  50. 37
      modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/create.js
  51. 46
      modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/update.js
  52. 122
      modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/zh-Hans.json
  53. 12
      modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogPost.cs
  54. 9
      modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogPostCoverImageContainer.cs
  55. 4
      modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogPostManager.cs
  56. 4
      modules/cms-kit/src/Volo.CmsKit.HttpApi/Volo.CmsKit.HttpApi.csproj
  57. 4
      modules/cms-kit/src/Volo.CmsKit.HttpApi/Volo/CmsKit/CmsKitHttpApiModule.cs
  58. 4
      modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Blogs/BlogPostPublicDto.cs
  59. 2
      modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Blogs/IBlogPostPublicAppService.cs
  60. 13
      modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Blogs/BlogPostPublicAppService.cs
  61. 12
      modules/cms-kit/src/Volo.CmsKit.Public.HttpApi/Volo/CmsKit/Public/Blogs/BlogPostPublicController.cs
  62. 8
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Blogs/BlogPost/Default.cshtml
  63. 4
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Pages/Default.cshtml
  64. 4
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/BlogPost.cshtml
  65. 15
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/Index.cshtml
  66. 23
      modules/cms-kit/test/Volo.CmsKit.Application.Tests/Blogs/BlogPostAdminAppService_Tests.cs
  67. 7
      modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/LocalizationExtensions/zh-Hans.json
  68. 18
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpClaimsService.cs
  69. 14
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpClaimsServiceOptions.cs
  70. 10
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpIdentityServerDomainModule.cs
  71. 4
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpProfileService.cs
  72. 37
      npm/ng-packs/angular.json
  73. 4
      npm/ng-packs/apps/dev-app/src/app/app-routing.module.ts
  74. 2
      npm/ng-packs/apps/dev-app/src/app/app.module.ts
  75. 2
      npm/ng-packs/apps/dev-app/src/app/home/home.component.ts
  76. 5
      npm/ng-packs/apps/dev-app/src/environments/environment.ts
  77. 3
      npm/ng-packs/packages/account/README.md
  78. 7
      npm/ng-packs/packages/account/config/ng-package.json
  79. 12
      npm/ng-packs/packages/account/config/src/account-config.module.ts
  80. 1
      npm/ng-packs/packages/account/config/src/enums/index.ts
  81. 6
      npm/ng-packs/packages/account/config/src/enums/route-names.ts
  82. 1
      npm/ng-packs/packages/account/config/src/providers/index.ts
  83. 39
      npm/ng-packs/packages/account/config/src/providers/route.provider.ts
  84. 3
      npm/ng-packs/packages/account/config/src/public-api.ts
  85. 6
      npm/ng-packs/packages/account/jest.config.js
  86. 8
      npm/ng-packs/packages/account/ng-package.json
  87. 6
      npm/ng-packs/packages/account/ngcc.config.js
  88. 16
      npm/ng-packs/packages/account/package.json
  89. 93
      npm/ng-packs/packages/account/src/lib/account-routing.module.ts
  90. 63
      npm/ng-packs/packages/account/src/lib/account.module.ts
  91. 25
      npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.html
  92. 28
      npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.ts
  93. 48
      npm/ng-packs/packages/account/src/lib/components/change-password/change-password.component.html
  94. 99
      npm/ng-packs/packages/account/src/lib/components/change-password/change-password.component.ts
  95. 40
      npm/ng-packs/packages/account/src/lib/components/forgot-password/forgot-password.component.html
  96. 35
      npm/ng-packs/packages/account/src/lib/components/forgot-password/forgot-password.component.ts
  97. 8
      npm/ng-packs/packages/account/src/lib/components/index.ts
  98. 60
      npm/ng-packs/packages/account/src/lib/components/login/login.component.html
  99. 80
      npm/ng-packs/packages/account/src/lib/components/login/login.component.ts
  100. 62
      npm/ng-packs/packages/account/src/lib/components/manage-profile/manage-profile.component.html

23
abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json

@ -215,8 +215,25 @@
"Gateway": "Gateway",
"State": "State",
"FailReason": "Fail reason",
"ReIndexAllPosts": "ReIndex All Posts",
"ReIndexAllPostsConfirmationMessage": "Are you sure to reindex all posts?",
"SuccessfullyReIndexAllPosts": "Successfully reindex for all posts"
"ReIndexAllPosts": "Reindex All Posts",
"ReIndexAllPostsConfirmationMessage": "Are you sure you want to reindex all posts?",
"SuccessfullyReIndexAllPosts": "All posts have been successfully reindexed.",
"Permission:FullSearch": "Full text search",
"Menu:CliAnalytics": "Cli Analytics",
"TemplateName": "Template name",
"TemplateVersion": "Template version",
"DatabaseProvider": "Database provider",
"IsTiered": "Is Tiered",
"ProjectName": "Project name",
"Username": "User name",
"Tool": "Tool",
"Command": "Command",
"UiFramework": "Ui framework",
"Options": "Options",
"CliAnalytics": "Cli Analytics",
"Permission:CliAnalyticses": "Cli Analyticses",
"Permission:CliAnalytics": "Cli Analytics",
"Search": "Search",
"ClearFilter": "Clear filter"
}
}

18
abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/tr.json

@ -166,6 +166,22 @@
"DeleteCoverImageSuccessMessage": "Kapak fotoğrafı başarılı bir şekilde silinmiştir",
"ReIndexAllPosts": "Yeniden indeksle",
"ReIndexAllPostsConfirmationMessage": "Tüm gönderileri yeniden indeksleyeceğinizden emin misiniz?",
"SuccessfullyReIndexAllPosts": "Tüm gönderiler başarıyla yeniden indekslendi"
"SuccessfullyReIndexAllPosts": "Tüm gönderiler başarıyla yeniden indekslendi",
"Menu:CliAnalytics": "Cli Analitik",
"TemplateName": "Şablon ismi",
"TemplateVersion": "Şablon versiyon",
"DatabaseProvider": "Veritabanı sağlayıcı",
"IsTiered": "Katmanlı mı",
"ProjectName": "Proje ismi",
"Username": "Kullanıcı adı",
"Tool": "Araç",
"Command": "Komut",
"UiFramework": "Ui çerçevesi",
"Options": "Seçenekler",
"CliAnalytics": "Cli Analitik",
"Permission:CliAnalyticses": "Cli Analitikleri",
"Permission:CliAnalytics": "Cli Analitik",
"Search": "Ara",
"ClearFilter": "Filtreyi temizle"
}
}

25
abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/zh-Hans.json

@ -35,6 +35,7 @@
"NameFilter": "名称",
"CreationTime": "创建时间",
"IsPro": "是否为专业版",
"IsFreeToActiveLicenseOwners": "许可证所有者免费",
"ShowOnModuleList": "展示模块列表",
"EfCoreConfigureMethodName": "配置方法法",
"IsProFilter": "是否为专业版",
@ -90,6 +91,7 @@
"UserNotFound": "用户不存在",
"{0}WillBeRemovedFromDevelopers": "{0} 将从开发者中移除, 你确定吗?",
"{0}WillBeRemovedFromOwners": "{0} 将从所有者中移除, 你确定吗?",
"{0}WillBeRemovedFromMembers": "{0} 将从成员中删除, 你确定吗?",
"Computers": "计算机",
"UniqueComputerId": "计算机唯一ID",
"LastSeenDate": "上次查看日期",
@ -194,6 +196,27 @@
"CoverImage": "封面图片",
"RemoveCacheConfirmationMessage": "你确定要删除\"{0}\" 文章缓存?",
"SuccessfullyRemoved": "清除成功",
"RemoveCache": "删除缓存"
"RemoveCache": "删除缓存",
"Language": "语言",
"Optional": "可选的",
"CreateArticleLanguageInfo": "发帖所用的语言",
"Enum:ContentSource:2": "视频发布",
"VideoPreview": "视频预览",
"VideoPreviewErrorMessage": "无法从YouTube获取给定的视频网址. 可能是由于视频是私有视频,或者给定的URL不可用.",
"DeleteCoverImage": "删除封面图片",
"DeleteCoverImageConfirmationMessage": "你确定要删除封面图片吗",
"DeleteCoverImageSuccessMessage": "成功删除封面图片",
"PaymentsOf": "付款的",
"ShowPaymentsOfOrganization": "显示付款",
"Date": "日期",
"Products": "商品",
"TotalAmount": "总金额",
"Currency": "货币",
"Gateway": "网关",
"State": "状态",
"FailReason": "失败原因",
"ReIndexAllPosts": "重新索引所有帖子",
"ReIndexAllPostsConfirmationMessage": "你确定要重新索引所有的帖子吗",
"SuccessfullyReIndexAllPosts": "成功索引所有的帖子"
}
}

3
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json

@ -138,6 +138,7 @@
"TwitterUserNameValidationMessage": "Your Twitter username can not include whitespace, please be sure your Twitter username is correct.",
"LinkedinUrlValidationMessage": "Your Linkedin URL can not include whitespace, please be sure your Linkedin URL is correct.",
"NoPostsFound": "No posts found!",
"SearchInPosts": "Search in posts..."
"SearchInPosts": "Search in posts...",
"MinimumSearchContent": "You must enter at least 3 characters!"
}
}

31
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json

@ -110,7 +110,34 @@
"VideoContentForm": "视频内容来源",
"GithubPostForm": "Github文章来源",
"ExternalPostForm": "外部文章来源",
"PostSourceTypeChooses": "我们接受文章的三种来源类型;",
"Posts": "文章"
"HowToPost": "如何发布",
"Posts": "文章",
"VideoUrl": "视频Url",
"GithubArticleUrl": "Github文章Url",
"ExternalArticleUrl": "外部文章Url",
"CreatePostCoverInfo": "要创建一个有效的帖子,需要添加封面图片. 上传16:9的图片获取最佳视觉效果. 文件最大支持: 1MB",
"ThankYouForContribution": "感谢你对ABP社区的贡献",
"GithubArticle": "Github文章",
"GithubArticleSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> 用Markdown格式在GitHub的任何公共存储库上写一篇文章. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">示例</a>",
"GithubArticleSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> 使用表单提交你的文章URL.",
"GithubArticleSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> 你的文章将在这个网站中呈现.",
"YoutubeVideo": "Youtube视频",
"YoutubeVideoSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> 在YouTube上发布你的视频.",
"YoutubeVideoSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> 使用表单提交视频URL.",
"YoutubeVideoSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> 访客将能够在这个网站上直接观看你的视频内容.",
"ExternalContent": "外部内容",
"ExternalContentSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> 在任何公开的平台创建内容(medium, 你自己的博客或者任何你喜欢的).",
"ExternalContentSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> 使用表单提交内容URL.",
"ExternalContentSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> 访客被重定向到原网站的内容.",
"ChooseYourContentType": "请选择你想要添加内容的方式",
"PostContentViaGithub": "我想要使用<span class=\"font-weight-bold\"><i class=\"fa fa-github\"></i> GitHub</span>提交markdown格式的文章",
"PostContentViaYoutube": "我想要分享我在<span class=\"font-weight-bold\"><i class=\"fa fa-youtube\"></i> Youtube</span>的可用视频",
"PostContentViaExternalSource": "我想要添加我在<span class=\"font-weight-bold\">其他平台</span>发布的内容",
"GitHubUserNameValidationMessage": "您的Github用户名不能包含空格, 请确认你的Github用户名是正确的.",
"PersonalSiteUrlValidationMessage": "你的个人网站URL不能包含空格, 请确定你的个人网站URL是正确的.",
"TwitterUserNameValidationMessage": "你的Twitter用户名不能包含空格, 请确认你的Twitter用户名是正确的.",
"LinkedinUrlValidationMessage": "你的领英URL不能包含空格, 请确认你的领英URL是正确的",
"NoPostsFound": "没有发现帖子",
"SearchInPosts": "搜索帖子…"
}
}

7
abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hans.json

@ -8,6 +8,7 @@
"ProjectName": "项目名称",
"ProjectType": "项目类型",
"DatabaseProvider": "数据库提供者",
"DatabaseManagementSystem": "数据库管理系统",
"NTier": "N层",
"IncludeUserInterface": "包含用户界面",
"CreateNow": "现在创建",
@ -194,7 +195,7 @@
"MultipleUIOptionsExplanation": "核心框架设计为独立与UI,可以和任何类型的UI系统一起使用. 同时提供了多个开箱即用的预构建集成选项.",
"MultipleDBOptionsExplanation": "该框架可以使用任何数据源,并且以下提供程序已得到正式开发和支持;",
"SelectLanguage": "选择语言",
"LatestArticleOnCommunity": "关于ABP社区的最新文章",
"Register": "寄存器"
"LatestArticleOnCommunity": "关于ABP社区的最新文章",
"Register": "寄存器"
}
}
}

2
docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/How-To-Add-Custom-Property-To-The-User-Entity.md

@ -78,7 +78,7 @@ public static class CustomizeUserDemoEfCoreEntityExtensionMappings
nameof(AppUser.Title),
(entityBuilder, propertyBuilder) =>
{
propertyBuilder.IsRequired();
propertyBuilder.HasDefaultValue();
propertyBuilder.HasMaxLength(UserConsts.MaxTitleLength);
}
).MapEfCoreProperty<IdentityUser, int>(

101
docs/en/UI/Angular/HTTP-Requests.md

@ -27,13 +27,13 @@ An `HttpInterceptor` is able to catch `HttpErrorResponse`  and can be used for
## RestService
ABP core module has a utility service for HTTP requests: `RestService`. Unless explicitly configured otherwise, it catches HTTP errors and dispatches a `RestOccurError` action. This action is then captured by the `ErrorHandler` introduced by the `ThemeSharedModule`. Since you should already import this module in your app, when the `RestService` is used, all HTTP errors get automatically handled by deafult.
ABP core module has a utility service for HTTP requests: `RestService`. Unless explicitly configured otherwise, it catches HTTP errors and dispatches a `RestOccurError` action. This action is then captured by the `ErrorHandler` introduced by the `ThemeSharedModule`. Since you should already import this module in your app, when the `RestService` is used, all HTTP errors get automatically handled by default.
### Getting Started with RestService
In order to use the `RestService`, you must inject it in your class as a dependency.
In order to use the `RestService`, you must inject it in your class as a dependency.
```js
import { RestService } from '@abp/ng.core';
@ -200,6 +200,101 @@ getSomeCustomHeaderValue() {
}
```
You may find `Rest.Observe` enum [here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L10).
You may find `Rest.Observe` enum [here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L10).
## HTTP Error Handling
When the `RestService` is used, all HTTP errors are automatically handled by `ErrorHandler` which is a service that exposed by the `@abp/ng.theme.shared` package.
### Custom HTTP Error Handler
A custom HTTP error handler can be registered to an injection token named `HTTP_ERROR_HANDLER`. If a custom handler function is registered, the `ErrorHandler` executes that function.
See an example:
```js
// http-error-handler.ts
import { ContentProjectionService, PROJECTION_STRATEGY } from '@abp/ng.core';
import { ToasterService } from '@abp/ng.theme.shared';
import { HttpErrorResponse } from '@angular/common/http';
import { Injector } from '@angular/core';
import { throwError } from 'rxjs';
import { Error404Component } from './error404/error404.component';
export function handleHttpErrors(injector: Injector, httpError: HttpErrorResponse) {
if (httpError.status === 400) {
const toaster = injector.get(ToasterService);
toaster.error(httpError.error?.error?.message || 'Bad request!', '400');
return;
}
if (httpError.status === 404) {
const contentProjection = injector.get(ContentProjectionService);
contentProjection.projectContent(PROJECTION_STRATEGY.AppendComponentToBody(Error404Component));
return;
}
return throwError(httpError);
}
// app.module.ts
import { Error404Component } from './error404/error404.component';
import { handleHttpErrors } from './http-error-handling';
import { HTTP_ERROR_HANDLER, ... } from '@abp/ng.theme.shared';
@NgModule({
// ...
providers: [
// ...
{ provide: HTTP_ERROR_HANDLER, useValue: handleHttpErrors }
],
declarations: [
//...
Error404Component],
})
export class AppModule {}
```
In the example above:
- Created a function named `handleHttpErrors` and defined as value of the `HTTP_ERROR_HANDLER` provider in app.module. After this, the function executes when an HTTP error occurs.
- 400 bad request errors is handled. When a 400 error occurs, backend error message will be displayed as shown below:
![custom-error-handler-toaster-message](images/custom-error-handler-toaster-message.jpg)
- 404 not found errors is handled. When a 404 error occurs, `Error404Component` will be appended to the `<body>` as shown below:
![custom-error-handler-404-component](images/custom-error-handler-404-component.jpg)
- Since `throwError(httpError)` is returned at bottom of the `handleHttpErrors`, the `ErrorHandler` will handle the HTTP errors except 400 and 404 errors.
**Note 1:** If you put `return` to next line of handling an error, default error handling will not work for that error.
```js
export function handleHttpErrors(injector: Injector, httpError: HttpErrorResponse) {
if (httpError.status === 403) {
// handle 403 errors here
return; // put return to skip default error handling
}
}
```
**Note 2:** If you put `return throwError(httpError)`, default error handling will work.
- `throwError` is a function. It can be imported from `rxjs`.
- `httpError` is the second parameter of the error handler function which is registered to the `HTTP_ERROR_HANDLER` provider. Type of the `httpError` is `HttpErrorResponse`.
```js
import { throwError } from 'rxjs';
export function handleHttpErrors(injector: Injector, httpError: HttpErrorResponse) {
if (httpError.status === 500) {
// handle 500 errors here
return;
}
// you can return the throwError(httpError) at bottom of the function to run the default handler of ABP for HTTP errors that you didn't handle above.
return throwError(httpError)
}
```

2
docs/en/UI/Angular/Router-Events.md

@ -1,6 +1,6 @@
# Router Events Simplified
`RouterEvents` is a utility service to provide an easy implementation for one of the most frequent needs in Angular templates: `TrackByFunction`. Please see [this page in Angular docs](https://angular.io/guide/template-syntax#ngfor-with-trackby) for its purpose.
`RouterEvents` is a utility service for filtering specific router events and reacting to them. Please see [this page in Angular docs](https://angular.io/api/router/Event) for available router events.

BIN
docs/en/UI/Angular/images/custom-error-handler-404-component.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
docs/en/UI/Angular/images/custom-error-handler-toaster-message.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

2
docs/zh-Hans/Domain-Driven-Design-Implementation-Guide.md

@ -781,7 +781,7 @@ namespace IssueTracking.Issues
- 创建时间大于30天
- 最近30天没有评论
这个业务逻辑就被实现了仓储内部,当我们需要重用这个业务规则时就会出现问题.
这个业务逻辑就被实现了仓储内部,当我们需要重用这个业务规则时就会出现问题.
例如:我们需要再实体`Issue`上添加一个方法来判断是否非活动`bool IsInActive()`,以方便我们在`Issue`实例上获取.

6
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpController.cs

@ -108,12 +108,12 @@ namespace Volo.Abp.AspNetCore.Mvc
return localizer;
}
protected RedirectResult RedirectSafely(string returnUrl, string returnUrlHash = null)
protected virtual RedirectResult RedirectSafely(string returnUrl, string returnUrlHash = null)
{
return Redirect(GetRedirectUrl(returnUrl, returnUrlHash));
}
private string GetRedirectUrl(string returnUrl, string returnUrlHash = null)
protected virtual string GetRedirectUrl(string returnUrl, string returnUrlHash = null)
{
returnUrl = NormalizeReturnUrl(returnUrl);
@ -125,7 +125,7 @@ namespace Volo.Abp.AspNetCore.Mvc
return returnUrl;
}
private string NormalizeReturnUrl(string returnUrl)
protected virtual string NormalizeReturnUrl(string returnUrl)
{
if (returnUrl.IsNullOrEmpty())
{

24
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Authentication/ChallengeAccountController.cs

@ -23,20 +23,8 @@ namespace Volo.Abp.AspNetCore.Mvc.Authentication
{
return RedirectSafely(returnUrl, returnUrlHash);
}
else
{
return Challenge(
new AuthenticationProperties
{
Parameters =
{
{"returnUrl", returnUrl},
{"returnUrlHash", returnUrlHash}
}
},
ChallengeAuthenticationSchemas
);
}
return Challenge(new AuthenticationProperties {RedirectUri = GetRedirectUrl(returnUrl, returnUrlHash)}, ChallengeAuthenticationSchemas);
}
[HttpGet]
@ -44,20 +32,20 @@ namespace Volo.Abp.AspNetCore.Mvc.Authentication
{
await HttpContext.SignOutAsync();
if (HttpContext.User.Identity.AuthenticationType == AuthenticationType)
if (HttpContext.User.Identity?.AuthenticationType == AuthenticationType)
{
return RedirectSafely(returnUrl, returnUrlHash);
}
return new SignOutResult(ChallengeAuthenticationSchemas);
return SignOut(new AuthenticationProperties {RedirectUri = GetRedirectUrl(returnUrl, returnUrlHash)}, ChallengeAuthenticationSchemas);
}
[HttpGet]
public async Task<IActionResult> FrontChannelLogout(string sid)
{
if (User.Identity.IsAuthenticated)
if (User.Identity != null && User.Identity.IsAuthenticated)
{
var currentSid = User.FindFirst("sid").Value ?? string.Empty;
var currentSid = User.FindFirst("sid")?.Value ?? string.Empty;
if (string.Equals(currentSid, sid, StringComparison.Ordinal))
{
await Logout();

78
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/MicroserviceServiceRandomPortStep.cs

@ -0,0 +1,78 @@
using System;
using System.IO;
using System.Linq;
using Volo.Abp.Cli.Commands;
namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps
{
public class MicroserviceServiceRandomPortStep : ProjectBuildPipelineStep
{
private readonly string _defaultPort = string.Empty;
private string _tyeFileContent = null;
public MicroserviceServiceRandomPortStep(string defaultPort)
{
_defaultPort = defaultPort;
}
public override void Execute(ProjectBuildContext context)
{
var newPort = GetNewRandomPort(context);
var targetFiles = context.Files.Where(f=> f.Name.EndsWith("launchSettings.json") || f.Name.EndsWith("appsettings.json")).ToList();
foreach (var file in targetFiles)
{
file.SetContent(file.Content.Replace(_defaultPort, newPort));
}
}
private string GetNewRandomPort(ProjectBuildContext context)
{
string newPort;
var rnd = new Random();
var tryCount = 0;
do
{
newPort = rnd.Next(44350, 45350).ToString();
if (tryCount++ > 2000)
{
break;
}
} while (PortExistsForAnotherService(context, newPort));
return newPort;
}
private bool PortExistsForAnotherService(ProjectBuildContext context, string newPort)
{
return ReadTyeFileContent(context).SplitToLines().Any(l => l.Contains("port") && l.Contains(newPort));
}
private string ReadTyeFileContent(ProjectBuildContext context)
{
if (_tyeFileContent != null)
{
return _tyeFileContent;
}
var solutionFolderPath = context.BuildArgs.ExtraProperties[NewCommand.Options.OutputFolder.Short] ??
context.BuildArgs.ExtraProperties[NewCommand.Options.OutputFolder.Long] ??
Directory.GetCurrentDirectory();
var tyeFilePath = Path.Combine(solutionFolderPath, "tye.yaml");
if (!File.Exists(tyeFilePath))
{
return String.Empty;
}
_tyeFileContent = File.ReadAllText(tyeFilePath);
return _tyeFileContent;
}
}
}

54
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveProjectFromTyeStep.cs

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps
{
public class RemoveProjectFromTyeStep : ProjectBuildPipelineStep
{
private readonly string _name;
public RemoveProjectFromTyeStep(string name)
{
_name = name;
}
public override void Execute(ProjectBuildContext context)
{
var tyeFile = context.Files.FirstOrDefault(f => f.Name == "/tye.yaml");
if (tyeFile == null)
{
return;
}
var lines = tyeFile.GetLines();
var newLines = new List<string>();
var nameLine = $"- name:";
var isOneOfTargetLines = false;
foreach (var line in lines)
{
if (line.Equals($"{nameLine} {_name}"))
{
isOneOfTargetLines = true;
continue;
}
if (line.StartsWith(nameLine))
{
isOneOfTargetLines = false;
}
if (!isOneOfTargetLines)
{
newLines.Add(line);
}
}
tyeFile.SetContent(String.Join(Environment.NewLine, newLines));
}
}
}

6
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/Microservice/MicroserviceServiceTemplateBase.cs

@ -31,6 +31,7 @@ namespace Volo.Abp.Cli.ProjectBuilding.Templates.Microservice
var steps = new List<ProjectBuildPipelineStep>();
DeleteUnrelatedUiProject(context, steps);
SetRandomPortForHostProject(context, steps);
RandomizeStringEncryption(context, steps);
return steps;
@ -54,6 +55,11 @@ namespace Volo.Abp.Cli.ProjectBuilding.Templates.Microservice
}
}
private static void SetRandomPortForHostProject(ProjectBuildContext context, List<ProjectBuildPipelineStep> steps)
{
steps.Add(new MicroserviceServiceRandomPortStep("44371"));
}
private static void RandomizeStringEncryption(ProjectBuildContext context, List<ProjectBuildPipelineStep> steps)
{
steps.Add(new RandomizeStringEncryptionStep());

10
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/Microservice/MicroserviceTemplateBase.cs

@ -36,9 +36,13 @@ namespace Volo.Abp.Cli.ProjectBuilding.Templates.Microservice
steps.Add(new RemoveProjectFromSolutionStep("MyCompanyName.MyProjectName.Web",null,
"/applications/web/src/MyCompanyName.MyProjectName.Web"));
steps.Add(new RemoveFolderStep("/applications/web"));
steps.Add(new RemoveProjectFromTyeStep("web"));
steps.Add(new RemoveProjectFromSolutionStep("MyCompanyName.MyProjectName.Blazor",null,
"/applications/blazor/src/MyCompanyName.MyProjectName.Blazor"));
steps.Add(new RemoveFolderStep("/applications/blazor"));
steps.Add(new RemoveProjectFromTyeStep("blazor"));
steps.Add(new RemoveFolderStep("/angular"));
break;
@ -46,9 +50,12 @@ namespace Volo.Abp.Cli.ProjectBuilding.Templates.Microservice
steps.Add(new RemoveProjectFromSolutionStep("MyCompanyName.MyProjectName.Web",null,
"/applications/web/src/MyCompanyName.MyProjectName.Web"));
steps.Add(new RemoveFolderStep("/applications/web"));
steps.Add(new RemoveProjectFromTyeStep("web"));
steps.Add(new RemoveProjectFromSolutionStep("MyCompanyName.MyProjectName.Blazor",null,
"/applications/blazor/src/MyCompanyName.MyProjectName.Blazor"));
steps.Add(new RemoveFolderStep("/applications/blazor"));
steps.Add(new RemoveProjectFromTyeStep("blazor"));
break;
case UiFramework.Blazor:
@ -56,6 +63,7 @@ namespace Volo.Abp.Cli.ProjectBuilding.Templates.Microservice
"/applications/web/src/MyCompanyName.MyProjectName.Web"));
steps.Add(new RemoveFolderStep("/applications/web"));
steps.Add(new RemoveFolderStep("/angular"));
steps.Add(new RemoveProjectFromTyeStep("web"));
break;
case UiFramework.Mvc:
@ -63,6 +71,8 @@ namespace Volo.Abp.Cli.ProjectBuilding.Templates.Microservice
steps.Add(new RemoveProjectFromSolutionStep("MyCompanyName.MyProjectName.Blazor",null,
"/applications/blazor/src/MyCompanyName.MyProjectName.Blazor"));
steps.Add(new RemoveFolderStep("/applications/blazor"));
steps.Add(new RemoveProjectFromTyeStep("blazor"));
steps.Add(new RemoveFolderStep("/angular"));
break;
}

13
framework/src/Volo.Abp.Core/Microsoft/Extensions/DependencyInjection/ServiceCollectionLoggingExtensions.cs

@ -0,0 +1,13 @@
using Microsoft.Extensions.Logging;
using Volo.Abp.Logging;
namespace Microsoft.Extensions.DependencyInjection
{
public static class ServiceCollectionLoggingExtensions
{
public static ILogger<T> GetInitLogger<T>(this IServiceCollection services)
{
return services.GetSingletonInstance<IInitLoggerFactory>().Create<T>();
}
}
}

21
framework/src/Volo.Abp.Core/System/AbpStringExtensions.cs

@ -260,8 +260,9 @@ namespace System
/// </summary>
/// <param name="str">String to convert</param>
/// <param name="useCurrentCulture">set true to use current culture. Otherwise, invariant culture will be used.</param>
/// <param name="handleAbbreviations">set true to if you want to convert 'XYZ' to 'xyz'.</param>
/// <returns>camelCase of the string</returns>
public static string ToCamelCase(this string str, bool useCurrentCulture = false)
public static string ToCamelCase(this string str, bool useCurrentCulture = false, bool handleAbbreviations = false)
{
if (string.IsNullOrWhiteSpace(str))
{
@ -273,6 +274,11 @@ namespace System
return useCurrentCulture ? str.ToLower() : str.ToLowerInvariant();
}
if (handleAbbreviations && IsAllUpperCase(str))
{
return useCurrentCulture ? str.ToLower() : str.ToLowerInvariant();
}
return (useCurrentCulture ? char.ToLower(str[0]) : char.ToLowerInvariant(str[0])) + str.Substring(1);
}
@ -545,5 +551,18 @@ namespace System
return encoding.GetBytes(str);
}
private static bool IsAllUpperCase(string input)
{
for (int i = 0; i < input.Length; i++)
{
if (Char.IsLetter(input[i]) && !Char.IsUpper(input[i]))
{
return false;
}
}
return true;
}
}
}

27
framework/src/Volo.Abp.Core/Volo/Abp/AbpApplicationBase.cs

@ -3,8 +3,10 @@ using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Internal;
using Volo.Abp.Logging;
using Volo.Abp.Modularity;
namespace Volo.Abp
@ -60,7 +62,7 @@ namespace Volo.Abp
{
//TODO: Shutdown if not done before?
}
protected virtual void SetServiceProvider(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
@ -71,12 +73,31 @@ namespace Volo.Abp
{
using (var scope = ServiceProvider.CreateScope())
{
WriteInitLogs(scope.ServiceProvider);
scope.ServiceProvider
.GetRequiredService<IModuleManager>()
.InitializeModules(new ApplicationInitializationContext(scope.ServiceProvider));
}
}
protected virtual void WriteInitLogs(IServiceProvider serviceProvider)
{
var logger = serviceProvider.GetService<ILogger<AbpApplicationBase>>();
if (logger == null)
{
return;
}
var initLogger = serviceProvider.GetRequiredService<IInitLoggerFactory>().Create<AbpApplicationBase>();
foreach (var entry in initLogger.Entries)
{
logger.Log(entry.LogLevel, entry.EventId, entry.State, entry.Exception, entry.Formatter);
}
initLogger.Entries.Clear();
}
protected virtual IReadOnlyList<IAbpModuleDescriptor> LoadModules(IServiceCollection services, AbpApplicationCreationOptions options)
{
return services
@ -87,7 +108,7 @@ namespace Volo.Abp
options.PlugInSources
);
}
//TODO: We can extract a new class for this
protected virtual void ConfigureServices()
{
@ -158,4 +179,4 @@ namespace Volo.Abp
}
}
}
}
}

6
framework/src/Volo.Abp.Core/Volo/Abp/Internal/InternalServiceCollectionExtensions.cs

@ -1,6 +1,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Volo.Abp.Logging;
using Volo.Abp.Modularity;
using Volo.Abp.Reflection;
@ -16,7 +17,7 @@ namespace Volo.Abp.Internal
}
internal static void AddCoreAbpServices(this IServiceCollection services,
IAbpApplication abpApplication,
IAbpApplication abpApplication,
AbpApplicationCreationOptions applicationCreationOptions)
{
var moduleLoader = new ModuleLoader();
@ -35,6 +36,7 @@ namespace Volo.Abp.Internal
services.TryAddSingleton<IModuleLoader>(moduleLoader);
services.TryAddSingleton<IAssemblyFinder>(assemblyFinder);
services.TryAddSingleton<ITypeFinder>(typeFinder);
services.TryAddSingleton<IInitLoggerFactory>(new DefaultInitLoggerFactory());
services.AddAssemblyOf<IAbpApplication>();
@ -47,4 +49,4 @@ namespace Volo.Abp.Internal
});
}
}
}
}

20
framework/src/Volo.Abp.Core/Volo/Abp/Logging/AbpInitLogEntry.cs

@ -0,0 +1,20 @@
using System;
using Microsoft.Extensions.Logging;
namespace Volo.Abp.Logging
{
public class AbpInitLogEntry
{
public LogLevel LogLevel { get; set; }
public EventId EventId { get; set; }
public object State { get; set; }
public Exception Exception { get; set; }
public Func<object, Exception, string> Formatter { get; set; }
public string Message => Formatter(State, Exception);
}
}

38
framework/src/Volo.Abp.Core/Volo/Abp/Logging/DefaultInitLogger.cs

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
namespace Volo.Abp.Logging
{
public class DefaultInitLogger<T> : IInitLogger<T>
{
public List<AbpInitLogEntry> Entries { get; }
public DefaultInitLogger()
{
Entries = new List<AbpInitLogEntry>();
}
public virtual void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
Entries.Add(new AbpInitLogEntry
{
LogLevel = logLevel,
EventId = eventId,
State = state,
Exception = exception,
Formatter = (s, e) => formatter((TState)s, e),
});
}
public virtual bool IsEnabled(LogLevel logLevel)
{
return logLevel != LogLevel.None;
}
public virtual IDisposable BeginScope<TState>(TState state)
{
return NullDisposable.Instance;
}
}
}

15
framework/src/Volo.Abp.Core/Volo/Abp/Logging/DefaultInitLoggerFactory.cs

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
namespace Volo.Abp.Logging
{
public class DefaultInitLoggerFactory : IInitLoggerFactory
{
private readonly Dictionary<Type, object> _cache = new Dictionary<Type, object>();
public virtual IInitLogger<T> Create<T>()
{
return (IInitLogger<T>)_cache.GetOrAdd(typeof(T), () => new DefaultInitLogger<T>());;
}
}
}

10
framework/src/Volo.Abp.Core/Volo/Abp/Logging/IInitLogger.cs

@ -0,0 +1,10 @@
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
namespace Volo.Abp.Logging
{
public interface IInitLogger<out T> : ILogger<T>
{
public List<AbpInitLogEntry> Entries { get; }
}
}

7
framework/src/Volo.Abp.Core/Volo/Abp/Logging/IInitLoggerFactory.cs

@ -0,0 +1,7 @@
namespace Volo.Abp.Logging
{
public interface IInitLoggerFactory
{
IInitLogger<T> Create<T>();
}
}

15
framework/src/Volo.Abp.Core/Volo/Abp/Modularity/AbpModuleHelper.cs

@ -2,15 +2,17 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Logging;
namespace Volo.Abp.Modularity
{
internal static class AbpModuleHelper
{
public static List<Type> FindAllModuleTypes(Type startupModuleType)
public static List<Type> FindAllModuleTypes(Type startupModuleType, ILogger logger)
{
var moduleTypes = new List<Type>();
AddModuleAndDependenciesResursively(moduleTypes, startupModuleType);
logger.Log(LogLevel.Information, "Loaded ABP modules:");
AddModuleAndDependenciesResursively(moduleTypes, startupModuleType, logger);
return moduleTypes;
}
@ -35,7 +37,11 @@ namespace Volo.Abp.Modularity
return dependencies;
}
private static void AddModuleAndDependenciesResursively(List<Type> moduleTypes, Type moduleType)
private static void AddModuleAndDependenciesResursively(
List<Type> moduleTypes,
Type moduleType,
ILogger logger,
int depth = 0)
{
AbpModule.CheckAbpModuleType(moduleType);
@ -45,10 +51,11 @@ namespace Volo.Abp.Modularity
}
moduleTypes.Add(moduleType);
logger.Log(LogLevel.Information, $"{new string(' ', depth * 2)}- {moduleType.FullName}");
foreach (var dependedModuleType in FindDependedModuleTypes(moduleType))
{
AddModuleAndDependenciesResursively(moduleTypes, dependedModuleType);
AddModuleAndDependenciesResursively(moduleTypes, dependedModuleType, logger, depth + 1);
}
}
}

10
framework/src/Volo.Abp.Core/Volo/Abp/Modularity/ModuleLoader.cs

@ -25,7 +25,7 @@ namespace Volo.Abp.Modularity
}
private List<IAbpModuleDescriptor> GetDescriptors(
IServiceCollection services,
IServiceCollection services,
Type startupModuleType,
PlugInSourceList plugInSources)
{
@ -43,14 +43,16 @@ namespace Volo.Abp.Modularity
Type startupModuleType,
PlugInSourceList plugInSources)
{
var logger = services.GetInitLogger<AbpApplicationBase>();
//All modules starting from the startup module
foreach (var moduleType in AbpModuleHelper.FindAllModuleTypes(startupModuleType))
foreach (var moduleType in AbpModuleHelper.FindAllModuleTypes(startupModuleType, logger))
{
modules.Add(CreateModuleDescriptor(services, moduleType));
}
//Plugin modules
foreach (var moduleType in plugInSources.GetAllModules())
foreach (var moduleType in plugInSources.GetAllModules(logger))
{
if (modules.Any(m => m.Type == moduleType))
{
@ -102,4 +104,4 @@ namespace Volo.Abp.Modularity
}
}
}
}
}

12
framework/src/Volo.Abp.Core/Volo/Abp/Modularity/ModuleManager.cs

@ -32,8 +32,6 @@ namespace Volo.Abp.Modularity
public void InitializeModules(ApplicationInitializationContext context)
{
LogListOfModules();
foreach (var contributor in _lifecycleContributors)
{
foreach (var module in _moduleContainer.Modules)
@ -52,16 +50,6 @@ namespace Volo.Abp.Modularity
_logger.LogInformation("Initialized all ABP modules.");
}
private void LogListOfModules()
{
_logger.LogInformation("Loaded ABP modules:");
foreach (var module in _moduleContainer.Modules)
{
_logger.LogInformation("- " + module.Type.FullName);
}
}
public void ShutdownModules(ApplicationShutdownContext context)
{
var modules = _moduleContainer.Modules.Reverse().ToList();

8
framework/src/Volo.Abp.Core/Volo/Abp/Modularity/PlugIns/PlugInSourceExtensions.cs

@ -1,21 +1,23 @@
using System;
using System.Linq;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Volo.Abp.Logging;
namespace Volo.Abp.Modularity.PlugIns
{
public static class PlugInSourceExtensions
{
[NotNull]
public static Type[] GetModulesWithAllDependencies([NotNull] this IPlugInSource plugInSource)
public static Type[] GetModulesWithAllDependencies([NotNull] this IPlugInSource plugInSource, ILogger logger)
{
Check.NotNull(plugInSource, nameof(plugInSource));
return plugInSource
.GetModules()
.SelectMany(AbpModuleHelper.FindAllModuleTypes)
.SelectMany(type => AbpModuleHelper.FindAllModuleTypes(type, logger))
.Distinct()
.ToArray();
}
}
}
}

7
framework/src/Volo.Abp.Core/Volo/Abp/Modularity/PlugIns/PlugInSourceList.cs

@ -2,18 +2,19 @@
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
namespace Volo.Abp.Modularity.PlugIns
{
public class PlugInSourceList : List<IPlugInSource>
{
[NotNull]
internal Type[] GetAllModules()
internal Type[] GetAllModules(ILogger logger)
{
return this
.SelectMany(pluginSource => pluginSource.GetModulesWithAllDependencies())
.SelectMany(pluginSource => pluginSource.GetModulesWithAllDependencies(logger))
.Distinct()
.ToArray();
}
}
}
}

6
framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/ExceptionHandling/Localization/zh-Hans.json

@ -18,6 +18,8 @@
"401Message": "未授权",
"403Message": "禁止访问",
"404Message": "网页未找到",
"500Message": "内部服务器错误"
"500Message": "内部服务器错误",
"403MessageDetail": "您没有权限执行此操作",
"404MessageDetail": "抱歉, 这个地址是空的"
}
}
}

10
framework/test/Volo.Abp.Core.Tests/Volo/Abp/Modularity/ModuleLoader_Tests.cs

@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.Logging;
using Volo.Abp.Modularity.PlugIns;
using Xunit;
@ -11,7 +12,12 @@ namespace Volo.Abp.Modularity
public void Should_Load_Modules_By_Dependency_Order()
{
var moduleLoader = new ModuleLoader();
var modules = moduleLoader.LoadModules(new ServiceCollection(), typeof(MyStartupModule), new PlugInSourceList());
var modules = moduleLoader.LoadModules(
new ServiceCollection()
.AddSingleton<IInitLoggerFactory>(new DefaultInitLoggerFactory()),
typeof(MyStartupModule),
new PlugInSourceList()
);
modules.Length.ShouldBe(2);
modules[0].Type.ShouldBe(typeof(IndependentEmptyModule));
modules[1].Type.ShouldBe(typeof(MyStartupModule));
@ -22,7 +28,7 @@ namespace Volo.Abp.Modularity
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
}
}
}

1951
modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20210305122554_BlogPost_CoverImage.Designer.cs

File diff suppressed because it is too large

56
modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20210305122554_BlogPost_CoverImage.cs

@ -0,0 +1,56 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Volo.CmsKit.Migrations
{
public partial class BlogPost_CoverImage : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CmsContents");
migrationBuilder.AddColumn<Guid>(
name: "CoverImageMediaId",
table: "CmsBlogPosts",
type: "uniqueidentifier",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CoverImageMediaId",
table: "CmsBlogPosts");
migrationBuilder.CreateTable(
name: "CmsContents",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
ConcurrencyStamp = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true),
EntityId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
EntityType = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
ExtraProperties = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Value = table.Column<string>(type: "nvarchar(max)", maxLength: 2147483647, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CmsContents", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_CmsContents_TenantId_EntityType_EntityId",
table: "CmsContents",
columns: new[] { "TenantId", "EntityType", "EntityId" });
}
}
}

75
modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/UnifiedDbContextModelSnapshot.cs

@ -1273,6 +1273,9 @@ namespace Volo.CmsKit.Migrations
.HasMaxLength(2147483647)
.HasColumnType("nvarchar(max)");
b.Property<Guid?>("CoverImageMediaId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
@ -1379,78 +1382,6 @@ namespace Volo.CmsKit.Migrations
b.ToTable("CmsComments");
});
modelBuilder.Entity("Volo.CmsKit.Contents.Content", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasMaxLength(40)
.HasColumnType("nvarchar(40)")
.HasColumnName("ConcurrencyStamp");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("EntityId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("EntityType")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("ExtraProperties")
.HasColumnType("nvarchar(max)")
.HasColumnName("ExtraProperties");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(2147483647)
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("TenantId", "EntityType", "EntityId");
b.ToTable("CmsContents");
});
modelBuilder.Entity("Volo.CmsKit.MediaDescriptors.MediaDescriptor", b =>
{
b.Property<Guid>("Id")

4
modules/cms-kit/host/Volo.CmsKit.Web.Unified/Volo.CmsKit.Web.Unified.csproj

@ -62,8 +62,4 @@
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations" />
</ItemGroup>
</Project>

7
modules/cms-kit/host/Volo.CmsKit.Web.Unified/wwwroot/libs/tui-editor/tui-editor-jquery-patch.js

@ -0,0 +1,7 @@
/*
https://jquery.com/upgrade-guide/3.5/#jquery-htmlprefilter-changes
*/
var rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi;
jQuery.htmlPrefilter = function (html) {
return html.replace(rxhtmlTag, "<$1></$2>");
};

2
modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Blogs/BlogPostDto.cs

@ -15,5 +15,7 @@ namespace Volo.CmsKit.Admin.Blogs
public string ShortDescription { get; set; }
public string Content { get; set; }
public Guid? CoverImageMediaId { get; set; }
}
}

2
modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Blogs/CreateBlogPostDto.cs

@ -24,5 +24,7 @@ namespace Volo.CmsKit.Admin.Blogs
[DynamicMaxLength(typeof(BlogPostConsts), nameof(BlogPostConsts.MaxContentLength))]
public string Content { get; set; }
public Guid? CoverImageMediaId { get; set; }
}
}

3
modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Blogs/IBlogPostAdminAppService.cs

@ -14,8 +14,5 @@ namespace Volo.CmsKit.Admin.Blogs
CreateBlogPostDto,
UpdateBlogPostDto>
{
Task SetCoverImageAsync(Guid id, RemoteStreamContent streamContent);
Task<RemoteStreamContent> GetCoverImageAsync(Guid id);
}
}

2
modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Blogs/UpdateBlogPostDto.cs

@ -21,5 +21,7 @@ namespace Volo.CmsKit.Admin.Blogs
[DynamicMaxLength(typeof(BlogPostConsts), nameof(BlogPostConsts.MaxContentLength))]
public string Content { get; set; }
public Guid? CoverImageMediaId { get; set; }
}
}

33
modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Blogs/BlogPostAdminAppService.cs

@ -18,27 +18,23 @@ namespace Volo.CmsKit.Admin.Blogs
{
[RequiresGlobalFeature(typeof(BlogsFeature))]
[Authorize(CmsKitAdminPermissions.BlogPosts.Default)]
public class BlogPostAdminAppService: CmsKitAppServiceBase, IBlogPostAdminAppService
public class BlogPostAdminAppService : CmsKitAppServiceBase, IBlogPostAdminAppService
{
protected BlogPostManager BlogPostManager { get; }
protected IBlogPostRepository BlogPostRepository { get; }
protected IBlogRepository BlogRepository { get; }
protected IBlobContainer<BlogPostCoverImageContainer> BlobContainer { get; }
protected ICmsUserLookupService UserLookupService { get; }
public BlogPostAdminAppService(
BlogPostManager blogPostManager,
IBlogPostRepository blogPostRepository,
IBlogRepository blogRepository,
IBlobContainer<BlogPostCoverImageContainer> blobContainer,
ICmsUserLookupService userLookupService)
{
BlogPostManager = blogPostManager;
BlogPostRepository = blogPostRepository;
BlogRepository = blogRepository;
BlobContainer = blobContainer;
UserLookupService = userLookupService;
}
[Authorize(CmsKitAdminPermissions.BlogPosts.Create)]
@ -54,7 +50,8 @@ namespace Volo.CmsKit.Admin.Blogs
input.Title,
input.Slug,
input.ShortDescription,
input.Content);
input.Content,
input.CoverImageMediaId);
await BlogPostRepository.InsertAsync(blogPost);
@ -69,7 +66,8 @@ namespace Volo.CmsKit.Admin.Blogs
blogPost.SetTitle(input.Title);
blogPost.SetShortDescription(input.ShortDescription);
blogPost.SetContent(input.Content);
blogPost.CoverImageMediaId = input.CoverImageMediaId;
if (blogPost.Slug != input.Slug)
{
await BlogPostManager.SetSlugUrlAsync(blogPost, input.Slug);
@ -77,26 +75,7 @@ namespace Volo.CmsKit.Admin.Blogs
await BlogPostRepository.UpdateAsync(blogPost);
return ObjectMapper.Map<BlogPost,BlogPostDto>(blogPost);
}
[Authorize(CmsKitAdminPermissions.BlogPosts.Update)]
public virtual async Task SetCoverImageAsync(Guid id, RemoteStreamContent streamContent)
{
await BlogPostRepository.GetAsync(id);
using (var stream = streamContent.GetStream())
{
await BlobContainer.SaveAsync(id.ToString(), stream, overrideExisting: true);
}
}
[Authorize(CmsKitAdminPermissions.BlogPosts.Default)]
public virtual async Task<RemoteStreamContent> GetCoverImageAsync(Guid id)
{
var stream = await BlobContainer.GetAsync(id.ToString());
return new RemoteStreamContent(stream);
return ObjectMapper.Map<BlogPost, BlogPostDto>(blogPost);
}
[Authorize(CmsKitAdminPermissions.BlogPosts.Default)]

3
modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/MediaDescriptors/MediaDescriptorAdminAppService.cs

@ -35,12 +35,11 @@ namespace Volo.CmsKit.Admin.MediaDescriptors
await CheckAnyOfPoliciesAsync(definition.CreatePolicies);
var newId = GuidGenerator.Create();
using (var stream = inputStream.GetStream())
{
var newEntity = await MediaDescriptorManager.CreateAsync(inputStream.EntityType, inputStream.Name, inputStream.ContentType, inputStream.ContentLength ?? 0);
await MediaContainer.SaveAsync(newId.ToString(), stream);
await MediaContainer.SaveAsync(newEntity.Id.ToString(), stream);
await MediaDescriptorRepository.InsertAsync(newEntity);
return ObjectMapper.Map<MediaDescriptor, MediaDescriptorDto>(newEntity);

44
modules/cms-kit/src/Volo.CmsKit.Admin.HttpApi/Volo/CmsKit/Admin/Blogs/BlogPostAdminController.cs

@ -1,14 +1,9 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Content;
using Volo.Abp.GlobalFeatures;
using Volo.CmsKit.GlobalFeatures;
using Volo.CmsKit.Permissions;
@ -52,50 +47,13 @@ namespace Volo.CmsKit.Admin.Blogs
return BlogPostAdminAppService.GetAsync(id);
}
[HttpGet]
[Route("{id}/cover-image")]
[Authorize(CmsKitAdminPermissions.BlogPosts.Default)]
public virtual Task<RemoteStreamContent> GetCoverImageAsync(Guid id)
{
Response.Headers.Add("Content-Disposition", $"inline;filename=\"{id}\"");
Response.Headers.Add("Accept-Ranges", "bytes");
Response.Headers.Add("Cache-Control", "max-age=120");
Response.ContentType = "image";
return BlogPostAdminAppService.GetCoverImageAsync(id);
}
[HttpGet]
[Authorize(CmsKitAdminPermissions.BlogPosts.Default)]
public virtual Task<PagedResultDto<BlogPostDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{
return BlogPostAdminAppService.GetListAsync(input);
}
[NonAction]
public virtual Task SetCoverImageAsync(Guid id, RemoteStreamContent streamContent)
{
return BlogPostAdminAppService.SetCoverImageAsync(id, streamContent);
}
[HttpPost]
[Route("{id}/cover-image")]
[Authorize(CmsKitAdminPermissions.BlogPosts.Update)]
public virtual async Task<IActionResult> UploadCoverImageAsync(Guid id, IFormFile file)
{
if (file == null)
{
return BadRequest();
}
using (var stream = file.OpenReadStream())
{
await BlogPostAdminAppService.SetCoverImageAsync(id, new RemoteStreamContent(stream));
}
return CreatedAtAction(nameof(GetCoverImageAsync), new { id });
}
[HttpPut]
[Route("{id}")]
[Authorize(CmsKitAdminPermissions.BlogPosts.Update)]

3
modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/Create.cshtml.cs

@ -62,6 +62,9 @@ namespace Volo.CmsKit.Admin.Web.Pages.CmsKit.BlogPosts
[HiddenInput]
[DynamicMaxLength(typeof(BlogPostConsts), nameof(BlogPostConsts.MaxContentLength))]
public string Content { get; set; }
[HiddenInput]
public Guid? CoverImageMediaId { get; set; }
}
}
}

2
modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/Update.cshtml

@ -45,7 +45,7 @@
<abp-card-body>
<div class="form-group">
<img height="120" src="/api/cms-kit-admin/blogs/blog-posts/@Model.Id/cover-image" />
<img height="120" src="/api/cms-kit/media/@Model.ViewModel.CoverImageMediaId" />
<label>@L["CoverImage"]</label>
<input type="file" id="BlogPostCoverImage" class="form-control" />
</div>

3
modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/Update.cshtml.cs

@ -76,6 +76,9 @@ namespace Volo.CmsKit.Admin.Web.Pages.CmsKit.BlogPosts
[HiddenInput]
[DynamicMaxLength(typeof(BlogPostConsts), nameof(BlogPostConsts.MaxContentLength))]
public string Content { get; set; }
[HiddenInput]
public Guid? CoverImageMediaId { get; set; }
}
}
}

37
modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/create.js

@ -6,6 +6,7 @@
var $formCreate = $('#form-blog-post-create');
var $title = $('#ViewModel_Title');
var $shortDescription = $('#ViewModel_ShortDescription');
var $coverImage = $('#ViewModel_CoverImageMediaId');
var $url = $('#ViewModel_Slug');
var $buttonSubmit = $('#button-blog-post-create');
var $pageContentInput = $('#ViewModel_Content');
@ -13,7 +14,6 @@
var $fileInput = $('#BlogPostCoverImage');
var $tagsWrapper = $('#blog-post-tags-wrapper');
var UPPY_UPLOAD_ENDPOINT = "/api/cms-kit-admin/blogs/blog-posts/{0}/cover-image";
var UPPY_FILE_ID = "uppy-upload-file";
var isTagsEnabled = true;
@ -37,15 +37,13 @@
if ($formCreate.valid()) {
abp.ui.setBusy();
$formCreate.ajaxSubmit({
success: function (result) {
if (isTagsEnabled) {
submitEntityTags(result.id);
}
else {
submitCoverImage(result.id);
finishSaving();
}
},
error: function (result) {
@ -54,22 +52,25 @@
}
});
}
else {
abp.ui.clearBusy();
}
});
$buttonSubmit.click(function (e) {
e.preventDefault();
$formCreate.submit();
submitCoverImage();
});
function submitEntityTags(blogPostId) {
var tags = $tagsInput.val().split(',').map(x => x.trim()).filter(x => x);
if(tags.length === 0){
submitCoverImage(blogPostId);
if (tags.length === 0) {
finishSaving();
return;
}
volo.cmsKit.admin.tags.entityTagAdmin
.setEntityTags({
entityType: 'BlogPost',
@ -77,7 +78,7 @@
tags: tags
})
.then(function (result) {
submitCoverImage(blogPostId);
finishSaving(result);
});
}
@ -88,9 +89,11 @@
return headers;
}
function submitCoverImage(blogPostId) {
function submitCoverImage() {
abp.ui.setBusy();
var UPPY_OPTIONS = {
endpoint: UPPY_UPLOAD_ENDPOINT.replace("{0}", blogPostId),
endpoint: fileUploadUri,
formData: true,
fieldName: "file",
method: "post",
@ -116,12 +119,14 @@
if (result.failed.length > 0) {
abp.message.error(l("UploadFailedMessage"));
} else {
finishSaving();
$coverImage.val(result.successful[0].response.body.id);
$formCreate.submit();
}
});
}
else {
finishSaving();
$formCreate.submit();
}
}
@ -201,7 +206,7 @@
}
});
});
// -----------------------------------
var fileUploadUri = "/api/cms-kit-admin/media/blogpost";
var fileUriPrefix = "/api/cms-kit/media/";

46
modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/update.js

@ -4,21 +4,19 @@
var $selectBlog = $('#BlogSelectionSelect');
var $formUpdate = $('#form-blog-post-update');
var $title = $('#ViewModel_Title');
var $titleClone = $('#title-clone');
var $coverImage = $('#ViewModel_CoverImageMediaId');
var $slug = $('#ViewModel_Slug');
var $buttonSubmit = $('#button-blog-post-update');
var $blogPostIdInput = $('#Id');
var $tagsInput = $('.tag-editor-form input[name=tags]');
var $fileInput = $('#BlogPostCoverImage');
var UPPY_UPLOAD_ENDPOINT = "/api/cms-kit-admin/blogs/blog-posts/{0}/cover-image";
var UPPY_FILE_ID = "uppy-upload-file";
var isTagsEnabled = true;
$formUpdate.data('validator').settings.ignore = ":hidden, [contenteditable='true']:not([name]), .tui-popup-wrapper";
function initSelectBlog() {
$selectBlog.data('autocompleteApiUrl', '/api/cms-kit-admin/blogs/blogs');
$selectBlog.data('autocompleteDisplayProperty', 'name');
@ -44,30 +42,34 @@
submitEntityTags($blogPostIdInput.val());
}
else {
submitCoverImage($blogPostIdInput.val());
finishSaving(result);
}
},
error: function (result) {
abp.ui.clearBusy(); abp.notify.error(result.responseJSON.error.message);
abp.ui.clearBusy();
abp.notify.error(result.responseJSON.error.message);
}
});
}
else {
abp.ui.clearBusy();
}
});
$buttonSubmit.click(function (e) {
e.preventDefault();
$formUpdate.submit();
submitCoverImage();
});
function submitEntityTags(blogPostId) {
function submitEntityTags(blogPostId) {
var tags = $tagsInput.val().split(',').map(x => x.trim()).filter(x => x);
if(tags.length === 0){
submitCoverImage(blogPostId);
if (tags.length === 0) {
finishSaving();
return;
}
volo.cmsKit.admin.tags.entityTagAdmin
.setEntityTags({
entityType: 'BlogPost',
@ -75,7 +77,7 @@
tags: tags
})
.then(function (result) {
submitCoverImage(blogPostId);
finishSaving(result);
});
}
@ -86,9 +88,11 @@
return headers;
}
function submitCoverImage(blogPostId) {
function submitCoverImage() {
abp.ui.setBusy();
var UPPY_OPTIONS = {
endpoint: UPPY_UPLOAD_ENDPOINT.replace("{0}", blogPostId),
endpoint: fileUploadUri,
formData: true,
fieldName: "file",
method: "post",
@ -114,12 +118,14 @@
if (result.failed.length > 0) {
abp.message.error(l("UploadFailedMessage"));
} else {
finishSaving();
$coverImage.val(result.successful[0].response.body.id);
$formUpdate.submit();
}
});
}
else {
finishSaving();
$formUpdate.submit();
}
}
@ -129,10 +135,6 @@
location.href = "../../BlogPosts";
}
$titleClone.on('change paste keyup', function () {
$title.val($titleClone.val());
});
$slug.on('change', function () {
reflectUrlChanges();
});

122
modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/zh-Hans.json

@ -1,22 +1,124 @@
{
"culture": "zh-Hans",
"texts": {
"PickYourReaction": "选择你的回应",
"YourComment": "你的评论",
"YourReply": "你的回复",
"BlogDeletionConfirmationMessage": "博客 '{0}' 将被删除. 你确定吗?",
"BlogFeatureNotAvailable": "这个功能目前不可用. 使用 `GlobalFeatureManager` 来启用它.",
"BlogId": "博客",
"BlogPostDeletionConfirmationMessage": "博客帖子 '{0}' 将被删除. 你确定吗?",
"BlogPosts": "博客帖子",
"Blogs": "博客",
"ChoosePreference": "选择首选项...",
"Cms": "Cms",
"CmsKit.Comments": "评论",
"CmsKit.Ratings": "评分",
"CmsKit.Reactions": "反应",
"CmsKit.Tags": "标签",
"CmsKit:0002": "内容已经存在!",
"CmsKit:0003": "实体 {0} 不可标记.",
"CmsKit:Blog:0001": "给定的slug ({Slug}) 已经存在!",
"CmsKit:BlogPost:0001": "给定的slug已经存在!",
"CmsKit:Comments:0001": "实体不可 {0} 不可评论.",
"CmsKit:Media:0001": "'{Name}' 不是有效的媒体名称.",
"CmsKit:Media:0002": "实体不可以含有媒体",
"CmsKit:Page:0001": "给定的url ({0}) 已经存在.",
"CmsKit:Tag:0002": "实体不可标记!",
"CommentAuthorizationExceptionMessage": "这些评论不允许公开显示",
"CommentDeletionConfirmationMessage": "此评论和所有回复将被删除!",
"Comments": "评论",
"Send": "发送",
"ContentDeletionConfirmationMessage": "你确定要删除这个内容吗?",
"Contents": "内容",
"CoverImage": "封面图片",
"CreateBlogPostPage": "新博客帖子",
"CreationTime": "创建时间",
"Delete": "删除",
"Reply": "回复",
"Update": "更新",
"Detail": "详情",
"Details": "详情",
"DoYouPreferAdditionalEmails": "你是否更喜欢额外的邮件?",
"Edit": "修改",
"EndDate": "结束时间",
"EntityId": "实体Id",
"EntityType": "实体类型",
"ExportCSV": "导出CSV",
"Features": "功能",
"GenericDeletionConfirmationMessage": "你确定删除 '{0}' 吗?",
"LastModification": "最后一次修改",
"LoginToAddComment": "登录添加评论",
"LoginToRate": "登录进行评分",
"LoginToReply": "登录进行回复",
"Menu:CMS": "CMS",
"Message": "消息",
"MessageDeletionConfirmationMessage": "这条评论将被完全删除",
"CommentAuthorizationExceptionMessage": "这些评论不允许公开显示",
"Undo": "撤消",
"Name": "名称",
"New": "新",
"OK": "OK",
"PageDeletionConfirmationMessage": "你确定删除这个页面吗?",
"PageSlugInformation": "Slug用于url. 你的url将是 '/pages/{{slug}}'.",
"Permission:BlogManagement": "博客管理",
"Permission:BlogManagement.Create": "创建",
"Permission:BlogManagement.Delete": "删除",
"Permission:BlogManagement.Features": "删除",
"Permission:BlogManagement.Update": "更新",
"Permission:BlogPostManagement": "博客帖子管理",
"Permission:BlogPostManagement.Create": "创建",
"Permission:BlogPostManagement.Delete": "删除",
"Permission:BlogPostManagement.Update": "更新",
"Permission:CmsKit": "Cms工具包",
"Permission:Comments": "评论管理",
"Permission:Comments.Delete": "删除",
"Permission:Contents": "内容管理",
"Permission:Contents.Create": "创建内容",
"Permission:Contents.Delete": "删除内容",
"Permission:Contents.Update": "更新内容",
"Permission:MediaDescriptorManagement": "媒体管理",
"Permission:MediaDescriptorManagement:Create": "创建",
"Permission:MediaDescriptorManagement:Delete": "删除",
"Permission:PageManagement": "页面管理",
"Permission:PageManagement:Create": "创建",
"Permission:PageManagement:Delete": "删除",
"Permission:PageManagement:Update": "更新",
"Permission:TagManagement": "标签管理",
"Permission:TagManagement.Create": "创建",
"Permission:TagManagement.Delete": "删除",
"Permission:TagManagement.Update": "更新",
"PickYourReaction": "选择你的回应",
"RatingUndoMessage": "您的评分将被撤消",
"LoginToRate": "登录进行评分",
"Star": "星"
"Read": "阅读",
"RepliesToThisComment": "回复此评论",
"Reply": "回复",
"ReplyTo": "回复",
"SamplePageMessage": "Pro模块的示例页面",
"SaveChanges": "保存更改",
"SelectAll": "选择所有",
"Send": "发送",
"SendMessage": "发送消息",
"ShortDescription": "简介",
"Slug": "Slug",
"Source": "源",
"SourceUrl": "源URL",
"Star": "星",
"StartDate": "开始时间",
"Subject": "主题",
"SubjectPlaceholder": "请输入主题",
"Submit": "提交",
"Subscribe": "订阅",
"SuccessfullyDeleted": "删除成功!",
"SuccessfullySaved": "保存成功!",
"TagDeletionConfirmationMessage": "你确定删除 '{0}' 标签吗?",
"Tags": "标签",
"Text": "文本",
"ThankYou": "谢谢你",
"Title": "标题",
"Undo": "撤消",
"Update": "更新",
"UpdatePreferenceSuccessMessage": "您的首选项已经保存",
"UpdateYourEmailPreferences": "更新你的邮件首选项",
"UploadFailedMessage": "上传失败",
"UserId": "用户Id",
"Username": "用户名称",
"YourComment": "你的评论",
"YourEmailAddress": "你的邮件地址",
"YourFullName": "你的全称",
"YourMessage": "你的消息",
"YourReply": "你的回复"
}
}

12
modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogPost.cs

@ -12,17 +12,19 @@ namespace Volo.CmsKit.Blogs
{
public virtual Guid BlogId { get; protected set; }
[NotNull]
[NotNull]
public virtual string Title { get; protected set; }
[NotNull]
[NotNull]
public virtual string Slug { get; protected set; }
[NotNull]
[NotNull]
public virtual string ShortDescription { get; protected set; }
public virtual string Content { get; protected set; }
public Guid? CoverImageMediaId { get; set; }
public virtual Guid? TenantId { get; protected set; }
public Guid AuthorId { get; set; }
@ -41,6 +43,7 @@ namespace Volo.CmsKit.Blogs
[NotNull] string slug,
[CanBeNull] string shortDescription = null,
[CanBeNull] string content = null,
[CanBeNull] Guid? coverImageMediaId = null,
[CanBeNull] Guid? tenantId = null) : base(id)
{
TenantId = tenantId;
@ -50,6 +53,7 @@ namespace Volo.CmsKit.Blogs
SetSlug(slug);
SetShortDescription(shortDescription);
SetContent(content);
CoverImageMediaId = coverImageMediaId;
}
public virtual void SetTitle(string title)

9
modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogPostCoverImageContainer.cs

@ -1,9 +0,0 @@
using Volo.Abp.BlobStoring;
namespace Volo.CmsKit.Blogs
{
[BlobContainerName("blog-post-cover-images")]
public class BlogPostCoverImageContainer
{
}
}

4
modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogPostManager.cs

@ -27,7 +27,8 @@ namespace Volo.CmsKit.Blogs
[NotNull] string title,
[NotNull] string slug,
[CanBeNull] string shortDescription = null,
[CanBeNull] string content = null)
[CanBeNull] string content = null,
[CanBeNull] Guid? coverImageMediaId = null)
{
Check.NotNull(author, nameof(author));
Check.NotNull(blog, nameof(blog));
@ -44,6 +45,7 @@ namespace Volo.CmsKit.Blogs
slug,
shortDescription,
content,
coverImageMediaId,
CurrentTenant.Id
);

4
modules/cms-kit/src/Volo.CmsKit.HttpApi/Volo.CmsKit.HttpApi.csproj

@ -9,9 +9,9 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.CmsKit.Admin.Application\Volo.CmsKit.Admin.Application.csproj" />
<ProjectReference Include="..\Volo.CmsKit.Admin.HttpApi\Volo.CmsKit.Admin.HttpApi.csproj" />
<ProjectReference Include="..\Volo.CmsKit.Application.Contracts\Volo.CmsKit.Application.Contracts.csproj" />
<ProjectReference Include="..\Volo.CmsKit.Public.Application\Volo.CmsKit.Public.Application.csproj" />
<ProjectReference Include="..\Volo.CmsKit.Public.HttpApi\Volo.CmsKit.Public.HttpApi.csproj" />
</ItemGroup>
</Project>

4
modules/cms-kit/src/Volo.CmsKit.HttpApi/Volo/CmsKit/CmsKitHttpApiModule.cs

@ -5,8 +5,8 @@ using Volo.CmsKit.Public;
namespace Volo.CmsKit
{
[DependsOn(
typeof(CmsKitAdminApplicationModule),
typeof(CmsKitPublicApplicationModule),
typeof(CmsKitAdminHttpApiModule),
typeof(CmsKitPublicHttpApiModule),
typeof(CmsKitApplicationContractsModule)
)]
public class CmsKitHttpApiModule : AbpModule

4
modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Blogs/BlogPostPublicDto.cs

@ -16,7 +16,9 @@ namespace Volo.CmsKit.Public.Blogs
public string ShortDescription { get; set; }
public string Content { get; set; }
public Guid? CoverImageMediaId { get; set; }
public CmsUserDto Author { get; set; }
}
}

2
modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Blogs/IBlogPostPublicAppService.cs

@ -12,7 +12,5 @@ namespace Volo.CmsKit.Public.Blogs
Task<PagedResultDto<BlogPostPublicDto>> GetListAsync([NotNull] string blogSlug, PagedAndSortedResultRequestDto input);
Task<BlogPostPublicDto> GetAsync([NotNull] string blogSlug, [NotNull] string blogPostSlug);
Task<RemoteStreamContent> GetCoverImageAsync(Guid id);
}
}

13
modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Blogs/BlogPostPublicAppService.cs

@ -15,16 +15,12 @@ namespace Volo.CmsKit.Public.Blogs
protected IBlogPostRepository BlogPostRepository { get; }
protected IBlobContainer<BlogPostCoverImageContainer> BlobContainer { get; }
public BlogPostPublicAppService(
IBlogRepository blogRepository,
IBlogPostRepository blogPostRepository,
IBlobContainer<BlogPostCoverImageContainer> blobContainer)
IBlogPostRepository blogPostRepository)
{
BlogRepository = blogRepository;
BlogPostRepository = blogPostRepository;
BlobContainer = blobContainer;
}
public virtual async Task<BlogPostPublicDto> GetAsync([NotNull] string blogSlug, [NotNull] string blogPostSlug)
@ -46,12 +42,5 @@ namespace Volo.CmsKit.Public.Blogs
await BlogPostRepository.GetCountAsync(blog.Id),
ObjectMapper.Map<List<BlogPost>, List<BlogPostPublicDto>>(blogPosts));
}
public virtual async Task<RemoteStreamContent> GetCoverImageAsync(Guid id)
{
var stream = await BlobContainer.GetAsync(id.ToString());
return new RemoteStreamContent(stream);
}
}
}

12
modules/cms-kit/src/Volo.CmsKit.Public.HttpApi/Volo/CmsKit/Public/Blogs/BlogPostPublicController.cs

@ -29,18 +29,6 @@ namespace Volo.CmsKit.Public.Blogs
return BlogPostPublicAppService.GetAsync(blogSlug, blogPostSlug);
}
[HttpGet]
[Route("{id}/cover-image")]
public virtual Task<RemoteStreamContent> GetCoverImageAsync(Guid id)
{
Response.Headers.Add("Content-Disposition", $"inline;filename=\"{id}\"");
Response.Headers.Add("Accept-Ranges", "bytes");
Response.Headers.Add("Cache-Control", "max-age=120");
Response.ContentType = "image";
return BlogPostPublicAppService.GetCoverImageAsync(id);
}
[HttpGet]
[Route("{blogSlug}")]
public virtual Task<PagedResultDto<BlogPostPublicDto>> GetListAsync(string blogSlug, PagedAndSortedResultRequestDto input)

8
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Blogs/BlogPost/Default.cshtml

@ -4,14 +4,16 @@
@inject IStringLocalizer<CmsKitResource> L
@{
const string dummyImageSource = "https://dummyimage.com/300x200/a3a3a3/fff.png";
}
<abp-card>
<abp-card-header>
<img src="/api/cms-kit/media/@Model.CoverImageMediaId" class="card-img-top" onerror="this.src='@dummyImageSource'" />
<abp-card-body>
<abp-card-title>@Model.Title</abp-card-title>
<abp-card-subtitle>@Model.Author?.UserName</abp-card-subtitle>
</abp-card-header>
<abp-card-body>
@Html.Raw(Model.Content)
</abp-card-body>
<abp-card-footer>

4
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Pages/Default.cshtml

@ -3,9 +3,7 @@
@model Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Pages.PageViewModel
@inject IPageLayout PageLayout
@{
PageLayout.Content.Title = Model.Title;
}
<h1>@Model.Title</h1>
<abp-card>
<abp-card-body>

4
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/BlogPost.cshtml

@ -13,10 +13,6 @@
@model Volo.CmsKit.Public.Web.Pages.Public.CmsKit.Blogs.BlogPostModel
@{
PageLayout.Content.Title = Model.BlogPost.Title;
}
@await Component.InvokeAsync(typeof(DefaultBlogPostViewComponent), new
{
Model.BlogSlug,

15
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/Index.cshtml

@ -1,12 +1,16 @@
@page
@using Volo.CmsKit.Public.Web.Pages
@using Volo.CmsKit.Public.Web.Pages.Public.CmsKit.Blogs
@using Volo.CmsKit.Public.Web.Pages.Public.CmsKit.Blogs
@inherits CmsKitPublicPageBase
@model IndexModel
@{
const string dummyImageSource = "https://dummyimage.com/300x200/a3a3a3/fff.png";
}
<abp-row id="blogs-container">
@foreach (var blog in Model.Blogs.Items)
{
@ -21,7 +25,14 @@
</abp-card-subtitle>
</abp-card-header>
<abp-card-body>
<img src="/api/cms-kit-public/blog-posts/@blog.Id/cover-image" class="card-img-top" style="max-width:200px;max-height:200px;" onerror="this.src='https://dummyimage.com/300x200/a3a3a3/fff.png'" />
@if (blog.CoverImageMediaId != null)
{
<img src="/api/cms-kit/media/@blog.CoverImageMediaId" class="card-img-top" style="max-width:200px;max-height:200px;" onerror="this.src='@dummyImageSource'" />
}
else
{
<img src="@dummyImageSource" class="card-img-top" style="max-width:200px;max-height:200px;" />
}
<abp-card-text>
@blog.ShortDescription
</abp-card-text>

23
modules/cms-kit/test/Volo.CmsKit.Application.Tests/Blogs/BlogPostAdminAppService_Tests.cs

@ -156,28 +156,5 @@ namespace Volo.CmsKit.Blogs
exception.EntityType.ShouldBe(typeof(BlogPost));
exception.Id.ShouldBe(cmsKitTestData.Page_2_Id);
}
[Fact]
public async Task SetCoverImage_ShouldNotThrowException_WithCorrectData()
{
using (var imageStream = GetSampleImageStream())
{
await WithUnitOfWorkAsync(async () =>
{
await blogPostAdminAppService.SetCoverImageAsync(
cmsKitTestData.BlogPost_2_Id,
new RemoteStreamContent(imageStream));
});
}
}
private Stream GetSampleImageStream()
{
var assembly = GetType().Assembly;
var resourceName = "Volo.CmsKit.Data.BlogPostSample.png";
return assembly.GetManifestResourceStream(resourceName);
}
// SetCoverImage
}
}

7
modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/LocalizationExtensions/zh-Hans.json

@ -0,0 +1,7 @@
{
"culture": "zh-Hans",
"texts": {
"Volo.Abp.Identity:PasswordTooShort": "密码长度必须大于{0}字符. ",
"Volo.Abp.Identity:PasswordRequiresNonAlphanumeric": "密码必须至少包含一个非字母数字字符."
}
}

18
modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpClaimsService.cs

@ -4,12 +4,15 @@ using System.Security.Claims;
using IdentityModel;
using IdentityServer4.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.Security.Claims;
namespace Volo.Abp.IdentityServer
{
public class AbpClaimsService : DefaultClaimsService
{
protected readonly AbpClaimsServiceOptions Options;
private static readonly string[] AdditionalOptionalClaimNames =
{
AbpClaimTypes.TenantId,
@ -20,20 +23,21 @@ namespace Volo.Abp.IdentityServer
JwtClaimTypes.FamilyName,
};
public AbpClaimsService(IProfileService profile, ILogger<DefaultClaimsService> logger)
public AbpClaimsService(
IProfileService profile,
ILogger<DefaultClaimsService> logger,
IOptions<AbpClaimsServiceOptions> options)
: base(profile, logger)
{
Options = options.Value;
}
protected override IEnumerable<string> FilterRequestedClaimTypes(IEnumerable<string> claimTypes)
{
return base.FilterRequestedClaimTypes(claimTypes)
.Union(new []{
AbpClaimTypes.TenantId,
AbpClaimTypes.EditionId
});
.Union(Options.RequestedClaims);
}
protected override IEnumerable<Claim> GetOptionalClaims(ClaimsPrincipal subject)
{
return base.GetOptionalClaims(subject)
@ -52,4 +56,4 @@ namespace Volo.Abp.IdentityServer
}
}
}
}
}

14
modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpClaimsServiceOptions.cs

@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace Volo.Abp.IdentityServer
{
public class AbpClaimsServiceOptions
{
public List<string> RequestedClaims { get; }
public AbpClaimsServiceOptions()
{
RequestedClaims = new List<string>();
}
}
}

10
modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpIdentityServerDomainModule.cs

@ -10,7 +10,6 @@ using Volo.Abp.Domain.Entities.Events.Distributed;
using Volo.Abp.Identity;
using Volo.Abp.IdentityServer.ApiResources;
using Volo.Abp.IdentityServer.AspNetIdentity;
using Volo.Abp.IdentityServer.ApiScopes;
using Volo.Abp.IdentityServer.Clients;
using Volo.Abp.IdentityServer.Devices;
using Volo.Abp.IdentityServer.IdentityResources;
@ -19,6 +18,7 @@ using Volo.Abp.Modularity;
using Volo.Abp.ObjectExtending;
using Volo.Abp.ObjectExtending.Modularity;
using Volo.Abp.Security;
using Volo.Abp.Security.Claims;
using Volo.Abp.Validation;
using Volo.Abp.Threading;
@ -54,6 +54,14 @@ namespace Volo.Abp.IdentityServer
options.EtoMappings.Add<IdentityResource, IdentityResourceEto>(typeof(AbpIdentityServerDomainModule));
});
Configure<AbpClaimsServiceOptions>(options =>
{
options.RequestedClaims.AddRange(new []{
AbpClaimTypes.TenantId,
AbpClaimTypes.EditionId
});
});
AddIdentityServer(context.Services);
}

4
modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpProfileService.cs

@ -24,7 +24,7 @@ namespace Volo.Abp.IdentityServer.AspNetIdentity
}
[UnitOfWork]
public async override Task GetProfileDataAsync(ProfileDataRequestContext context)
public override async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
using (CurrentTenant.Change(context.Subject.FindTenantId()))
{
@ -33,7 +33,7 @@ namespace Volo.Abp.IdentityServer.AspNetIdentity
}
[UnitOfWork]
public async override Task IsActiveAsync(IsActiveContext context)
public override async Task IsActiveAsync(IsActiveContext context)
{
using (CurrentTenant.Change(context.Subject.FindTenantId()))
{

37
npm/ng-packs/angular.json

@ -184,6 +184,43 @@
}
}
},
"account": {
"projectType": "library",
"root": "packages/account",
"sourceRoot": "packages/account/src",
"prefix": "abp",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"options": {
"tsConfig": "packages/account/tsconfig.lib.json",
"project": "packages/account/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "packages/account/tsconfig.lib.prod.json"
}
}
},
"test": {
"builder": "@angular-builders/jest:run",
"options": {
"coverage": true,
"passWithNoTests": true
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"packages/account/tsconfig.lib.json",
"packages/account/tsconfig.spec.json"
],
"exclude": ["**/node_modules/**"]
}
}
}
},
"identity": {
"projectType": "library",
"root": "packages/identity",

4
npm/ng-packs/apps/dev-app/src/app/app-routing.module.ts

@ -7,6 +7,10 @@ const routes: Routes = [
pathMatch: 'full',
loadChildren: () => import('./home/home.module').then(m => m.HomeModule),
},
{
path: 'account',
loadChildren: () => import('@abp/ng.account').then(m => m.AccountModule.forLazy()),
},
{
path: 'identity',
loadChildren: () => import('@abp/ng.identity').then(m => m.IdentityModule.forLazy()),

2
npm/ng-packs/apps/dev-app/src/app/app.module.ts

@ -11,6 +11,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { InspectorModule } from '@ngneat/inspector';
import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin';
import { NgxsModule } from '@ngxs/store';
import { AccountConfigModule } from '@abp/ng.account/config';
import { environment } from '../environments/environment';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@ -33,6 +34,7 @@ const INSPECTION_TOOLS = [
skipGetAppConfiguration: false,
}),
ThemeSharedModule.forRoot(),
AccountConfigModule.forRoot(),
IdentityConfigModule.forRoot(),
TenantManagementConfigModule.forRoot(),
SettingManagementConfigModule.forRoot(),

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

@ -14,6 +14,6 @@ export class HomeComponent {
constructor(private oAuthService: OAuthService, private authService: AuthService) {}
login() {
this.authService.initLogin();
this.authService.navigateToLogin();
}
}

5
npm/ng-packs/apps/dev-app/src/environments/environment.ts

@ -12,10 +12,11 @@ export const environment = {
},
oAuthConfig: {
issuer: 'https://localhost:44305',
redirectUri: baseUrl,
clientId: 'MyProjectName_App',
responseType: 'code',
dummyClientSecret: '1q2w3e*',
scope: 'offline_access MyProjectName',
// responseType: 'code',
// redirectUri: baseUrl
},
apis: {
default: {

3
npm/ng-packs/packages/account/README.md

@ -0,0 +1,3 @@
<h1> @abp/ng.account </h1>
[docs.abp.io](https://docs.abp.io)

7
npm/ng-packs/packages/account/config/ng-package.json

@ -0,0 +1,7 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/account/config",
"lib": {
"entryFile": "src/public-api.ts"
}
}

12
npm/ng-packs/packages/account/config/src/account-config.module.ts

@ -0,0 +1,12 @@
import { ModuleWithProviders, NgModule } from '@angular/core';
import { ACCOUNT_ROUTE_PROVIDERS } from './providers/route.provider';
@NgModule()
export class AccountConfigModule {
static forRoot(): ModuleWithProviders<AccountConfigModule> {
return {
ngModule: AccountConfigModule,
providers: [ACCOUNT_ROUTE_PROVIDERS],
};
}
}

1
npm/ng-packs/packages/account/config/src/enums/index.ts

@ -0,0 +1 @@
export * from './route-names';

6
npm/ng-packs/packages/account/config/src/enums/route-names.ts

@ -0,0 +1,6 @@
export const enum eAccountRouteNames {
Account = 'AbpAccount::Menu:Account',
Login = 'AbpAccount::Login',
Register = 'AbpAccount::Register',
ManageProfile = 'AbpAccount::ManageYourProfile',
}

1
npm/ng-packs/packages/account/config/src/providers/index.ts

@ -0,0 +1 @@
export * from './route.provider';

39
npm/ng-packs/packages/account/config/src/providers/route.provider.ts

@ -0,0 +1,39 @@
import { eLayoutType, RoutesService } from '@abp/ng.core';
import { APP_INITIALIZER } from '@angular/core';
import { eAccountRouteNames } from '../enums/route-names';
export const ACCOUNT_ROUTE_PROVIDERS = [
{ provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true },
];
export function configureRoutes(routes: RoutesService) {
return () => {
routes.add([
{
path: '/account',
name: eAccountRouteNames.Account,
invisible: true,
layout: eLayoutType.application,
order: 1,
},
{
path: '/account/login',
name: eAccountRouteNames.Login,
parentName: eAccountRouteNames.Account,
order: 1,
},
{
path: '/account/register',
name: eAccountRouteNames.Register,
parentName: eAccountRouteNames.Account,
order: 2,
},
{
path: '/account/manage-profile',
name: eAccountRouteNames.ManageProfile,
parentName: eAccountRouteNames.Account,
order: 3,
},
]);
};
}

3
npm/ng-packs/packages/account/config/src/public-api.ts

@ -0,0 +1,3 @@
export * from './account-config.module';
export * from './enums';
export * from './providers';

6
npm/ng-packs/packages/account/jest.config.js

@ -0,0 +1,6 @@
const jestConfig = require('../../jest.config');
module.exports = {
...jestConfig,
name: 'account',
};

8
npm/ng-packs/packages/account/ng-package.json

@ -0,0 +1,8 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/account",
"lib": {
"entryFile": "src/public-api.ts"
},
"whitelistedNonPeerDependencies": ["@abp/ng.theme.shared"]
}

6
npm/ng-packs/packages/account/ngcc.config.js

@ -0,0 +1,6 @@
module.exports = {
entryPoints: {
'.': {},
'./config': {}
},
};

16
npm/ng-packs/packages/account/package.json

@ -0,0 +1,16 @@
{
"name": "@abp/ng.account",
"version": "4.3.0",
"homepage": "https://abp.io",
"repository": {
"type": "git",
"url": "https://github.com/abpframework/abp.git"
},
"dependencies": {
"@abp/ng.theme.shared": "~3.3.2",
"tslib": "^2.0.0"
},
"publishConfig": {
"access": "public"
}
}

93
npm/ng-packs/packages/account/src/lib/account-routing.module.ts

@ -0,0 +1,93 @@
import {
AuthGuard,
DynamicLayoutComponent,
ReplaceableComponents,
ReplaceableRouteContainerComponent,
} from '@abp/ng.core';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthWrapperComponent } from './components/auth-wrapper/auth-wrapper.component';
import { ForgotPasswordComponent } from './components/forgot-password/forgot-password.component';
import { LoginComponent } from './components/login/login.component';
import { ManageProfileComponent } from './components/manage-profile/manage-profile.component';
import { RegisterComponent } from './components/register/register.component';
import { ResetPasswordComponent } from './components/reset-password/reset-password.component';
import { eAccountComponents } from './enums/components';
import { AuthenticationFlowGuard } from './guards/authentication-flow.guard';
const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'login' },
{
path: '',
component: DynamicLayoutComponent,
children: [
{
path: '',
component: AuthWrapperComponent,
children: [
{
path: 'login',
component: ReplaceableRouteContainerComponent,
canActivate: [AuthenticationFlowGuard],
data: {
replaceableComponent: {
key: eAccountComponents.Login,
defaultComponent: LoginComponent,
} as ReplaceableComponents.RouteData<LoginComponent>,
},
},
{
path: 'register',
component: ReplaceableRouteContainerComponent,
canActivate: [AuthenticationFlowGuard],
data: {
replaceableComponent: {
key: eAccountComponents.Register,
defaultComponent: RegisterComponent,
} as ReplaceableComponents.RouteData<RegisterComponent>,
},
},
{
path: 'forgot-password',
component: ReplaceableRouteContainerComponent,
canActivate: [AuthenticationFlowGuard],
data: {
replaceableComponent: {
key: eAccountComponents.ForgotPassword,
defaultComponent: ForgotPasswordComponent,
} as ReplaceableComponents.RouteData<ForgotPasswordComponent>,
},
},
{
path: 'reset-password',
component: ReplaceableRouteContainerComponent,
canActivate: [AuthenticationFlowGuard],
data: {
replaceableComponent: {
key: eAccountComponents.ResetPassword,
defaultComponent: ResetPasswordComponent,
} as ReplaceableComponents.RouteData<ResetPasswordComponent>,
},
},
],
},
{
path: 'manage-profile',
component: ReplaceableRouteContainerComponent,
canActivate: [AuthGuard],
data: {
replaceableComponent: {
key: eAccountComponents.ManageProfile,
defaultComponent: ManageProfileComponent,
} as ReplaceableComponents.RouteData<ManageProfileComponent>,
},
},
],
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class AccountRoutingModule {}

63
npm/ng-packs/packages/account/src/lib/account.module.ts

@ -0,0 +1,63 @@
import { CoreModule, LazyModuleFactory } from '@abp/ng.core';
import { ThemeSharedModule } from '@abp/ng.theme.shared';
import { ModuleWithProviders, NgModule, NgModuleFactory } from '@angular/core';
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { NgxValidateCoreModule } from '@ngx-validate/core';
import { AccountRoutingModule } from './account-routing.module';
import { AuthWrapperComponent } from './components/auth-wrapper/auth-wrapper.component';
import { ChangePasswordComponent } from './components/change-password/change-password.component';
import { LoginComponent } from './components/login/login.component';
import { ManageProfileComponent } from './components/manage-profile/manage-profile.component';
import { PersonalSettingsComponent } from './components/personal-settings/personal-settings.component';
import { RegisterComponent } from './components/register/register.component';
import { TenantBoxComponent } from './components/tenant-box/tenant-box.component';
import { AccountConfigOptions } from './models/config-options';
import { ACCOUNT_CONFIG_OPTIONS } from './tokens/config-options.token';
import { accountConfigOptionsFactory } from './utils/factory-utils';
import { AuthenticationFlowGuard } from './guards/authentication-flow.guard';
import { ForgotPasswordComponent } from './components/forgot-password/forgot-password.component';
import { ResetPasswordComponent } from './components/reset-password/reset-password.component';
const declarations = [
AuthWrapperComponent,
LoginComponent,
RegisterComponent,
TenantBoxComponent,
ChangePasswordComponent,
ManageProfileComponent,
PersonalSettingsComponent,
ForgotPasswordComponent,
ResetPasswordComponent,
];
@NgModule({
declarations: [...declarations],
imports: [
CoreModule,
AccountRoutingModule,
ThemeSharedModule,
NgbDropdownModule,
NgxValidateCoreModule,
],
exports: [...declarations],
})
export class AccountModule {
static forChild(options = {} as AccountConfigOptions): ModuleWithProviders<AccountModule> {
return {
ngModule: AccountModule,
providers: [
AuthenticationFlowGuard,
{ provide: ACCOUNT_CONFIG_OPTIONS, useValue: options },
{
provide: 'ACCOUNT_OPTIONS',
useFactory: accountConfigOptionsFactory,
deps: [ACCOUNT_CONFIG_OPTIONS],
},
],
};
}
static forLazy(options = {} as AccountConfigOptions): NgModuleFactory<AccountModule> {
return new LazyModuleFactory(AccountModule.forChild(options));
}
}

25
npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.html

@ -0,0 +1,25 @@
<div class="row">
<div class="mx-auto col col-md-5">
<ng-container *ngIf="(isMultiTenancyEnabled$ | async) && multiTenancy.isTenantBoxVisible">
<abp-tenant-box *abpReplaceableTemplate="{ componentKey: tenantBoxKey }"></abp-tenant-box>
</ng-container>
<div class="abp-account-container">
<div
*ngIf="enableLocalLogin$ | async; else disableLocalLoginTemplate"
class="card mt-3 shadow-sm rounded"
>
<div class="card-body p-5">
<router-outlet></router-outlet>
</div>
</div>
</div>
</div>
</div>
<ng-template #disableLocalLoginTemplate>
<div class="alert alert-warning">
<strong>{{ 'AbpAccount::InvalidLoginRequest' | abpLocalization }}</strong>
{{ 'AbpAccount::ThereAreNoLoginSchemesConfiguredForThisClient' | abpLocalization }}
</div>
</ng-template>

28
npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.ts

@ -0,0 +1,28 @@
import { ConfigStateService, MultiTenancyService, SubscriptionService } from '@abp/ng.core';
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { eAccountComponents } from '../../enums/components';
@Component({
selector: 'abp-auth-wrapper',
templateUrl: './auth-wrapper.component.html',
exportAs: 'abpAuthWrapper',
providers: [SubscriptionService],
})
export class AuthWrapperComponent {
isMultiTenancyEnabled$ = this.configState.getDeep$('multiTenancy.isEnabled');
get enableLocalLogin$(): Observable<boolean> {
return this.configState
.getSetting$('Abp.Account.EnableLocalLogin')
.pipe(map(value => value?.toLowerCase() !== 'false'));
}
tenantBoxKey = eAccountComponents.TenantBox;
constructor(
public readonly multiTenancy: MultiTenancyService,
private configState: ConfigStateService,
) {}
}

48
npm/ng-packs/packages/account/src/lib/components/change-password/change-password.component.html

@ -0,0 +1,48 @@
<form [formGroup]="form" (ngSubmit)="onSubmit()" [mapErrorsFn]="mapErrorsFn" validateOnSubmit>
<div *ngIf="!hideCurrentPassword" class="form-group">
<label for="current-password">{{
'AbpIdentity::DisplayName:CurrentPassword' | abpLocalization
}}</label
><span> * </span
><input
type="password"
id="current-password"
class="form-control"
formControlName="password"
autofocus
autocomplete="current-password"
/>
</div>
<div class="form-group">
<label for="new-password">{{ 'AbpIdentity::DisplayName:NewPassword' | abpLocalization }}</label
><span> * </span
><input
type="password"
id="new-password"
class="form-control"
formControlName="newPassword"
autocomplete="new-password"
/>
</div>
<div class="form-group">
<label for="confirm-new-password">{{
'AbpIdentity::DisplayName:NewPasswordConfirm' | abpLocalization
}}</label
><span> * </span
><input
type="password"
id="confirm-new-password"
class="form-control"
formControlName="repeatNewPassword"
autocomplete="new-password"
/>
</div>
<abp-button
iconClass="fa fa-check"
buttonClass="btn btn-primary color-white"
buttonType="submit"
[loading]="inProgress"
[disabled]="form?.invalid"
>{{ 'AbpIdentity::Save' | abpLocalization }}</abp-button
>
</form>

99
npm/ng-packs/packages/account/src/lib/components/change-password/change-password.component.ts

@ -0,0 +1,99 @@
import { Profile, ProfileService } from '@abp/ng.core';
import { getPasswordValidators, ToasterService } from '@abp/ng.theme.shared';
import { Component, Injector, Input, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { comparePasswords, Validation } from '@ngx-validate/core';
import { finalize } from 'rxjs/operators';
import snq from 'snq';
import { Account } from '../../models/account';
import { ManageProfileStateService } from '../../services/manage-profile.state.service';
const { required } = Validators;
const PASSWORD_FIELDS = ['newPassword', 'repeatNewPassword'];
@Component({
selector: 'abp-change-password-form',
templateUrl: './change-password.component.html',
exportAs: 'abpChangePasswordForm',
})
export class ChangePasswordComponent
implements OnInit, Account.ChangePasswordComponentInputs, Account.ChangePasswordComponentOutputs {
form: FormGroup;
inProgress: boolean;
hideCurrentPassword: boolean;
mapErrorsFn: Validation.MapErrorsFn = (errors, groupErrors, control) => {
if (PASSWORD_FIELDS.indexOf(String(control.name)) < 0) return errors;
return errors.concat(groupErrors.filter(({ key }) => key === 'passwordMismatch'));
};
constructor(
private fb: FormBuilder,
private injector: Injector,
private toasterService: ToasterService,
private profileService: ProfileService,
private manageProfileState: ManageProfileStateService,
) {}
ngOnInit(): void {
this.hideCurrentPassword = !this.manageProfileState.getProfile()?.hasPassword;
const passwordValidations = getPasswordValidators(this.injector);
this.form = this.fb.group(
{
password: ['', required],
newPassword: [
'',
{
validators: [required, ...passwordValidations],
},
],
repeatNewPassword: [
'',
{
validators: [required, ...passwordValidations],
},
],
},
{
validators: [comparePasswords(PASSWORD_FIELDS)],
},
);
if (this.hideCurrentPassword) this.form.removeControl('password');
}
onSubmit() {
if (this.form.invalid) return;
this.inProgress = true;
this.profileService
.changePassword({
...(!this.hideCurrentPassword && { currentPassword: this.form.get('password').value }),
newPassword: this.form.get('newPassword').value,
})
.pipe(finalize(() => (this.inProgress = false)))
.subscribe({
next: () => {
this.form.reset();
this.toasterService.success('AbpAccount::PasswordChangedMessage', '', {
life: 5000,
});
if (this.hideCurrentPassword) {
this.hideCurrentPassword = false;
this.form.addControl('password', new FormControl('', [required]));
}
},
error: err => {
this.toasterService.error(
snq(() => err.error.error.message, 'AbpAccount::DefaultErrorMessage'),
);
},
});
}
}

40
npm/ng-packs/packages/account/src/lib/components/forgot-password/forgot-password.component.html

@ -0,0 +1,40 @@
<h4>{{ 'AbpAccount::ForgotPassword' | abpLocalization }}</h4>
<form
*ngIf="!isEmailSent; else emailSentTemplate"
[formGroup]="form"
(ngSubmit)="onSubmit()"
validateOnSubmit
>
<p>{{ 'AbpAccount::SendPasswordResetLink_Information' | abpLocalization }}</p>
<div class="form-group">
<label for="input-email-address">{{ 'AbpAccount::EmailAddress' | abpLocalization }}</label
><span> * </span>
<input type="email" id="input-email-address" class="form-control" formControlName="email" />
</div>
<abp-button
class="d-block"
buttonClass="mt-2 mb-3 btn btn-primary btn-block"
[loading]="inProgress"
buttonType="submit"
[disabled]="form?.invalid"
>
{{ 'AbpAccount::Submit' | abpLocalization }}
</abp-button>
<a routerLink="/account/login"
><i class="fa fa-long-arrow-left mr-1"></i>{{ 'AbpAccount::Login' | abpLocalization }}</a
>
</form>
<ng-template #emailSentTemplate>
<p>
{{ 'AbpAccount::PasswordResetMailSentMessage' | abpLocalization }}
</p>
<a routerLink="/account/login">
<button class="d-block mt-2 mb-3 btn btn-primary btn-block">
<i class="fa fa-long-arrow-left mr-1"></i>
{{ 'AbpAccount::BackToLogin' | abpLocalization }}
</button>
</a>
</ng-template>

35
npm/ng-packs/packages/account/src/lib/components/forgot-password/forgot-password.component.ts

@ -0,0 +1,35 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { finalize } from 'rxjs/operators';
import { AccountService } from '../../proxy/account/account.service';
@Component({
selector: 'abp-forgot-password',
templateUrl: 'forgot-password.component.html',
})
export class ForgotPasswordComponent {
form: FormGroup;
inProgress: boolean;
isEmailSent = false;
constructor(private fb: FormBuilder, private accountService: AccountService) {
this.form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
});
}
onSubmit() {
if (this.form.invalid) return;
this.inProgress = true;
this.accountService
.sendPasswordResetCode({ email: this.form.get('email').value, appName: 'Angular' })
.pipe(finalize(() => (this.inProgress = false)))
.subscribe(() => {
this.isEmailSent = true;
});
}
}

8
npm/ng-packs/packages/account/src/lib/components/index.ts

@ -0,0 +1,8 @@
export * from './change-password/change-password.component';
export * from './forgot-password/forgot-password.component';
export * from './login/login.component';
export * from './manage-profile/manage-profile.component';
export * from './register/register.component';
export * from './personal-settings/personal-settings.component';
export * from './reset-password/reset-password.component';
export * from './tenant-box/tenant-box.component';

60
npm/ng-packs/packages/account/src/lib/components/login/login.component.html

@ -0,0 +1,60 @@
<h4>{{ 'AbpAccount::Login' | abpLocalization }}</h4>
<strong *ngIf="isSelfRegistrationEnabled">
{{ 'AbpAccount::AreYouANewUser' | abpLocalization }}
<a class="text-decoration-none" routerLink="/account/register" queryParamsHandling="preserve">{{
'AbpAccount::Register' | abpLocalization
}}</a>
</strong>
<form [formGroup]="form" (ngSubmit)="onSubmit()" validateOnSubmit class="mt-4">
<div class="form-group">
<label for="login-input-user-name-or-email-address">{{
'AbpAccount::UserNameOrEmailAddress' | abpLocalization
}}</label>
<input
class="form-control"
type="text"
id="login-input-user-name-or-email-address"
formControlName="username"
autocomplete="username"
autofocus
/>
</div>
<div class="form-group">
<label for="login-input-password">{{ 'AbpAccount::Password' | abpLocalization }}</label>
<input
class="form-control"
type="password"
id="login-input-password"
formControlName="password"
autocomplete="current-password"
/>
</div>
<div class="row">
<div class="col">
<label class="custom-checkbox custom-control mb-2" for="login-input-remember-me">
<input
class="form-check-input"
type="checkbox"
id="login-input-remember-me"
formControlName="rememberMe"
/>
{{ 'AbpAccount::RememberMe' | abpLocalization }}
</label>
</div>
<div class="text-right col">
<a routerLink="/account/forgot-password">{{
'AbpAccount::ForgotPassword' | abpLocalization
}}</a>
</div>
</div>
<abp-button
[loading]="inProgress"
buttonType="submit"
name="Action"
buttonClass="btn-block btn-lg mt-3 btn btn-primary"
>
{{ 'AbpAccount::Login' | abpLocalization }}
</abp-button>
</form>

80
npm/ng-packs/packages/account/src/lib/components/login/login.component.ts

@ -0,0 +1,80 @@
import { ConfigStateService, AuthService } from '@abp/ng.core';
import { ToasterService } from '@abp/ng.theme.shared';
import { Component, Injector, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Store } from '@ngxs/store';
import { throwError } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import snq from 'snq';
import { eAccountComponents } from '../../enums/components';
import { getRedirectUrl } from '../../utils/auth-utils';
const { maxLength, required } = Validators;
@Component({
selector: 'abp-login',
templateUrl: './login.component.html',
})
export class LoginComponent implements OnInit {
form: FormGroup;
inProgress: boolean;
isSelfRegistrationEnabled = true;
authWrapperKey = eAccountComponents.AuthWrapper;
constructor(
protected injector: Injector,
protected fb: FormBuilder,
protected toasterService: ToasterService,
protected authService: AuthService,
protected configState: ConfigStateService,
) {}
ngOnInit() {
this.init();
this.buildForm();
}
protected init() {
this.isSelfRegistrationEnabled =
(
(this.configState.getSetting('Abp.Account.IsSelfRegistrationEnabled') as string) || ''
).toLowerCase() !== 'false';
}
protected buildForm() {
this.form = this.fb.group({
username: ['', [required, maxLength(255)]],
password: ['', [required, maxLength(128)]],
rememberMe: [false],
});
}
onSubmit() {
if (this.form.invalid) return;
this.inProgress = true;
const { username, password, rememberMe } = this.form.value;
const redirectUrl = getRedirectUrl(this.injector);
this.authService
.login({ username, password, rememberMe, redirectUrl })
.pipe(
catchError(err => {
this.toasterService.error(
snq(() => err.error.error_description) ||
snq(() => err.error.error.message, 'AbpAccount::DefaultErrorMessage'),
'Error',
{ life: 7000 },
);
return throwError(err);
}),
finalize(() => (this.inProgress = false)),
)
.subscribe();
}
}

62
npm/ng-packs/packages/account/src/lib/components/manage-profile/manage-profile.component.html

@ -0,0 +1,62 @@
<div id="AbpContentToolbar"></div>
<div class="card border-0 shadow-sm min-h-400" [abpLoading]="!(profile$ | async)">
<div class="card-body">
<div class="row">
<div class="col-12 col-md-3">
<ul class="nav flex-column nav-pills" id="nav-tab" role="tablist">
<li
*ngIf="!hideChangePasswordTab && (profile$ | async)"
class="nav-item"
(click)="selectedTab = 0"
>
<a
class="nav-link"
[ngClass]="{ active: selectedTab === 0 }"
role="tab"
href="javascript:void(0)"
>{{ 'AbpUi::ChangePassword' | abpLocalization }}</a
>
</li>
<li class="nav-item mb-2" (click)="selectedTab = 1">
<a
class="nav-link"
[ngClass]="{ active: selectedTab === 1 }"
role="tab"
href="javascript:void(0)"
>{{ 'AbpAccount::PersonalSettings' | abpLocalization }}</a
>
</li>
</ul>
</div>
<div *ngIf="profile$ | async" class="col-12 col-md-9">
<div class="tab-content" *ngIf="selectedTab === 0" [@fadeIn]>
<div class="tab-pane active" role="tabpanel">
<h4>
{{ 'AbpIdentity::ChangePassword' | abpLocalization }}
<hr />
</h4>
<abp-change-password-form
*abpReplaceableTemplate="{
componentKey: changePasswordKey
}"
></abp-change-password-form>
</div>
</div>
<div class="tab-content" *ngIf="selectedTab === 1" [@fadeIn]>
<div class="tab-pane active" role="tabpanel">
<h4>
{{ 'AbpIdentity::PersonalSettings' | abpLocalization }}
<hr />
</h4>
<abp-personal-settings-form
*abpReplaceableTemplate="{
componentKey: personalSettingsKey
}"
></abp-personal-settings-form>
</div>
</div>
</div>
</div>
</div>
</div>

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

Loading…
Cancel
Save