Merge branch 'dev' into auto-merge/rel-5-2/974

pull/12177/head
maliming 3 years ago committed by GitHub
commit 0e41609b1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -108,6 +108,10 @@ ABP is a community-driven open source project. See [the contribution guide](http
Love ABP Framework? **Please give a star** to this repository :star:
## Discord Channel
You can use this link to join the ABP Community Discord Server: https://discord.gg/abp
## ABP Commercial
See also [ABP Commercial](https://commercial.abp.io/) if you are looking for pre-built application modules, professional themes, code generation tooling and premium support for the ABP Framework.

@ -337,18 +337,6 @@
"Expired": "Expired",
"TrialLicenseDeletionWarningMessage": "Are you sure you want to delete the trial license? Trial license, organization, support accounts will be deleted!",
"LicenseCategoryFilter": "License category",
"Volo.AbpIo.Commercial:030000": "You already used your trial period.",
"Volo.AbpIo.Commercial:030001": "This organization name already exists.",
"Volo.AbpIo.Commercial:030002": "Once activated, trial license cannot be set to requested!",
"Volo.AbpIo.Commercial:030003": "There is no such status!",
"Volo.AbpIo.Commercial:030004": "Status could not be changed due to an unexpected error!",
"Volo.AbpIo.Commercial:030005": "Start and end date can be updated when the trial license is in the -activated- status!",
"Volo.AbpIo.Commercial:030006": "End date must always be greater than start date!",
"Volo.AbpIo.Commercial:030007": "This trial license has already been activated once!",
"Volo.AbpIo.Commercial:030008": "Purchase date can be set only when status is Purchased!",
"Volo.AbpIo.Commercial:030009": "User not found!",
"Volo.AbpIo.Commercial:030010": "To purchase the trial license, first you need to activate your trial license!",
"Volo.AbpIo.Commercial:030011": "You cannot delete a trial license when it is purchased!",
"Permission:SendWelcomeEmail": "Send Welcome Email",
"SendWelcomeEmail": "Send Welcome Email",
"SendWelcomeEmailWarningMessage": "Are you sure you want to send welcome email to the organization members?",
@ -385,12 +373,15 @@
"Menu:Speakers": "Speakers",
"ChooseSpeakerImage": "Choose a speaker image...",
"SpeakerImage": "Speaker image",
"AddSpeaker": "Add Speaker",
"ShowPurchaseItemsOfOrganizations": "Purchase Items",
"Enum:OrganizationPurchaseState:0": "Not delivered",
"Enum:OrganizationPurchaseState:1": "Delivered",
"PurchaseItems": "Purchase Items",
"SuccessfullyUpdated": "Successfully updated",
"SuccessfullyAdded": "Successfully added",
"PurchaseState": "Purchase State"
"PurchaseState": "Purchase State",
"ShowBetweenDayCount": "Show Between Days",
"PurchaseOrder": "Purchase Order"
}
}

@ -14,6 +14,18 @@
"Volo.AbpIo.Domain:020002": "Could not delete this NPM Package because \"{Modules}\" Modules are using this package.",
"Volo.AbpIo.Domain:020003": "Could not delete this NPM Package because \"{Modules}\" Modules are using this package and \"{NugetPackages}\" Nuget Packages are dependent to this package.",
"Volo.AbpIo.Domain:020004": "Could not delete this Nuget Package because \"{Modules}\" Modules are using this package.",
"Volo.AbpIo.Domain:030000": "You have already completed your trial period.",
"Volo.AbpIo.Domain:030001": "This organization name already exists.",
"Volo.AbpIo.Domain:030002": "Once activated, you cannot switch the trial license to -requested- status!",
"Volo.AbpIo.Domain:030003": "There is no such status!",
"Volo.AbpIo.Domain:030004": "Status could not be changed due to an unexpected error!",
"Volo.AbpIo.Domain:030005": "Start and end date can be updated when the trial license is in the -activated- status!",
"Volo.AbpIo.Domain:030006": "The end date must be greater than the start date!",
"Volo.AbpIo.Domain:030007": "This trial license has already been activated!",
"Volo.AbpIo.Domain:030008": "The purchase date can be set only when the status is -purchased-!",
"Volo.AbpIo.Domain:030009": "User not found!",
"Volo.AbpIo.Domain:030010": "To purchase the trial license, you first need to activate your trial license!",
"Volo.AbpIo.Domain:030011": "You cannot delete a trial license when it is purchased!",
"WantToLearn?": "Want to learn?",
"ReadyToGetStarted?": "Ready to get started?",
"JoinOurCommunity": "Join our community",
@ -72,7 +84,7 @@
"WouldLikeToReceiveMarketingMaterials": "I would like to receive marketing materials like product deals & special offers.",
"JoinOurMarketingNewsletter": "Join our marketing newsletter",
"CommunityPrivacyPolicyConfirmation": "I agree to the Terms & Conditions and <a class=\"text-white fw-6 text-decoration-underline opacity-50\" href=\"https://commercial.abp.io/Privacy\">Privacy Policy</a>.",
"ABPIO-Common": "ABPIO-Common",
"WouldLikeToReceiveNotification": "I would like to receive the latest news from abp.io websites.",
"CommercialNewsletterConfirmationMessage": "I agree to the <a class=\"text-white fw-6 text-decoration-underline opacity-50\" href=\"https://commercial.abp.io/TermsConditions\">Terms & Conditions</a> and <a class=\"text-white fw-6 text-decoration-underline opacity-50\" href=\"https://commercial.abp.io/Privacy\">Privacy Policy</a>.",
"FreeDDDEBook": "Free DDD E-Book",
"AdditionalServices": "Additional Services",
@ -104,7 +116,9 @@
"WatchTheEvent": "Watch the Event",
"RegisterNow": "Register Now",
"ThereIsNoEvent": "There is no event.",
"Events": "Events",
"Volo.AbpIo.Domain:080000": "There is already a purchase item named \"{Name}\"",
"MasteringAbpFrameworkBook": "Book: Mastering ABP Framework"
"MasteringAbpFrameworkBook": "Book: Mastering ABP Framework",
"ABPIO-CommonPreferenceDefinition": "Get latest news about ABP Platform like new posts, events and more."
}
}

@ -495,7 +495,9 @@
"LicenseTypeNotCorrect": "The license type is not correct!",
"Trainings": "Trainings",
"ChoseTrainingPlaceholder": "Chose the training...",
"ContactUsToGetQuote": "Contact us to get a quote",
"DoYouNeedTrainings": "Do you need one of these trainings?",
"DoYouNeedTraining": "Do you need {0} training?",
"GetInTouchUs": "Get in touch with us",
"ForMoreInformationClickHere": "For more information, click <a href='{0}'>here.</a>",
"IsGetOnboardingTraining": "Would you like to get onboarding & web application development training?",
"OnboardingWebApplicationDevelopmentTrainingMessage": "To schedule your training calendar, please contact {0} after creating the organization",
@ -503,6 +505,7 @@
"AdditionalNote": "Additional Note",
"OnboardingTrainingFaqTitle": "Do you have ABP onboarding training?",
"OnboardingTrainingFaqExplanation": "Yes, we have ABP Training Services to help you get your ABP project started fast. You will learn about ABP from an ABP core team member and you will get the skills to begin your ABP project. In the onboarding training, we will explain how to set up your development environment, install the required tools, create a fully functional CRUD page. The training will be live and the Zoom application will be used, and we are open to using other online meeting platforms. The language of the training will be English. You can also ask your questions about ABP during the sessions. A convenient time and date will be planned for both parties. To get more information, contact us at <a href=\"mailto:info@abp.io\">info@abp.io</a>.",
"AddBasket": "Add to Basket"
"AddBasket": "Add to Basket",
"SendTrainingRequest": "Send Training Request"
}
}

@ -318,6 +318,7 @@
"InstallingTheABPCLI": "Installing the ABP CLI",
"CreateYourProjectNow": "Create Your Project Now",
"OrderOn": "Order on {0}",
"DownloadFreeDDDBook": "Download Free DDD Book"
"DownloadFreeDDDBook": "Download Free DDD Book",
"WhatIsABPFramework": "What is the ABP Framework?"
}
}

@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Version>5.2.0</Version>
<Version>5.3.0</Version>
<NoWarn>$(NoWarn);CS1591;CS0436</NoWarn>
<PackageIconUrl>https://abp.io/assets/abp_nupkg.png</PackageIconUrl>
<PackageProjectUrl>https://abp.io/</PackageProjectUrl>

@ -126,4 +126,4 @@ context.ServiceProvider
So, it resolves the given background worker and adds to the `IBackgroundWorkerManager`.
While we generally add workers in OnApplicationInitialization, there are no restrictions on that. You can inject IBackgroundWorkerManager anywhere and add workers at runtime. Background worker manager will stop and release all the registered workers when your application is being shut down.
While we generally add workers in OnApplicationInitialization, there are no restrictions on that. You can inject IBackgroundWorkerManager anywhere and add workers at runtime. Background worker manager will stop and release all the registered workers when your application is being shut down.

@ -258,7 +258,7 @@ ABP's distributed cache interfaces provide methods to perform batch methods thos
Distributed cache service provides an interesting feature. Assume that you've updated the price of a book in the database, then set the new price to the cache, so you can use the cached value later. What if you have an exception after setting the cache and you **rollback the transaction** that updates the price of the book? In this case, cache value will be incorrect.
`IDistributedCache<..>` methods gets an optional parameter, named `considerOuw`, which is `false` by default. If you set it to `true`, then the changes you made for the cache are not actually applied to the real cache store, but associated with the current [unit of work](Unit-Of-Work.md). You get the value you set in the same unit of work, but the changes are applied **only if the current unit of work succeed**.
`IDistributedCache<..>` methods gets an optional parameter, named `considerUow`, which is `false` by default. If you set it to `true`, then the changes you made for the cache are not actually applied to the real cache store, but associated with the current [unit of work](Unit-Of-Work.md). You get the value you set in the same unit of work, but the changes are applied **only if the current unit of work succeed**.
### IDistributedCacheSerializer

@ -1,164 +1,180 @@
# 缓存
ABP框架扩展了ASP.NET Core的分布式缓存系统.
ABP框架扩展了 [ASP.NET Core的分布式缓存系统](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed).
## Volo.Abp.Caching Package
## 安装
> 默认情况下启动模板已经安装了这个包,所以大部分情况下你不需要手动安装.
> 默认情况下 [启动模板](Startup-Templates/Application.md) 已经安装了这个包. 所以大部分情况下你不需要手动安装.
Volo.Abp.Caching是缓存系统的核心包.使用包管理控制台(PMC)安装到项目:
[Volo.Abp.Caching](https://www.nuget.org/packages/Volo.Abp.Caching)是缓存系统的核心包. 可以使用 [ABP CLI](CLI.md) 的add-package命令将其安装到项目中
```
Install-Package Volo.Abp.Caching
abp add-package Volo.Abp.Caching
```
你需要在包含 `csproj` 文件的文件夹中的命令行终端上运行此命令(请参阅 [其他选项](https://abp.io/package-detail/Volo.Abp.Caching) 安装).
然后将 **AbpCachingModule** 依赖添加到你的模块:
## 使用方式
```c#
using Volo.Abp.Modularity;
using Volo.Abp.Caching;
namespace MyCompany.MyProject
{
[DependsOn(typeof(AbpCachingModule))]
public class MyModule : AbpModule
{
//...
}
}
```
## `IDistributedCache` 接口
### `IDistributedCache` 接口
ASP.NET Core 定义了 `IDistributedCache` 接口用于 get/set 缓存值 . 但是会有以下问题:
ASP.NET Core 定义了 `IDistributedCache` 接口用于 get/set 缓存值. 但是会有以下问题:
* 它适用于 **byte 数组** 而不是 .NET 对象. 因此你需要对缓存的对象进行**序列化/反序列化**.
* 它为所有的缓存项提供了 **单个 key 池** , 因此 ;
* 它为所有的缓存项提供了 **单个 key 池** , 因此;
* 你需要注意键区分 **不同类型的对象**.
* 你需要注意**不同租户**(参见[多租户](Multi-Tenancy.md))的缓存项.
> `IDistributedCache` 定义在 `Microsoft.Extensions.Caching.Abstractions` 包中. 这使它不仅适用于ASP.NET Core应用程序, 也可用于**任何类型的程序**.
> `IDistributedCache` 接口的默认实现是 `MemoryDistributedCache` 它使用**内存**工作. 参见 [ASP.NET Core文档](https://docs.microsoft.com/zh-cn/aspnet/core/performance/caching/distributed) 了解如何切换到 **Redis** 或其他缓存提供程序.
> `IDistributedCache` 接口的默认实现是 `MemoryDistributedCache` 它使用**内存**工作. 参见 [ASP.NET Core文档](https://docs.microsoft.com/zh-cn/aspnet/core/performance/caching/distributed) 了解如何切换到 **Redis** 或其他缓存提供程序. 此外, 如果要将Redis用作分布式缓存服务器, [Redis缓存](Redis-Cache.md) 文档.
有关更多信息, 参见 [ASP.NET Core 分布式缓存文档](https://docs.microsoft.com/zh-cn/aspnet/core/performance/caching/distributed).
## `IDistributedCache<TCacheItem>` 接口
### `IDistributedCache<TCacheItem>` 接口
ABP框架在[Volo.Abp.Caching](https://www.nuget.org/packages/Volo.Abp.Caching/)包定义了通用的泛型 `IDistributedCache<TCacheItem>` 接口. `TCacheItem` 是存储在缓存中的对象类型.
`IDistributedCache<TCacheItem>` 接口了上述中的问题;
`IDistributedCache<TCacheItem>` 接口解决了上述中的问题;
* 它在内部 **序列化/反序列化** 缓存对象. 默认使用 **JSON** 序列化, 但可以替换[依赖注入](Dependency-Injection.md)系统中 `IDistributedCacheSerializer` 服务的实现来覆盖默认的处理.
* 它根据缓存中对象类型自动向缓存key添加 **缓存名称** 前缀. 默认缓存名是缓存对象类的全名(如果你的类名以`CacheItem` 结尾, 那么`CacheItem` 会被忽略,不应用到缓存名称上). 你也可以在缓存类上使用 `CacheName` 设置换缓存的名称.
* 它自动将当前的**租户id**添加到缓存键中, 以区分不同租户的缓存项 (只有在你的应用程序是[多租户](Multi-Tenancy.md)的情况下生效). 在缓存类上应用 `IgnoreMultiTenancy` attribute, 可以在所有的租户间共享缓存.
* 允许为每个应用程序定义 **全局缓存键前缀** ,不同的应用程序可以在共享的分布式缓存中拥有自己的隔离池.
* 它根据缓存中对象类型自动向缓存key添加 **缓存名称** 前缀. 默认缓存名是缓存对象类的全名(如果你的类名以`CacheItem` 结尾, 那么`CacheItem` 会被忽略,不应用到缓存名称上). 你也可以在缓存类上使用 **`CacheName` 特性** 设置缓存的名称.
* 它自动将**当前的租户id**添加到缓存键中, 以区分不同租户的缓存项 (只有在你的应用程序是[多租户](Multi-Tenancy.md)的情况下生效). 如果要在多租户应用程序中的所有租户之间共享缓存对象, 请在缓存项类上定义`IgnoreMultiTenancy`特性以禁用此选项.
* 允许为每个应用程序定义 **全局缓存键前缀** , 不同的应用程序可以在共享的分布式缓存中拥有自己的隔离池.
* 它可以在任何可能绕过缓存的情况下 **容忍错误** 的发生. 这在缓存服务器出现临时问题时非常有用.
* 它有 `GetManyAsync``SetManyAsync` 等方法, 可以显著提高**批处理**的性能.
### 使用方式
缓存中存储项的示例类:
**示例: 在缓存中存储图书名称和价格**
````csharp
public class BookCacheItem
namespace MyProject
{
public string Name { get; set; }
public class BookCacheItem
{
public string Name { get; set; }
public float Price { get; set; }
public float Price { get; set; }
}
}
````
你可以注入 `IDistributedCache<BookCacheItem>` 服务用于 get/set `BookCacheItem` 对象.
使用示例:
````csharp
public class BookService : ITransientDependency
{
private readonly IDistributedCache<BookCacheItem> _cache;
public BookService(IDistributedCache<BookCacheItem> cache)
{
_cache = cache;
}
public async Task<BookCacheItem> GetAsync(Guid bookId)
{
return await _cache.GetOrAddAsync(
bookId.ToString(), //Cache key
async () => await GetBookFromDatabaseAsync(bookId),
() => new DistributedCacheEntryOptions
{
AbsoluteExpiration = DateTimeOffset.Now.AddHours(1)
}
);
}
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
private Task<BookCacheItem> GetBookFromDatabaseAsync(Guid bookId)
namespace MyProject
{
public class BookService : ITransientDependency
{
//TODO: get from database
private readonly IDistributedCache<BookCacheItem> _cache;
public BookService(IDistributedCache<BookCacheItem> cache)
{
_cache = cache;
}
public async Task<BookCacheItem> GetAsync(Guid bookId)
{
return await _cache.GetOrAddAsync(
bookId.ToString(), //缓存键
async () => await GetBookFromDatabaseAsync(bookId),
() => new DistributedCacheEntryOptions
{
AbsoluteExpiration = DateTimeOffset.Now.AddHours(1)
}
);
}
private Task<BookCacheItem> GetBookFromDatabaseAsync(Guid bookId)
{
//TODO: 从数据库获取数据
}
}
}
````
* 示例服务代码中的 `GetOrAddAsync()` 方法从缓存中获取图书项.
* 示例服务代码中的 `GetOrAddAsync()` 方法从缓存中获取图书项. `GetOrAddAsync`是ABP框架在 ASP.NET Core 分布式缓存方法中添增的附加方法.
* 如果没有在缓存中找到图书,它会调用工厂方法 (本示例中是 `GetBookFromDatabaseAsync`)从原始数据源中获取图书项.
* `GetOrAddAsync` 有一个可选参数 `DistributedCacheEntryOptions` , 可用于设置缓存的生命周期.
`IDistributedCache<BookCacheItem>` 的其他方法与ASP.NET Core的`IDistributedCache` 接口相同, 你可以参考 [ASP.NET Core文档](https://docs.microsoft.com/zh-cn/aspnet/core/performance/caching/distributed).
`IDistributedCache<BookCacheItem>` 与ASP.NET Core的`IDistributedCache` 接口拥有相同的方法, 你可以参考 [ASP.NET Core文档](https://docs.microsoft.com/zh-cn/aspnet/core/performance/caching/distributed).
## `IDistributedCache<TCacheItem, TCacheKey>` 接口
### `IDistributedCache<TCacheItem, TCacheKey>` 接口
`IDistributedCache<TCacheItem>` 接口默认了键是 `string` 类型 (如果你的键不是string类型需要进行手动类型转换). `IDistributedCache<TCacheItem, TCacheKey>` 将键的类型泛型化试图简化手动转换的操作.
`IDistributedCache<TCacheItem>` 接口默认了**缓存**`string` 类型 (如果你的键不是string类型需要进行手动类型转换). 但当缓存键的类型不是`string`时, 可以使用`IDistributedCache<TCacheItem, TCacheKey>`.
### 使用示例
**示例: 在缓存中存储图书名称和价格**
示例缓存项
````csharp
public class BookCacheItem
using Volo.Abp.Caching;
namespace MyProject
{
public string Name { get; set; }
[CacheName("Books")]
public class BookCacheItem
{
public string Name { get; set; }
public float Price { get; set; }
public float Price { get; set; }
}
}
````
用法示例 (这里假设你的键类型是 `Guid`):
* 在本例中使用`CacheName`特性给`BookCacheItem`类设置缓存名称.
````csharp
public class BookService : ITransientDependency
{
private readonly IDistributedCache<BookCacheItem, Guid> _cache;
你可以注入 `IDistributedCache<BookCacheItem, Guid>` 服务用于 get/set `BookCacheItem` 对象.
public BookService(IDistributedCache<BookCacheItem, Guid> cache)
{
_cache = cache;
}
````csharp
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
public async Task<BookCacheItem> GetAsync(Guid bookId)
{
return await _cache.GetOrAddAsync(
bookId, //Guid type used as the cache key
async () => await GetBookFromDatabaseAsync(bookId),
() => new DistributedCacheEntryOptions
{
AbsoluteExpiration = DateTimeOffset.Now.AddHours(1)
}
);
}
private Task<BookCacheItem> GetBookFromDatabaseAsync(Guid bookId)
namespace MyProject
{
public class BookService : ITransientDependency
{
//TODO: get from database
private readonly IDistributedCache<BookCacheItem, Guid> _cache;
public BookService(IDistributedCache<BookCacheItem, Guid> cache)
{
_cache = cache;
}
public async Task<BookCacheItem> GetAsync(Guid bookId)
{
return await _cache.GetOrAddAsync(
bookId, //Guid类型作为缓存键
async () => await GetBookFromDatabaseAsync(bookId),
() => new DistributedCacheEntryOptions
{
AbsoluteExpiration = DateTimeOffset.Now.AddHours(1)
}
);
}
private Task<BookCacheItem> GetBookFromDatabaseAsync(Guid bookId)
{
//TODO: 从数据库获取数据
}
}
}
````
* 示例服务中 `GetOrAddAsync()` 方法获取缓存的图书项.
* 我们采用了 `Guid` 做为键,在 `_cache_GetOrAddAsync()` 方法中传入 `Guid` 类型的bookid.
* 我们采用了 `Guid` 做为键, 在 `_cache_GetOrAddAsync()` 方法中传入 `Guid` 类型的bookid.
#### 复杂类型的缓存键
`IDistributedCache<TCacheItem, TCacheKey>` 在内部使用键对象的 `ToString()` 方法转换类型为string. 如果你的将复杂对象做为键,那么需要重写类的 `ToString` 方法.
`IDistributedCache<TCacheItem, TCacheKey>` 在内部使用键对象的 `ToString()` 方法转换类型为string. 如果你的将复杂对象做为缓存键,那么需要重写类的 `ToString` 方法.
示例:
举例一个作为缓存键的类:
````csharp
public class UserInOrganizationCacheKey
@ -187,23 +203,72 @@ public class BookService : ITransientDependency
{
_cache = cache;
}
...
}
````
## 配置
### AbpDistributedCacheOptions
`AbpDistributedCacheOptions` 是配置缓存的主要[Option类](Options.md).
**示例:为应用程序设置缓存键前缀**
```csharp
Configure<AbpDistributedCacheOptions>(options =>
{
options.KeyPrefix = "MyApp1";
});
```
> 在[模块类](Module-Development-Basics.md)的`ConfigureServices`方法中添加代码.
#### 可用选项
* `HideErrors` (`bool`, 默认: `true`): 启用/禁用隐藏从缓存服务器写入/读取值时的错误.
* `KeyPrefix` (`string`, 默认: `null`): 如果你的缓存服务器由多个应用程序共同使用, 则可以为应用程序的缓存键设置一个前缀. 在这种情况下, 不同的应用程序不能覆盖彼此的缓存内容.
* `GlobalCacheEntryOptions` (`DistributedCacheEntryOptions`): 用于设置保存缓内容却没有指定选项时, 默认的分布式缓存选项 (例如 `AbsoluteExpiration``SlidingExpiration`). `SlidingExpiration`的默认值设置为20分钟.
## 错误处理
当为你的对象设计缓存时, 通常会首先尝试从缓存中获取值. 如果在缓存中找不到该值, 则从**来源**查询对象. 它可能在**数据库**中, 或者可能需要通过HTTP调用远程服务器.
在大多数情况下, 你希望**容忍缓存错误**; 如果缓存服务器出现错误, 也不希望取消该操作. 相反, 你可以默默地隐藏(并记录)错误并**从来源查询**. 这就是ABP框架默认的功能.
ABP的分布式缓存 [异常处理](Exception-Handling.md), 默认记录并隐藏错误. 有一个全局修改该功能的选项(参见下面的选项内容).
所有的`IDistributedCache<TCacheItem>` (和 `IDistributedCache<TCacheItem, TCacheKey>`)方法都有一个可选的参数`hideErrors`, 默认值为`null`. 如果此参数设置为`null`, 则全局生效, 否则你可以选择单个方法调用时隐藏或者抛出异常.
## 批量操作
ABP的分布式缓存接口定义了以下批量操作方法,当你需要在一个方法中调用多次缓存操作时,这些方法可以提高性能
* `SetManyAsync``SetMany` 方法可以用来设置多个值.
* `SetManyAsync``SetMany` 方法可以用来向缓存中设置多个值.
* `GetManyAsync``GetMany` 方法可以用来从缓存中获取多个值.
* `GetOrAddManyAsync``GetOrAddMany` 方法可以用来从缓存中获取并添加缺少的值.
* `RefreshManyAsync``RefreshMany` 方法可以来用重置多个值的滚动过期时间.
* `RemoveManyAsync``RemoveMany` 方法呆以用来删除多个值.
* `RemoveManyAsync``RemoveMany` 方法可以用来从缓存中删除多个值.
> 这些不是标准的ASP.NET Core缓存方法, 所以某些提供程序可能不支持. [ABP Redis集成包](Redis-Cache.md)实现了它们. 如果提供程序不支持,会回退到 `SetAsync``GetAsync` ... 方法(循环调用).
### DistributedCacheOptions
## 高级主题
### 工作单元级别的缓存
分布式缓存服务提供了一个有趣的功能. 假设你已经更新了数据库中某本书的价格, 然后将新价格设置到缓存中, 以便以后使用缓存的值. 如果设置缓存后出现异常, 并且更新图书价格的**事务被回滚了**, 该怎么办?在这种情况下, 缓存值是错误的.
`IDistributedCache<..>`方法提供一个可选参数, `considerUow`, 默认为`false`. 如果将其设置为`true`, 则你对缓存所做的更改不会应用于真正的缓存存储, 而是与当前的[工作单元](Unit-Of-Work.md)关联. 你将获得在同一工作单元中设置的缓存值, 但**仅当前工作单元成功时**更改才会生效.
### IDistributedCacheSerializer
`IDistributedCacheSerializer`服务用于序列化和反序列化缓存内容. 默认实现是`Utf8JsonDistributedCacheSerializer`类, 它使用`IJsonSerializer`服务将对象转换为[JSON](Json-Serialization.md), 反之亦然. 然后, 它使用UTC8编码将JSON字符串转换为分布式缓存接受的字节数组.
如果你想实现自己的序列化逻辑, 可以自己实现并[替换](Dependency-Injection.md) 此服务.
### IDistributedCacheKeyNormalizer
默认情况下, `IDistributedCacheKeyNormalizer`是由`DistributedCacheKeyNormalizer`类实现的. 它将缓存名称、应用程序缓存前缀和当前租户id添加到缓存键中. 如果需要更高级的键规范化, 可以自己实现并[替换](Dependency-Injection.md)此服务.
## 另请参阅
TODO
* [Redis 缓存](Redis-Cache.md)

@ -1,10 +1,10 @@
# 分布式事件总线Kafka集成
> 本文解释了**如何配置[Kafka](https://kafka.apache.org/)**做为分布式总线提供程序. 参阅[分布式事件总线文档](Distributed-Event-Bus.md)了解如何使用分布式事件总线系统.
> 本文解释了 **如何配置[Kafka](https://kafka.apache.org/)** 做为分布式总线提供程序. 参阅[分布式事件总线文档](Distributed-Event-Bus.md)了解如何使用分布式事件总线系统.
## 安装
使用ABP CLI添加[Volo.Abp.EventBus.Kafka[Volo.Abp.EventBus.Kafka](https://www.nuget.org/packages/Volo.Abp.EventBus.Kafka)NuGet包到你的项目:
使用ABP CLI添加[Volo.Abp.EventBus.Kafka](https://www.nuget.org/packages/Volo.Abp.EventBus.Kafka)NuGet包到你的项目:
* 安装[ABP CLI](https://docs.abp.io/en/abp/latest/CLI),如果你还没有安装.
* 在你想要安装 `Volo.Abp.EventBus.Kafka` 包的 `.csproj` 文件目录打开命令行(终端).
@ -18,7 +18,7 @@
### `appsettings.json` 文件配置
这是配置Kafka设置最简单的方法. 它也非常强大,因为你可以使用[由AspNet Core支持](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/)的任何其他配置源(如环境变量).
这是配置Kafka设置最简单的方法. 它也非常强大,因为你可以使用[由AspNet Core支持](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/)的任何其他配置源(如环境变量).
**示例最小化配置与默认配置连接到本地的Kafka服务器**
@ -160,4 +160,4 @@ Configure<AbpKafkaOptions>(options =>
});
````
使用这些选项类可以与 `appsettings.json` 组合在一起. 在代码中配置选项属性会覆盖配置文件中的值.
使用这些选项类可以与 `appsettings.json` 组合在一起. 在代码中配置选项属性会覆盖配置文件中的值.

@ -1,10 +1,10 @@
# 分布式事件总线RabbitMQ集成
> 本文解释了**如何配置[RabbitMQ](https://www.rabbitmq.com/)**做为分布式总线提供程序. 参阅[分布式事件总线文档](Distributed-Event-Bus.md)了解如何使用分布式事件总线系统.
> 本文解释了 **如何配置[RabbitMQ](https://www.rabbitmq.com/)** 做为分布式总线提供程序. 参阅[分布式事件总线文档](Distributed-Event-Bus.md)了解如何使用分布式事件总线系统.
## 安装
使用ABP CLI添加[Volo.Abp.EventBus.RabbitMQ[Volo.Abp.EventBus.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.EventBus.RabbitMQ)NuGet包到你的项目:
使用ABP CLI添加[Volo.Abp.EventBus.RabbitMQ](https://www.nuget.org/packages/Volo.Abp.EventBus.RabbitMQ)NuGet包到你的项目:
* 安装[ABP CLI](https://docs.abp.io/en/abp/latest/CLI),如果你还没有安装.
* 在你想要安装 `Volo.Abp.EventBus.RabbitMQ` 包的 `.csproj` 文件目录打开命令行(终端).
@ -18,7 +18,7 @@
### `appsettings.json` 文件配置
这是配置RabbitMQ设置最简单的方法. 它也非常强大,因为你可以使用[由AspNet Core支持](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/)的任何其他配置源(如环境变量).
这是配置RabbitMQ设置最简单的方法. 它也非常强大,因为你可以使用[由AspNet Core支持](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/)的任何其他配置源(如环境变量).
**示例最小化配置与默认配置连接到本地的RabbitMQ服务器**
@ -151,4 +151,4 @@ Configure<AbpRabbitMqEventBusOptions>(options =>
});
````
使用这些选项类可以与 `appsettings.json` 组合在一起. 在代码中配置选项属性会覆盖配置文件中的值.
使用这些选项类可以与 `appsettings.json` 组合在一起. 在代码中配置选项属性会覆盖配置文件中的值.

@ -1,10 +1,10 @@
# 分布式事件总线Rebus集成
> 本文解释了**如何配置[Rebus](http://mookid.dk/category/rebus/)**做为分布式总线提供程序. 参阅[分布式事件总线文档](Distributed-Event-Bus.md)了解如何使用分布式事件总线系统.
> 本文解释了 **如何配置[Rebus](http://mookid.dk/category/rebus/)** 做为分布式总线提供程序. 参阅[分布式事件总线文档](Distributed-Event-Bus.md)了解如何使用分布式事件总线系统.
## 安装
使用ABP CLI添加[Volo.Abp.EventBus.Rebus[Volo.Abp.EventBus.Rebus](https://www.nuget.org/packages/Volo.Abp.EventBus.Rebus)NuGet包到你的项目:
使用ABP CLI添加[Volo.Abp.EventBus.Rebus](https://www.nuget.org/packages/Volo.Abp.EventBus.Rebus)NuGet包到你的项目:
* 安装[ABP CLI](https://docs.abp.io/en/abp/latest/CLI),如果你还没有安装.
* 在你想要安装 `Volo.Abp.EventBus.Rebus` 包的 `.csproj` 文件目录打开命令行(终端).

@ -264,7 +264,7 @@ Configure<AbpDistributedEntityEventOptions>(options =>
因此可以实现 `IDistributedEventHandler<EntityUpdatedEto<EntityEto>>` 订阅事件. 但是订阅这样的通用事件不是一个好方法,你可以为实体类型定义对应的ETO.
**示例: 为 `Product` 声明使用 `ProductDto`**
**示例: 为 `Product` 声明使用 `ProductEto`**
````csharp
Configure<AbpDistributedEntityEventOptions>(options =>

@ -1,3 +1,128 @@
# ABP Documentation
# 领域服务
待添加
## 介绍
在 [领域驱动设计](Domain-Driven-Design.md) (DDD) 解决方案中,核心业务逻辑通常在聚合 ([实体](Entities.md)) 和领域服务中实现. 在以下情况下特别需要创建领域服务
* 你实现了依赖于某些服务(如存储库或其他外部服务)的核心域逻辑.
* 你需要实现的逻辑与多个聚合/实体相关,因此它不适合任何聚合.
## ABP 领域服务基础设施
领域服务是简单的无状态类. 虽然你不必从任何服务或接口派生,但 ABP 框架提供了一些有用的基类和约定.
### DomainService 和 IDomainService
`DomainService` 基类派生领域服务或直接实现 `IDomainService` 接口.
**示例: 创建从 `DomainService` 基类派生的领域服务.**
````csharp
using Volo.Abp.Domain.Services;
namespace MyProject.Issues
{
public class IssueManager : DomainService
{
}
}
````
当你这样做时:
* ABP 框架自动将类注册为瞬态生命周期到依赖注入系统.
* 你可以直接使用一些常用服务作为基础属性,而无需手动注入 (例如 [ILogger](Logging.md) and [IGuidGenerator](Guid-Generation.md)).
> 建议使用 `Manager``Service` 后缀命名领域服务. 我们通常使用如上面示例中的 `Manager` 后缀.
**示例: 实现将问题分配给用户的领域逻辑**
````csharp
public class IssueManager : DomainService
{
private readonly IRepository<Issue, Guid> _issueRepository;
public IssueManager(IRepository<Issue, Guid> issueRepository)
{
_issueRepository = issueRepository;
}
public async Task AssignAsync(Issue issue, AppUser user)
{
var currentIssueCount = await _issueRepository
.CountAsync(i => i.AssignedUserId == user.Id);
//Implementing a core business validation
if (currentIssueCount >= 3)
{
throw new IssueAssignmentException(user.UserName);
}
issue.AssignedUserId = user.Id;
}
}
````
问题是定义如下所示的 [聚合根](Entities.md):
````csharp
public class Issue : AggregateRoot<Guid>
{
public Guid? AssignedUserId { get; internal set; }
//...
}
````
* 使用 `internal` 的 set 确保外层调用者不能直接在调用 set ,并强制始终使用 `IssueManager``User` 分配 `Issue`.
### 使用领域服务
领域服务通常用于 [应用程序服务](Application-Services.md).
**示例: 使用 `IssueManager` 将问题分配给用户**
````csharp
using System;
using System.Threading.Tasks;
using MyProject.Users;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace MyProject.Issues
{
public class IssueAppService : ApplicationService, IIssueAppService
{
private readonly IssueManager _issueManager;
private readonly IRepository<AppUser, Guid> _userRepository;
private readonly IRepository<Issue, Guid> _issueRepository;
public IssueAppService(
IssueManager issueManager,
IRepository<AppUser, Guid> userRepository,
IRepository<Issue, Guid> issueRepository)
{
_issueManager = issueManager;
_userRepository = userRepository;
_issueRepository = issueRepository;
}
public async Task AssignAsync(Guid id, Guid userId)
{
var issue = await _issueRepository.GetAsync(id);
var user = await _userRepository.GetAsync(userId);
await _issueManager.AssignAsync(issue, user);
await _issueRepository.UpdateAsync(issue);
}
}
}
````
由于 `IssueAppService` 在应用层, 它不能直接将问题分配给用户.因此,它使用 `IssueManager`.
## 应用程序服务与领域服务
虽然应用服务和领域服务都实现了业务规则,但存在根本的逻辑和形式差异;
虽然 [应用服务](Application-Services.md) 和领域服务都实现了业务规则,但存在根本的逻辑和形式差异:
* 应用程序服务实现应用程序的 **用例** (典型 Web 应用程序中的用户交互), 而领域服务实现 **核心的、用例独立的领域逻辑**.
* 应用程序服务获取/返回 [数据传输对象](Data-Transfer-Objects.md), 领域服务方法通常获取和返回 **领域对象** ([实体](Entities.md), [值对象](Value-Objects.md)).
* 领域服务通常由应用程序服务或其他领域服务使用,而应用程序服务由表示层或客户端应用程序使用.
## 生命周期
领域服务的生命周期是 [瞬态](https://docs.abp.io/en/abp/latest/Dependency-Injection) 的,它们会自动注册到依赖注入服务.

@ -1,3 +1,55 @@
# Background Jobs Module
# 后台作业模块
待添加
后台作业模块实现了 `IBackgroundJobStore` 接口,并且可以使用ABP框架的默认后台作业管理.如果你不想使用这个模块,那么你需要自己实现 `IBackgroundJobStore` 接口.
> 本文档仅介绍后台作业模块,该模块将后台作业持久化到数据库.有关后台作业系统的更多信息,请参阅[后台作业](../Background-Jobs.md)文档.
## 如何使用
当你使用ABP框架[创建一个新的解决方案](https://abp.io/get-started)时,这个模块是作为NuGet/NPM包预先安装的.你可以继续将其作为软件包使用并轻松获取更新,也可以将其源代码包含到解决方案中(请参阅 `get-source` [CLI](../CLI.md)命令)以开发自定义模块.
### 源代码
此模块的源代码可在[此处](https://github.com/abpframework/abp/tree/dev/modules/background-jobs)访问.源代码是由[MIT](https://choosealicense.com/licenses/mit/)授权的,所以你可以自由使用和定制它.
## 内部结构
### 领域层
#### 聚合
- `BackgroundJobRecord` (聚合根): 表示后台工作记录.
#### 仓储
为该模块定义了以下自定义仓储:
- `IBackgroundJobRepository`
### 数据库提供程序
#### 通用
##### 表/集合的前缀与架构
默认情况下,所有表/集合都使用 `Abp` 前缀.如果需要更改表前缀或设置架构名称(如果数据库提供程序支持),请在 `BackgroundJobsDbProperties` 类上设置静态属性.
##### 连接字符串
此模块使用 `AbpBackgroundJobs` 作为连接字符串名称.如果不使用此名称定义连接字符串,它将返回 `Default` 连接字符串.有关详细信息,请参阅[连接字符串](https://docs.abp.io/en/abp/latest/Connection-Strings)文档.
#### Entity Framework Core
##### 表
- **AbpBackgroundJobs**
#### MongoDB
##### 集合
- **AbpBackgroundJobs**
## 另请参阅
* [后台作业系统](../Background-Jobs.md)

@ -1,3 +1,257 @@
## 规约
TODO..
规约模式用于为实体和其他业务对象定义 **命名、可复用、可组合和可测试的过滤器** .
> 规约是领域层的一部分.
## 安装
> 这个包 **已经安装** 在启动模板中.所以,大多数时候你不需要手动去安装.
添加 [Volo.Abp.Specifications](https://abp.io/package-detail/Volo.Abp.Specifications) 包到你的项目. 如果当前文件夹是你的项目的根目录(`.csproj`)时,你可以在命令行终端中使用 [ABP CLI](CLI.md) *add package* 命令:
````bash
abp add-package Volo.Abp.Specifications
````
## 定义规约
假设你定义了如下的顾客实体:
````csharp
using System;
using Volo.Abp.Domain.Entities;
namespace MyProject
{
public class Customer : AggregateRoot<Guid>
{
public string Name { get; set; }
public byte Age { get; set; }
public long Balance { get; set; }
public string Location { get; set; }
}
}
````
你可以创建一个由 `Specification<Customer>` 派生的新规约类.
**例如:规定选择一个18岁以上的顾客**
````csharp
using System;
using System.Linq.Expressions;
using Volo.Abp.Specifications;
namespace MyProject
{
public class Age18PlusCustomerSpecification : Specification<Customer>
{
public override Expression<Func<Customer, bool>> ToExpression()
{
return c => c.Age >= 18;
}
}
}
````
你只需通过定义一个lambda[表达式](https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/lambda-expressions)来定义规约.
> 你也可以直接实现`ISpecification<T>`接口,但是基类`Specification<T>`做了大量简化.
## 使用规约
这里有两种常见的规约用例.
### IsSatisfiedBy
`IsSatisfiedBy` 方法可以用于检查单个对象是否满足规约.
**例如:如果顾客不满足年龄规定,则抛出异常**
````csharp
using System;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
namespace MyProject
{
public class CustomerService : ITransientDependency
{
public async Task BuyAlcohol(Customer customer)
{
if (!new Age18PlusCustomerSpecification().IsSatisfiedBy(customer))
{
throw new Exception(
"这位顾客不满足年龄规定!"
);
}
//TODO...
}
}
}
````
### ToExpression & Repositories
`ToExpression()` 方法可用于将规约转化为表达式.通过这种方式,你可以使用规约在**数据库查询时过滤实体**.
````csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Services;
namespace MyProject
{
public class CustomerManager : DomainService, ITransientDependency
{
private readonly IRepository<Customer, Guid> _customerRepository;
public CustomerManager(IRepository<Customer, Guid> customerRepository)
{
_customerRepository = customerRepository;
}
public async Task<List<Customer>> GetCustomersCanBuyAlcohol()
{
var queryable = await _customerRepository.GetQueryableAsync();
var query = queryable.Where(
new Age18PlusCustomerSpecification().ToExpression()
);
return await AsyncExecuter.ToListAsync(query);
}
}
}
````
> 规约被正确地转换为SQL/数据库查询语句,并且在DBMS端高效执行.虽然它与规约无关,但如果你想了解有关 `AsyncExecuter` 的更多信息,请参阅[仓储](Repositories.md)文档.
实际上,没有必要使用 `ToExpression()` 方法,因为规约会自动转换为表达式.这也会起作用:
````csharp
var queryable = await _customerRepository.GetQueryableAsync();
var query = queryable.Where(
new Age18PlusCustomerSpecification()
);
````
## 编写规约
规约有一个强大的功能是,它们可以与`And`、`Or`、`Not`以及`AndNot`扩展方法组合使用.
假设你有另一个规约,定义如下:
```csharp
using System;
using System.Linq.Expressions;
using Volo.Abp.Specifications;
namespace MyProject
{
public class PremiumCustomerSpecification : Specification<Customer>
{
public override Expression<Func<Customer, bool>> ToExpression()
{
return (customer) => (customer.Balance >= 100000);
}
}
}
```
你可以将 `PremiumCustomerSpecification``Age18PlusCustomerSpecification` 结合起来,查询优质成人顾客的数量,如下所示:
````csharp
using System;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Services;
using Volo.Abp.Specifications;
namespace MyProject
{
public class CustomerManager : DomainService, ITransientDependency
{
private readonly IRepository<Customer, Guid> _customerRepository;
public CustomerManager(IRepository<Customer, Guid> customerRepository)
{
_customerRepository = customerRepository;
}
public async Task<int> GetAdultPremiumCustomerCountAsync()
{
return await _customerRepository.CountAsync(
new Age18PlusCustomerSpecification()
.And(new PremiumCustomerSpecification()).ToExpression()
);
}
}
}
````
如果你想让这个组合成为一个可复用的规约,你可以创建这样一个组合的规约类,它派生自`AndSpecification`:
````csharp
using Volo.Abp.Specifications;
namespace MyProject
{
public class AdultPremiumCustomerSpecification : AndSpecification<Customer>
{
public AdultPremiumCustomerSpecification()
: base(new Age18PlusCustomerSpecification(),
new PremiumCustomerSpecification())
{
}
}
}
````
现在,你就可以向下面一样重新编写 `GetAdultPremiumCustomerCountAsync` 方法:
````csharp
public async Task<int> GetAdultPremiumCustomerCountAsync()
{
return await _customerRepository.CountAsync(
new AdultPremiumCustomerSpecification()
);
}
````
> 你可以从这些例子中看到规约的强大之处.如果你之后想要更改 `PremiumCustomerSpecification` ,比如将余额从 `100.000` 修改为 `200.000` ,所有查询语句和合并的规约都将受到本次更改的影响.这是减少代码重复的好方法!
## 讨论
虽然规约模式通常与C#的lambda表达式相比较,算是一种更老的方式.一些开发人员可能认为不再需要它,我们可以直接将表达式传入到仓储或领域服务中,如下所示:
````csharp
var count = await _customerRepository.CountAsync(c => c.Balance > 100000 && c.Age => 18);
````
自从ABP的[仓储](Repositories.md)支持表达式,这是一个完全有效的用法.你不必在应用程序中定义或使用任何规约,可以直接使用表达式.
所以,规约的意义是什么?为什么或者应该在什么时候考虑去使用它?
### 何时使用?
使用规约的一些好处:
- **可复用**:假设你在代码库的许多地方都需要用到优质顾客过滤器.如果使用表达式而不创建规约,那么如果以后更改“优质顾客”的定义会发生什么?假设你想将最低余额从100000美元更改为250000美元,并添加另一个条件,成为顾客超过3年.如果使用了规约,只需修改一个类.如果在任何其他地方重复(复制/粘贴)相同的表达式,则需要更改所有的表达式.
- **可组合**:可以组合多个规约来创建新规约.这是另一种可复用性.
- **命名**:`PremiumCustomerSpecification` 更好地解释了为什么使用规约,而不是复杂的表达式.因此,如果在你的业务中使用了一个有意义的表达式,请考虑使用规约.
- **可测试**:规约是一个单独(且易于)测试的对象.
### 什么时侯不要使用?
- **没有业务含义的表达式**:不要对与业务无关的表达式和操作使用规约.
- **报表**:如果只是创建报表,不要创建规约,而是直接使用 `IQueryable` 和LINQ表达式.你甚至可以使用普通SQL、视图或其他工具生成报表.DDD不关心报表,因此从性能角度来看,查询底层数据存储的方式可能很重要.

@ -13,6 +13,20 @@ public static class AbpClaimActionCollectionExtensions
claimActions.DeleteClaim("name");
claimActions.RemoveDuplicate(AbpClaimTypes.UserName);
}
if (AbpClaimTypes.Name != "given_name")
{
claimActions.MapJsonKey(AbpClaimTypes.Name, "given_name");
claimActions.DeleteClaim("given_name");
claimActions.RemoveDuplicate(AbpClaimTypes.Name);
}
if (AbpClaimTypes.SurName != "family_name")
{
claimActions.MapJsonKey(AbpClaimTypes.SurName, "family_name");
claimActions.DeleteClaim("family_name");
claimActions.RemoveDuplicate(AbpClaimTypes.SurName);
}
if (AbpClaimTypes.Email != "email")
{

@ -57,8 +57,7 @@ public static class AbpOpenIdConnectExtensions
if (receivedContext.Request.Cookies.ContainsKey(tenantKey))
{
receivedContext.TokenEndpointRequest.SetParameter(tenantKey,
receivedContext.Request.Cookies[tenantKey]);
receivedContext.TokenEndpointRequest?.SetParameter(tenantKey, receivedContext.Request.Cookies[tenantKey]);
}
}
}

@ -7,7 +7,7 @@
@if(Options.Value.RenderPageTitle)
{
<Column ColumnSize="ColumnSize.IsAuto">
<h1 class="content-header-title">@PageLayout.Title</h1>
<h5 class="content-header-title">@PageLayout.Title</h5>
</Column>
}

@ -17,6 +17,8 @@ public class ApplicationConfigurationDto
public ApplicationFeatureConfigurationDto Features { get; set; }
public ApplicationGlobalFeatureConfigurationDto GlobalFeatures { get; set; }
public MultiTenancyInfoDto MultiTenancy { get; set; }
public CurrentTenantDto CurrentTenant { get; set; }

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
[Serializable]
public class ApplicationGlobalFeatureConfigurationDto
{
public HashSet<string> EnabledFeatures { get; set; }
public Dictionary<string, List<string>> ModuleEnabledFeatures { get; set; }
public ApplicationGlobalFeatureConfigurationDto()
{
EnabledFeatures = new HashSet<string>();
ModuleEnabledFeatures = new Dictionary<string, List<string>>();
}
}

@ -14,6 +14,7 @@ using Volo.Abp.AspNetCore.Mvc.MultiTenancy;
using Volo.Abp.Authorization;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Features;
using Volo.Abp.GlobalFeatures;
using Volo.Abp.Localization;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Settings;
@ -87,6 +88,7 @@ public class AbpApplicationConfigurationAppService : ApplicationService, IAbpApp
{
Auth = await GetAuthConfigAsync(),
Features = await GetFeaturesConfigAsync(),
GlobalFeatures = await GetGlobalFeaturesConfigAsync(),
Localization = await GetLocalizationConfigAsync(),
CurrentUser = GetCurrentUser(),
Setting = await GetSettingConfigAsync(),
@ -284,6 +286,24 @@ public class AbpApplicationConfigurationAppService : ApplicationService, IAbpApp
return result;
}
protected virtual Task<ApplicationGlobalFeatureConfigurationDto> GetGlobalFeaturesConfigAsync()
{
var result = new ApplicationGlobalFeatureConfigurationDto();
foreach (var enabledFeatureName in GlobalFeatureManager.Instance.GetEnabledFeatureNames())
{
result.EnabledFeatures.AddIfNotContains(enabledFeatureName);
}
foreach (var module in GlobalFeatureManager.Instance.Modules)
{
result.ModuleEnabledFeatures.AddIfNotContains(new KeyValuePair<string, List<string>>(module.Key, module.Value.GetFeatures().Select(x => x.FeatureName).ToList()));
}
return Task.FromResult(result);
}
protected virtual async Task<TimingDto> GetTimingConfigAsync()
{
var windowsTimeZoneId = await _settingProvider.GetOrNullAsync(TimingSettingNames.TimeZone);

@ -47,7 +47,7 @@ public class ConnectionPool : IConnectionPool, ISingletonDependency
connectionName, new Lazy<ServiceBusAdministrationClient>(() =>
{
var config = _options.Connections.GetOrDefault(connectionName);
return new ServiceBusAdministrationClient(config.ConnectionString);
return new ServiceBusAdministrationClient(config.ConnectionString, config.Admin);
})
).Value;
}

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
namespace Volo.Abp.BackgroundWorkers.Hangfire;
@ -7,6 +8,16 @@ public abstract class HangfireBackgroundWorkerBase : BackgroundWorkerBase, IHang
public string RecurringJobId { get; set; }
public string CronExpression { get; set; }
public TimeZoneInfo TimeZone { get; set; }
public string Queue { get; set; }
public abstract Task DoWorkAsync();
protected HangfireBackgroundWorkerBase()
{
TimeZone = null;
Queue = "default";
}
}

@ -40,12 +40,13 @@ public class HangfireBackgroundWorkerManager : IBackgroundWorkerManager, ISingle
var unProxyWorker = ProxyHelper.UnProxy(hangfireBackgroundWorker);
if (hangfireBackgroundWorker.RecurringJobId.IsNullOrWhiteSpace())
{
RecurringJob.AddOrUpdate(() => ((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(), hangfireBackgroundWorker.CronExpression);
RecurringJob.AddOrUpdate(() => ((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(),
hangfireBackgroundWorker.CronExpression, hangfireBackgroundWorker.TimeZone, hangfireBackgroundWorker.Queue);
}
else
{
RecurringJob.AddOrUpdate(hangfireBackgroundWorker.RecurringJobId, () => ((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(),
hangfireBackgroundWorker.CronExpression);
hangfireBackgroundWorker.CronExpression, hangfireBackgroundWorker.TimeZone, hangfireBackgroundWorker.Queue);
}
}
else
@ -79,7 +80,7 @@ public class HangfireBackgroundWorkerManager : IBackgroundWorkerManager, ISingle
var adapterType = typeof(HangfirePeriodicBackgroundWorkerAdapter<>).MakeGenericType(ProxyHelper.GetUnProxiedType(worker));
var workerAdapter = Activator.CreateInstance(adapterType) as IHangfireBackgroundWorker;
RecurringJob.AddOrUpdate(() => workerAdapter.DoWorkAsync(), GetCron(period.Value));
RecurringJob.AddOrUpdate(() => workerAdapter.DoWorkAsync(), GetCron(period.Value), workerAdapter.TimeZone, workerAdapter.Queue);
}
return Task.CompletedTask;

@ -17,7 +17,7 @@ public class HangfirePeriodicBackgroundWorkerAdapter<TWorker> : HangfireBackgrou
_doWorkMethod = typeof(TWorker).GetMethod("DoWork", BindingFlags.Instance | BindingFlags.NonPublic);
}
public override async Task DoWorkAsync()
public async override Task DoWorkAsync()
{
var workerContext = new PeriodicBackgroundWorkerContext(ServiceProvider);
var worker = ServiceProvider.GetRequiredService<TWorker>();

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
namespace Volo.Abp.BackgroundWorkers.Hangfire;
@ -7,6 +8,10 @@ public interface IHangfireBackgroundWorker : IBackgroundWorker
string RecurringJobId { get; set; }
string CronExpression { get; set; }
TimeZoneInfo TimeZone { get; set; }
string Queue { get; set; }
Task DoWorkAsync();
}

@ -11,6 +11,7 @@
CurrentPage="@CurrentPage"
PageSize="@PageSize"
Responsive="@Responsive"
Striped
Class="@Class">
<LoadingTemplate>
<Row Class="w-100 align-items-center" Style="height: 150px;">

@ -94,6 +94,11 @@ public partial class UiMessageAlert : ComponentBase, IDisposable
Options = e.Options;
Callback = e.Callback;
await ShowMessageAlert();
}
protected virtual async Task ShowMessageAlert()
{
await InvokeAsync(ModalRef.Show);
}

@ -3,7 +3,7 @@
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<AssemblyName>Volo.Abp.BlobStoring.Minio</AssemblyName>
<PackageId>Volo.Abp.BlobStoring.Minio</PackageId>
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>
@ -15,7 +15,7 @@
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.BlobStoring\Volo.Abp.BlobStoring.csproj" />
<PackageReference Include="Minio" Version="3.1.13" />
<PackageReference Include="Minio" Version="4.0.1" />
</ItemGroup>
</Project>

@ -100,7 +100,7 @@ public class SolutionModuleAdder : ITransientDependency
Check.NotNull(solutionFile, nameof(solutionFile));
Check.NotNull(moduleName, nameof(moduleName));
await PublishEventAsync(1, "Retriving module info...");
await PublishEventAsync(1, "Retrieving module info...");
var module = await GetModuleInfoAsync(moduleName, newTemplate, newProTemplate);

@ -15,7 +15,7 @@ public class ExposeServicesAttribute : Attribute, IExposedServiceTypesProvider
public ExposeServicesAttribute(params Type[] serviceTypes)
{
ServiceTypes = serviceTypes ?? new Type[0];
ServiceTypes = serviceTypes ?? Type.EmptyTypes;
}
public Type[] GetExposedServiceTypes(Type targetType)
@ -49,6 +49,10 @@ public class ExposeServicesAttribute : Attribute, IExposedServiceTypesProvider
foreach (var interfaceType in type.GetTypeInfo().GetInterfaces())
{
var interfaceName = interfaceType.Name;
if (interfaceType.IsGenericType)
{
interfaceName = interfaceType.Name.Left(interfaceType.Name.IndexOf('`'));
}
if (interfaceName.StartsWith("I"))
{

@ -57,8 +57,6 @@ public static class CultureHelper
public static string GetBaseCultureName(string cultureName)
{
return cultureName.Contains("-")
? cultureName.Left(cultureName.IndexOf("-", StringComparison.Ordinal))
: cultureName;
return new CultureInfo(cultureName).Parent.Name;
}
}

@ -7,4 +7,6 @@ public class AbpAzureEventBusOptions
public string SubscriberName { get; set; }
public string TopicName { get; set; }
public bool IsServiceBusDisabled { get; set; }
}

@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.AzureServiceBus;
using Volo.Abp.Modularity;
@ -19,9 +20,15 @@ public class AbpEventBusAzureModule : AbpModule
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
context
.ServiceProvider
.GetRequiredService<AzureDistributedEventBus>()
.Initialize();
var options = context.ServiceProvider.GetRequiredService<IOptions<AbpAzureEventBusOptions>>().Value;
if (!options.IsServiceBusDisabled)
{
context
.ServiceProvider
.GetRequiredService<AzureDistributedEventBus>()
.Initialize();
}
}
}

@ -1,10 +1,23 @@
namespace Volo.Abp.EventBus.RabbitMq;
using Volo.Abp.RabbitMQ;
namespace Volo.Abp.EventBus.RabbitMq;
public class AbpRabbitMqEventBusOptions
{
public const string DefaultExchangeType = RabbitMqConsts.ExchangeTypes.Direct;
public string ConnectionName { get; set; }
public string ClientName { get; set; }
public string ExchangeName { get; set; }
public string ExchangeType { get; set; }
public string GetExchangeTypeOrDefault()
{
return string.IsNullOrEmpty(ExchangeType)
? DefaultExchangeType
: ExchangeType;
}
}

@ -69,7 +69,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, ISingletonDe
Consumer = MessageConsumerFactory.Create(
new ExchangeDeclareConfiguration(
AbpRabbitMqEventBusOptions.ExchangeName,
type: "direct",
type: AbpRabbitMqEventBusOptions.GetExchangeTypeOrDefault(),
durable: true
),
new QueueDeclareConfiguration(
@ -244,7 +244,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, ISingletonDe
{
channel.ExchangeDeclare(
AbpRabbitMqEventBusOptions.ExchangeName,
"direct",
AbpRabbitMqEventBusOptions.GetExchangeTypeOrDefault(),
durable: true
);

@ -8,4 +8,15 @@ public static class RabbitMqConsts
public const int Persistent = 2;
}
public static class ExchangeTypes
{
public const string Direct = "direct";
public const string Topic = "topic";
public const string Fanout = "fanout";
public const string Headers = "headers";
}
}

@ -33,6 +33,21 @@ public class AutoRegistrationHelper_Tests
exposedServices.ShouldContain(typeof(IDerivedService));
}
[Fact]
public void Should_Get_Conventional_Exposed_Generic_Types_By_Default()
{
//Act
var exposedServices = ExposedServiceExplorer.GetExposedServices(typeof(DefaultGenericService));
//Assert
exposedServices.Count.ShouldBe(4);
exposedServices.ShouldContain(typeof(IService));
exposedServices.ShouldContain(typeof(IGenericService<string>));
exposedServices.ShouldContain(typeof(IGenericService<int>));
exposedServices.ShouldContain(typeof(DefaultGenericService));
}
public class DefaultDerivedService : IDerivedService
{
}
@ -50,4 +65,14 @@ public class AutoRegistrationHelper_Tests
{
}
public interface IGenericService<T>
{
}
public class DefaultGenericService : IService, IGenericService<string>, IGenericService<int>
{
}
}

@ -1,4 +1,4 @@
using System.Globalization;
using System.Globalization;
using System.Linq;
using Microsoft.Extensions.Localization;
using Shouldly;
@ -183,6 +183,60 @@ public class AbpLocalization_Tests : AbpIntegratedTest<AbpLocalization_Tests.Tes
{
_localizer["CarPlural"].Value.ShouldBe("Autos");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-Hans")))
{
_localizer["Car"].Value.ShouldBe("汽车");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-Hans")))
{
_localizer["CarPlural"].Value.ShouldBe("汽车");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-CN")))
{
_localizer["Car"].Value.ShouldBe("汽车");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-CN")))
{
_localizer["CarPlural"].Value.ShouldBe("汽车");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-Hans-CN")))
{
_localizer["Car"].Value.ShouldBe("汽车");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-Hans-CN")))
{
_localizer["CarPlural"].Value.ShouldBe("汽车");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-Hant")))
{
_localizer["Car"].Value.ShouldBe("汽車");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-Hant")))
{
_localizer["CarPlural"].Value.ShouldBe("汽車");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-TW")))
{
_localizer["Car"].Value.ShouldBe("汽車");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-TW")))
{
_localizer["CarPlural"].Value.ShouldBe("汽車");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-Hant-TW")))
{
_localizer["Car"].Value.ShouldBe("汽車");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-Hant-TW")))
{
_localizer["CarPlural"].Value.ShouldBe("汽車");
}
}
[Fact]

@ -277,6 +277,18 @@ public class LoginModel : AccountPageModel
CheckIdentityErrors(await UserManager.SetEmailAsync(user, emailAddress));
CheckIdentityErrors(await UserManager.AddLoginAsync(user, info));
CheckIdentityErrors(await UserManager.AddDefaultRolesAsync(user));
user.Name = info.Principal.FindFirstValue(AbpClaimTypes.Name);
user.Surname = info.Principal.FindFirstValue(AbpClaimTypes.SurName);
var phoneNumber = info.Principal.FindFirstValue(AbpClaimTypes.PhoneNumber);
if (!phoneNumber.IsNullOrWhiteSpace())
{
var phoneNumberConfirmed = string.Equals(info.Principal.FindFirstValue(AbpClaimTypes.PhoneNumberVerified), "true", StringComparison.InvariantCultureIgnoreCase);
user.SetPhoneNumber(phoneNumber, phoneNumberConfirmed);
}
await UserManager.UpdateAsync(user);
return user;
}

@ -231,11 +231,16 @@ namespace Volo.Blogging.Posts
return url;
}
private async Task SaveTags(ICollection<string> newTags, Post post)
private async Task SaveTags(ICollection<string> tags, Post post)
{
await RemoveOldTags(newTags, post);
await AddNewTags(newTags, post);
tags = tags
.Select(t => t.ToLowerInvariant())
.Distinct()
.ToList();
await RemoveOldTags(tags, post);
await AddNewTags(tags, post);
}
private async Task RemoveOldTags(ICollection<string> newTags, Post post)

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Volo.CmsKit.Migrations
{
public partial class Added_BlogPostStatus : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "Status",
table: "CmsBlogPosts",
type: "int",
nullable: false,
defaultValue: 0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Status",
table: "CmsBlogPosts");
}
}
}

@ -1341,6 +1341,9 @@ namespace Volo.CmsKit.Migrations
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<int>("Status")
.HasColumnType("int");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");

@ -7,6 +7,5 @@ module.exports = {
"@libs"
],
mappings: {
}
}

@ -2,6 +2,7 @@
using Volo.Abp.Application.Dtos;
using Volo.Abp.Auditing;
using Volo.Abp.Domain.Entities;
using Volo.CmsKit.Blogs;
namespace Volo.CmsKit.Admin.Blogs;
@ -25,4 +26,6 @@ public class BlogPostDto : EntityDto<Guid>, IHasCreationTime, IHasModificationTi
public DateTime? LastModificationTime { get; set; }
public string ConcurrencyStamp { get; set; }
public BlogPostStatus Status { get; set; }
}

@ -1,5 +1,6 @@
using System;
using Volo.Abp.Application.Dtos;
using Volo.CmsKit.Blogs;
namespace Volo.CmsKit.Admin.Blogs;
@ -8,4 +9,8 @@ public class BlogPostGetListInput : PagedAndSortedResultRequestDto
public string Filter { get; set; }
public Guid? BlogId { get; set; }
}
public Guid? AuthorId { get; set; }
public BlogPostStatus? Status { get; set; }
}

@ -1,6 +1,7 @@
using System;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Auditing;
using Volo.CmsKit.Blogs;
namespace Volo.CmsKit.Admin.Blogs;
@ -24,4 +25,6 @@ public class BlogPostListDto : EntityDto<Guid>, IHasCreationTime, IHasModificati
public DateTime CreationTime { get; set; }
public DateTime? LastModificationTime { get; set; }
public BlogPostStatus? Status { get; set; }
}

@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace Volo.CmsKit.Admin.Blogs;
@ -12,4 +13,15 @@ public interface IBlogPostAdminAppService
CreateBlogPostDto,
UpdateBlogPostDto>
{
Task PublishAsync(Guid id);
Task DraftAsync(Guid id);
Task<BlogPostDto> CreateAndPublishAsync(CreateBlogPostDto input);
Task SendToReviewAsync(Guid id);
Task<BlogPostDto> CreateAndSendToReviewAsync(CreateBlogPostDto input);
Task<bool> HasBlogPostWaitingForReviewAsync();
}

@ -55,6 +55,8 @@ public class CmsKitAdminPermissionDefinitionProvider : PermissionDefinitionProvi
.RequireGlobalFeatures(typeof(BlogsFeature));
blogPostManagement.AddChild(CmsKitAdminPermissions.BlogPosts.Delete, L("Permission:BlogPostManagement.Delete"))
.RequireGlobalFeatures(typeof(BlogsFeature));
blogPostManagement.AddChild(CmsKitAdminPermissions.BlogPosts.Publish, L("Permission:BlogPostManagement.Publish"))
.RequireGlobalFeatures(typeof(BlogsFeature));
var menuManagement = cmsGroup.AddPermission(CmsKitAdminPermissions.Menus.Default, L("Permission:MenuManagement"))
.RequireGlobalFeatures(typeof(MenuFeature));

@ -51,6 +51,7 @@ public class CmsKitAdminPermissions
public const string Create = Default + ".Create";
public const string Update = Default + ".Update";
public const string Delete = Default + ".Delete";
public const string Publish = Default + ".Publish";
}
public static class Menus

@ -6,6 +6,7 @@ using Volo.Abp.Application.Dtos;
using Volo.Abp.Data;
using Volo.Abp.GlobalFeatures;
using Volo.Abp.Users;
using Volo.CmsKit.Admin.MediaDescriptors;
using Volo.CmsKit.Blogs;
using Volo.CmsKit.GlobalFeatures;
using Volo.CmsKit.Permissions;
@ -22,16 +23,20 @@ public class BlogPostAdminAppService : CmsKitAppServiceBase, IBlogPostAdminAppSe
protected IBlogRepository BlogRepository { get; }
protected ICmsUserLookupService UserLookupService { get; }
protected IMediaDescriptorAdminAppService MediaDescriptorAdminAppService { get; }
public BlogPostAdminAppService(
BlogPostManager blogPostManager,
IBlogPostRepository blogPostRepository,
IBlogRepository blogRepository,
ICmsUserLookupService userLookupService)
ICmsUserLookupService userLookupService,
IMediaDescriptorAdminAppService mediaDescriptorAdminAppService)
{
BlogPostManager = blogPostManager;
BlogPostRepository = blogPostRepository;
BlogRepository = blogRepository;
UserLookupService = userLookupService;
MediaDescriptorAdminAppService = mediaDescriptorAdminAppService;
}
[Authorize(CmsKitAdminPermissions.BlogPosts.Create)]
@ -42,13 +47,15 @@ public class BlogPostAdminAppService : CmsKitAppServiceBase, IBlogPostAdminAppSe
var blog = await BlogRepository.GetAsync(input.BlogId);
var blogPost = await BlogPostManager.CreateAsync(
author,
blog,
input.Title,
input.Slug,
input.ShortDescription,
input.Content,
input.CoverImageMediaId);
author,
blog,
input.Title,
input.Slug,
BlogPostStatus.Draft,
input.ShortDescription,
input.Content,
input.CoverImageMediaId
);
await BlogPostRepository.InsertAsync(blogPost);
@ -59,13 +66,18 @@ public class BlogPostAdminAppService : CmsKitAppServiceBase, IBlogPostAdminAppSe
public virtual async Task<BlogPostDto> UpdateAsync(Guid id, UpdateBlogPostDto input)
{
var blogPost = await BlogPostRepository.GetAsync(id);
blogPost.SetTitle(input.Title);
blogPost.SetShortDescription(input.ShortDescription);
blogPost.SetContent(input.Content);
blogPost.SetConcurrencyStampIfNotNull(input.ConcurrencyStamp);
if (blogPost.CoverImageMediaId != null && input.CoverImageMediaId == null)
{
await MediaDescriptorAdminAppService.DeleteAsync(blogPost.CoverImageMediaId.Value);
}
blogPost.CoverImageMediaId = input.CoverImageMediaId;
if (blogPost.Slug != input.Slug)
{
await BlogPostManager.SetSlugUrlAsync(blogPost, input.Slug);
@ -89,9 +101,11 @@ public class BlogPostAdminAppService : CmsKitAppServiceBase, IBlogPostAdminAppSe
{
var blogs = (await BlogRepository.GetListAsync()).ToDictionary(x => x.Id);
var blogPosts = await BlogPostRepository.GetListAsync(input.Filter, input.BlogId, input.MaxResultCount, input.SkipCount, input.Sorting);
var blogPosts = await BlogPostRepository.GetListAsync(input.Filter, input.BlogId, input.AuthorId,
statusFilter: input.Status,
input.MaxResultCount, input.SkipCount, input.Sorting);
var count = await BlogPostRepository.GetCountAsync(input.Filter);
var count = await BlogPostRepository.GetCountAsync(input.Filter, input.BlogId, input.AuthorId);
var dtoList = blogPosts.Select(x =>
{
@ -109,4 +123,57 @@ public class BlogPostAdminAppService : CmsKitAppServiceBase, IBlogPostAdminAppSe
{
await BlogPostRepository.DeleteAsync(id);
}
}
[Authorize(CmsKitAdminPermissions.BlogPosts.Publish)]
public virtual async Task PublishAsync(Guid id)
{
var blogPost = await BlogPostRepository.GetAsync(id);
blogPost.SetPublished();
await BlogPostRepository.UpdateAsync(blogPost);
}
[Authorize(CmsKitAdminPermissions.BlogPosts.Update)]
public virtual async Task DraftAsync(Guid id)
{
var blogPost = await BlogPostRepository.GetAsync(id);
blogPost.SetDraft();
await BlogPostRepository.UpdateAsync(blogPost);
}
[Authorize(CmsKitAdminPermissions.BlogPosts.Create)]
[Authorize(CmsKitAdminPermissions.BlogPosts.Publish)]
public virtual async Task<BlogPostDto> CreateAndPublishAsync(CreateBlogPostDto input)
{
var blogPost = await CreateAsync(input);
await CurrentUnitOfWork.SaveChangesAsync();
await PublishAsync(blogPost.Id);
blogPost.Status = BlogPostStatus.Published;
return blogPost;
}
[Authorize(CmsKitAdminPermissions.BlogPosts.Create)]
public virtual async Task SendToReviewAsync(Guid id)
{
var blogPost = await BlogPostRepository.GetAsync(id);
blogPost.SetWaitingForReview();
await BlogPostRepository.UpdateAsync(blogPost);
}
[Authorize(CmsKitAdminPermissions.BlogPosts.Create)]
public virtual async Task<BlogPostDto> CreateAndSendToReviewAsync(CreateBlogPostDto input)
{
var blogPost = await CreateAsync(input);
await CurrentUnitOfWork.SaveChangesAsync();
await SendToReviewAsync(blogPost.Id);
blogPost.Status = BlogPostStatus.WaitingForReview;
return blogPost;
}
[Authorize(CmsKitAdminPermissions.BlogPosts.Publish)]
public async Task<bool> HasBlogPostWaitingForReviewAsync()
{
return await BlogPostRepository.HasBlogPostWaitingForReviewAsync();
}
}

@ -55,4 +55,49 @@ public partial class BlogPostAdminClientProxy : ClientProxyBase<IBlogPostAdminAp
{ typeof(UpdateBlogPostDto), input }
});
}
public virtual async Task PublishAsync(Guid id)
{
await RequestAsync(nameof(PublishAsync), new ClientProxyRequestTypeValue
{
{ typeof(Guid), id },
});
}
public virtual async Task DraftAsync(Guid id)
{
await RequestAsync(nameof(DraftAsync), new ClientProxyRequestTypeValue
{
{ typeof(Guid), id },
});
}
public virtual async Task<BlogPostDto> CreateAndPublishAsync(CreateBlogPostDto input)
{
return await RequestAsync<BlogPostDto>(nameof(CreateAndPublishAsync), new ClientProxyRequestTypeValue
{
{ typeof(CreateBlogPostDto), input }
});
}
public virtual async Task SendToReviewAsync(Guid id)
{
await RequestAsync(nameof(SendToReviewAsync), new ClientProxyRequestTypeValue
{
{ typeof(Guid), id },
});
}
public virtual async Task<BlogPostDto> CreateAndSendToReviewAsync(CreateBlogPostDto input)
{
return await RequestAsync<BlogPostDto>(nameof(CreateAndSendToReviewAsync), new ClientProxyRequestTypeValue
{
{ typeof(CreateBlogPostDto), input }
});
}
public virtual async Task<bool> HasBlogPostWaitingForReviewAsync()
{
return await RequestAsync<bool>(nameof(HasBlogPostWaitingForReviewAsync));
}
}

@ -61,4 +61,53 @@ public class BlogPostAdminController : CmsKitAdminController, IBlogPostAdminAppS
{
return BlogPostAdminAppService.UpdateAsync(id, input);
}
}
[HttpPost]
[Route("{id}/publish")]
[Authorize(CmsKitAdminPermissions.BlogPosts.Publish)]
public virtual Task PublishAsync(Guid id)
{
return BlogPostAdminAppService.PublishAsync(id);
}
[HttpPost]
[Route("{id}/draft")]
[Authorize(CmsKitAdminPermissions.BlogPosts.Update)]
public virtual Task DraftAsync(Guid id)
{
return BlogPostAdminAppService.DraftAsync(id);
}
[HttpPost]
[Route("create-and-publish")]
[Authorize(CmsKitAdminPermissions.BlogPosts.Create)]
[Authorize(CmsKitAdminPermissions.BlogPosts.Publish)]
public virtual Task<BlogPostDto> CreateAndPublishAsync(CreateBlogPostDto input)
{
return BlogPostAdminAppService.CreateAndPublishAsync(input);
}
[HttpPost]
[Route("{id}/send-to-review")]
[Authorize(CmsKitAdminPermissions.BlogPosts.Create)]
public virtual Task SendToReviewAsync(Guid id)
{
return BlogPostAdminAppService.SendToReviewAsync(id);
}
[HttpPost]
[Route("create-and-send-to-review")]
[Authorize(CmsKitAdminPermissions.BlogPosts.Create)]
public virtual Task<BlogPostDto> CreateAndSendToReviewAsync(CreateBlogPostDto input)
{
return BlogPostAdminAppService.CreateAndSendToReviewAsync(input);
}
[HttpGet]
[Route("has-blogpost-waiting-for-review")]
[Authorize(CmsKitAdminPermissions.BlogPosts.Publish)]
public virtual Task<bool> HasBlogPostWaitingForReviewAsync()
{
return BlogPostAdminAppService.HasBlogPostWaitingForReviewAsync();
}
}

@ -1,6 +1,7 @@
@page
@using System.Globalization
@using Microsoft.AspNetCore.Authorization
@using Volo.Abp.AspNetCore.Mvc.UI.Packages.TuiEditor
@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Pages.Shared.Components.AbpPageToolbar
@using Volo.CmsKit.Admin.Web.Pages
@ -10,9 +11,10 @@
@using Volo.CmsKit.Blogs
@using Volo.Abp.AspNetCore.Mvc.UI.Packages.Uppy
@using Volo.Abp.AspNetCore.Mvc.UI.Packages.Slugify
@using Volo.CmsKit.Permissions
@inherits CmsKitAdminPageBase
@inject IAuthorizationService AuthorizationService
@model CreateModel
@{
@ -69,6 +71,7 @@
data-input-id="@Html.IdFor(x => x.ViewModel.Content)"
data-language="@(CultureInfo.CurrentUICulture.TwoLetterISOLanguageName)">
</div>
<input type="hidden" id="ViewModel_Status" name="ViewModel.Status" value="" class="form-control ">
</abp-dynamic-form>
<div id="blog-post-tags-wrapper">
@ -84,6 +87,13 @@
</abp-card-body>
<abp-card-footer>
<abp-button button-type="Primary" type="submit" text="@L["Submit"].Value" id="button-blog-post-create" />
<abp-button button-type="Primary" type="button" text="@L["SaveAsDraft"].Value" id="button-blog-post-create" />
@if ((await AuthorizationService.AuthorizeAsync(CmsKitAdminPermissions.BlogPosts.Publish)).Succeeded)
{
<abp-button button-type="Primary" type="button" text="@L["Publish"].Value" id="button-blog-post-publish"/>
}else
{
<abp-button button-type="Primary" type="button" text="@L["SendToReviewToPublish"].Value" id="button-blog-post-send-to-review"/>
}
</abp-card-footer>
</abp-card>

@ -31,9 +31,21 @@ public class CreateModel : CmsKitAdminPageModel
{
var dto = ObjectMapper.Map<CreateBlogPostViewModel, CreateBlogPostDto>(ViewModel);
var created = await BlogPostAdminAppService.CreateAsync(dto);
BlogPostDto createResult;
if (ViewModel.Status == BlogPostStatus.Published)
{
createResult = await BlogPostAdminAppService.CreateAndPublishAsync(dto);
}
else if (ViewModel.Status == BlogPostStatus.WaitingForReview)
{
createResult = await BlogPostAdminAppService.CreateAndSendToReviewAsync(dto);
}
else
{
createResult = await BlogPostAdminAppService.CreateAsync(dto);
}
return new OkObjectResult(created);
return new OkObjectResult(createResult);
}
[AutoMap(typeof(CreateBlogPostDto), ReverseMap = true)]
@ -65,5 +77,9 @@ public class CreateModel : CmsKitAdminPageModel
[HiddenInput]
public Guid? CoverImageMediaId { get; set; }
[HiddenInput]
[DynamicFormIgnore]
public BlogPostStatus Status { get; set; }
}
}

@ -5,6 +5,7 @@
@using Volo.CmsKit.Admin.Web.Pages
@using Volo.CmsKit.Admin.Web.Menus
@using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Pages.Shared.Components.AbpPageToolbar
@using Volo.CmsKit.Blogs
@inherits CmsKitAdminPageBase
@ -27,11 +28,24 @@
@await Component.InvokeAsync(typeof(AbpPageToolbarViewComponent), new {pageName = typeof(IndexModel).FullName})
}
<div class="alert alert-warning" style="display: none" role="alert" id="alertHasBlogPostWaitingForReview">
<abp-button button-type="Link" type="button" text="@L["HasBlogPostWaitingForReviewMessage"].Value" id="button-show-waiting-for-review"/>
</div>
<abp-card class="mb-4">
<abp-card-body>
<div id="CmsKitBlogPostsWrapper">
<abp-row>
<abp-column>
<abp-column size="_2">
<select id="StatusSelect" class="form-select">
<option value="">@L["SelectAStatus"]</option>
@foreach (var status in (BlogPostStatus[]) Enum.GetValues(typeof(BlogPostStatus)))
{
<option value="@((int)status)">@L["CmsKit.BlogPost.Status." + (int)status]</option>
}
</select>
</abp-column>
<abp-column size="_10">
@await Component.InvokeAsync(typeof(AbpPageSearchBoxViewComponent))
</abp-column>
</abp-row>

@ -48,13 +48,17 @@
<abp-card-body>
<div class="mb-3">
@if (Model.ViewModel.CoverImageMediaId != null)
{
<img height="120" src="/api/cms-kit/media/@Model.ViewModel.CoverImageMediaId" />
<br />
}
<label class="form-label" >@L["CoverImage"]</label>
<input type="file" id="BlogPostCoverImage" class="form-control" />
<div id="CurrentCoverImageArea">
@if (Model.ViewModel.CoverImageMediaId != null)
{
<img height="120" src="/api/cms-kit/media/@Model.ViewModel.CoverImageMediaId"/>
<br/>
<abp-button button-type="Link" type="button" text="@L["RemoveCoverImage"].Value" id="button-remove-cover-image"/>
<br/>
}
</div>
<label class="form-label">@L["CoverImage"]</label>
<input type="file" id="BlogPostCoverImage" class="form-control"/>
</div>
<abp-dynamic-form abp-model="ViewModel" asp-page="/CmsKit/BlogPosts/Update" id="form-blog-post-update">

@ -2,13 +2,22 @@ $(function () {
var l = abp.localization.getResource("CmsKit");
var blogPostStatus = {
Draft: 0,
Published: 1,
SendToReview: 2
};
var $selectBlog = $('#BlogSelectionSelect');
var $formCreate = $('#form-blog-post-create');
var $title = $('#ViewModel_Title');
var $shortDescription = $('#ViewModel_ShortDescription');
var $coverImage = $('#ViewModel_CoverImageMediaId');
var $url = $('#ViewModel_Slug');
var $status = $('#ViewModel_Status');
var $buttonSubmit = $('#button-blog-post-create');
var $buttonPublish = $('#button-blog-post-publish');
var $buttonSendToReview = $('#button-blog-post-send-to-review');
var $pageContentInput = $('#ViewModel_Content');
var $tagsInput = $('.tag-editor-form input[name=tags]');
var $fileInput = $('#BlogPostCoverImage');
@ -59,9 +68,36 @@ $(function () {
$buttonSubmit.click(function (e) {
e.preventDefault();
$status.val(blogPostStatus.Draft);
submitCoverImage();
});
$buttonPublish.click(function (e) {
abp.message.confirm(
l('BlogPostPublishConfirmationMessage', $title.val()),
function (isConfirmed) {
if (isConfirmed) {
e.preventDefault();
$status.val(blogPostStatus.Published);
submitCoverImage();
}
}
);
});
$buttonSendToReview.click(function (e) {
abp.message.confirm(
l('BlogPostSendToReviewConfirmationMessage', $title.val()),
function (isConfirmed) {
if (isConfirmed) {
e.preventDefault();
$status.val(blogPostStatus.SendToReview);
submitCoverImage();
}
}
);
});
function submitEntityTags(blogPostId) {
if ($tagsInput.val()) {

@ -1,13 +1,26 @@
$(function () {
var l = abp.localization.getResource("CmsKit");
var $statusFilter = $("#StatusSelect");
var blogPostStatus = {
Draft: 0,
Published: 1,
SendToReview: 2
};
var blogsService = volo.cmsKit.admin.blogs.blogPostAdmin;
var getFilter = function () {
return {
var filter = {
filter: $('#CmsKitBlogPostsWrapper input.page-search-filter-text').val()
};
if ($statusFilter.val()) {
filter.status = $statusFilter.val();
}
return filter;
};
var dataTable = $("#BlogPostsTable").DataTable(abp.libs.datatables.normalizeConfiguration({
@ -33,6 +46,60 @@ $(function () {
location.href = "BlogPosts/Update/" + data.record.id
}
},
{
text: l('Publish'),
visible: function(data) {
return data?.status !== blogPostStatus.Published && abp.auth.isGranted('CmsKit.BlogPosts.Publish');
},
confirmMessage: function (data) {
return l("BlogPostPublishConfirmationMessage", data.record.title)
},
action: function (data) {
blogsService
.publish(data.record.id)
.then(function () {
dataTable.ajax.reload();
abp.notify.success(l('SuccessfullyPublished'));
checkHasBlogPostWaitingForReview();
});
}
},
{
text: l('SendToReview'),
visible: function(data) {
return data?.status === blogPostStatus.Draft &&
!abp.auth.isGranted('CmsKit.BlogPosts.Publish');
},
confirmMessage: function (data) {
return l("BlogPostPublishConfirmationMessage", data.record.title)
},
action: function (data) {
blogsService
.sendToReview(data.record.id)
.then(function () {
dataTable.ajax.reload();
abp.notify.success(l('BlogPostSendToReviewSuccessMessage', data.record.title));
});
}
},
{
text: l('Draft'),
visible: function(data) {
return data?.status !== blogPostStatus.Draft && abp.auth.isGranted('CmsKit.BlogPosts.Update');
},
confirmMessage: function (data) {
return l("BlogPostDraftConfirmationMessage", data.record.title)
},
action: function (data) {
blogsService
.draft(data.record.id)
.then(function () {
dataTable.ajax.reload();
abp.notify.success(l('SuccessfullySaved'));
checkHasBlogPostWaitingForReview();
});
}
},
{
text: l('Delete'),
visible: abp.auth.isGranted('CmsKit.BlogPosts.Delete'),
@ -71,7 +138,15 @@ $(function () {
orderable: true,
data: 'creationTime',
dataFormat: "datetime"
}
},
{
title: l("Status"),
orderable: true,
data: "status",
render: function (data) {
return l("CmsKit.BlogPost.Status." + data);
}
},
]
}));
@ -84,4 +159,25 @@ $(function () {
e.preventDefault();
window.location.href = "BlogPosts/Create"
});
$('#button-show-waiting-for-review').on('click', function (e) {
e.preventDefault();
$statusFilter.val(blogPostStatus.SendToReview);
dataTable.ajax.reload();
});
function checkHasBlogPostWaitingForReview(){
if (!abp.auth.isGranted('CmsKit.BlogPosts.Publish')){
$('#alertHasBlogPostWaitingForReview').hide();
}
blogsService.hasBlogPostWaitingForReview().then(function (result) {
if (result) {
$('#alertHasBlogPostWaitingForReview').show('fast');
} else {
$('#alertHasBlogPostWaitingForReview').hide('fast');
}
});
}
checkHasBlogPostWaitingForReview();
});

@ -10,6 +10,7 @@ $(function () {
var $blogPostIdInput = $('#Id');
var $tagsInput = $('.tag-editor-form input[name=tags]');
var $fileInput = $('#BlogPostCoverImage');
var $buttonRemoveCoverImage = $('#button-remove-cover-image');
var UPPY_FILE_ID = "uppy-upload-file";
@ -155,7 +156,6 @@ $(function () {
}
}
// -----------------------------------
var fileUploadUri = "/api/cms-kit-admin/media/blogpost";
var fileUriPrefix = "/api/cms-kit/media/";
@ -228,4 +228,16 @@ $(function () {
}
});
}
$buttonRemoveCoverImage.on('click', function () {
abp.message.confirm(
l('RemoveCoverImageConfirmationMessage'),
function (isConfirmed) {
if (isConfirmed) {
$coverImage.val(null);
$('#CurrentCoverImageArea').remove();
}
}
);
});
});

@ -2,6 +2,7 @@
@using Microsoft.AspNetCore.Mvc.Localization
@using Volo.Abp.AspNetCore.Mvc.UI.Layout
@using Volo.Abp.AspNetCore.Mvc.UI.Packages.Codemirror
@using Volo.CmsKit.Admin.Web.Pages.CmsKit.GlobalResources
@using Volo.CmsKit.Admin.Web.Menus
@using Volo.CmsKit.Localization
@ -17,8 +18,15 @@
PageLayout.Content.MenuItemName = CmsKitAdminMenus.GlobalResources.GlobalResourcesMenu;
}
@section styles{
<abp-style type="typeof(CodemirrorStyleContributor)" />
}
@section scripts {
<abp-script-bundle>
<abp-script type="typeof(CodemirrorScriptContributor )"/>
<abp-script src="/libs/codemirror/mode/css/css.js"/>
<abp-script src="/libs/codemirror/mode/javascript/javascript.js"/>
<abp-script src="/client-proxies/cms-kit-common-proxy.js"/>
<abp-script src="/client-proxies/cms-kit-admin-proxy.js"/>
<abp-script src="/Pages/CmsKit/GlobalResources/index.js"/>

@ -3,11 +3,26 @@ $(function (){
var service = volo.cmsKit.admin.globalResources.globalResourceAdmin;
var scriptEditor = CodeMirror.fromTextArea(document.getElementById("ScriptContent"),{
mode:"javascript",
lineNumbers:true
});
var styleEditor = CodeMirror.fromTextArea(document.getElementById("StyleContent"),{
mode:"css",
lineNumbers:true
});
$('.nav-tabs a').on('shown.bs.tab', function() {
scriptEditor.refresh();
styleEditor.refresh();
});
$('#SaveResourcesButton').on('click','',function(){
service.setGlobalResources(
{
style: $('#StyleContent').val(),
script: $('#ScriptContent').val()
style: styleEditor.getValue(),
script: scriptEditor.getValue()
}
).then(function () {
abp.message.success(l("SavedSuccessfully"));

@ -1,6 +1,7 @@
@page
@using System.Globalization
@using Volo.Abp.AspNetCore.Mvc.UI.Packages.Codemirror
@using Volo.Abp.AspNetCore.Mvc.UI.Packages.TuiEditor
@using Volo.Abp.AspNetCore.Mvc.UI.Packages.Uppy
@using Volo.CmsKit.Admin.Web.Pages
@ -22,6 +23,9 @@
<abp-script type="typeof(TuiEditorScriptContributor)" />
<abp-script type="typeof(UppyScriptContributor)" />
<abp-script type="typeof(SlugifyScriptContributor)" />
<abp-script type="typeof(CodemirrorScriptContributor)"/>
<abp-script src="/libs/codemirror/mode/css/css.js"/>
<abp-script src="/libs/codemirror/mode/javascript/javascript.js"/>
<abp-script src="/client-proxies/cms-kit-common-proxy.js"/>
<abp-script src="/client-proxies/cms-kit-admin-proxy.js"/>
<abp-script src="/Pages/CmsKit/Pages/create.js" />
@ -31,6 +35,7 @@
@section styles {
<abp-style-bundle>
<abp-style type="typeof(TuiEditorStyleContributor)" />
<abp-style type="typeof(CodemirrorStyleContributor)" />
<abp-style src="/Pages/CmsKit/Pages/create.css"/>
</abp-style-bundle>
}
@ -56,11 +61,11 @@
</abp-tab>
<abp-tab title="@L["Script"]">
<abp-input asp-for="ViewModel.Script" suppress-label="true" class="cms-kit-editor" />
<abp-input asp-for="ViewModel.Script" suppress-label="true" />
</abp-tab>
<abp-tab title="@L["Style"]">
<abp-input asp-for="ViewModel.Style" suppress-label="true" class="cms-kit-editor"/>
<abp-input asp-for="ViewModel.Style" suppress-label="true"/>
</abp-tab>
</abp-tabs>

@ -1,6 +1,7 @@
@page "{Id}"
@using System.Globalization
@using Volo.Abp.AspNetCore.Mvc.UI.Packages.Codemirror
@using Volo.Abp.AspNetCore.Mvc.UI.Packages.TuiEditor
@using Volo.Abp.AspNetCore.Mvc.UI.Packages.Uppy
@using Volo.Abp.AspNetCore.Mvc.UI.Packages.Slugify
@ -23,6 +24,9 @@
<abp-script type="typeof(TuiEditorScriptContributor)" />
<abp-script type="typeof(UppyScriptContributor)" />
<abp-script type="typeof(SlugifyScriptContributor)" />
<abp-script type="typeof(CodemirrorScriptContributor)"/>
<abp-script src="/libs/codemirror/mode/css/css.js"/>
<abp-script src="/libs/codemirror/mode/javascript/javascript.js"/>
<abp-script src="/client-proxies/cms-kit-common-proxy.js"/>
<abp-script src="/client-proxies/cms-kit-admin-proxy.js"/>
<abp-script src="/Pages/CmsKit/Pages/update.js" />
@ -32,6 +36,7 @@
@section styles {
<abp-style-bundle>
<abp-style type="typeof(TuiEditorStyleContributor)" />
<abp-style type="typeof(CodemirrorStyleContributor)" />
<abp-style src="/Pages/CmsKit/Pages/update.css" />
</abp-style-bundle>
}

@ -6,22 +6,40 @@ $(function () {
var $slug = $('#ViewModel_Slug');
var $buttonSubmit = $('#button-page-create');
var scriptEditor = CodeMirror.fromTextArea(document.getElementById("ViewModel_Script"), {
mode: "javascript",
lineNumbers: true
});
var styleEditor = CodeMirror.fromTextArea(document.getElementById("ViewModel_Style"), {
mode: "css",
lineNumbers: true
});
$('.nav-tabs a').on('shown.bs.tab', function () {
scriptEditor.refresh();
styleEditor.refresh();
});
$createForm.data('validator').settings.ignore = ":hidden, [contenteditable='true']:not([name]), .tui-popup-wrapper";
$createForm.on('submit', function (e) {
e.preventDefault();
if ($createForm.valid()) {
abp.ui.setBusy();
$("#ViewModel_Style").val(styleEditor.getValue());
$("#ViewModel_Script").val(scriptEditor.getValue());
$createForm.ajaxSubmit({
success: function (result) {
abp.notify.success(l('SuccessfullySaved'));
abp.ui.clearBusy();
location.href = "../Pages";
},
error: function(result){
error: function (result) {
abp.ui.clearBusy();
abp.notify.error(result.responseJSON.error.message);
}

@ -7,6 +7,21 @@ $(function () {
$formUpdate.data('validator').settings.ignore = ":hidden, [contenteditable='true']:not([name]), .tui-popup-wrapper";
var scriptEditor = CodeMirror.fromTextArea(document.getElementById("ViewModel_Script"), {
mode: "javascript",
lineNumbers: true
});
var styleEditor = CodeMirror.fromTextArea(document.getElementById("ViewModel_Style"), {
mode: "css",
lineNumbers: true
});
$('.nav-tabs a').on('shown.bs.tab', function () {
scriptEditor.refresh();
styleEditor.refresh();
});
$formUpdate.on('submit', function (e) {
e.preventDefault();
@ -14,6 +29,9 @@ $(function () {
abp.ui.setBusy();
$("#ViewModel_Style").val(styleEditor.getValue());
$("#ViewModel_Script").val(scriptEditor.getValue());
$formUpdate.ajaxSubmit({
success: function (result) {
abp.notify.success(l('SuccessfullySaved'));

@ -378,7 +378,7 @@
volo.cmsKit.admin.blogs.blogPostAdmin.getList = function(input, ajaxParams) {
return abp.ajax($.extend(true, {
url: abp.appPath + 'api/cms-kit-admin/blogs/blog-posts' + abp.utils.buildQueryString([{ name: 'filter', value: input.filter }, { name: 'blogId', value: input.blogId }, { name: 'sorting', value: input.sorting }, { name: 'skipCount', value: input.skipCount }, { name: 'maxResultCount', value: input.maxResultCount }]) + '',
url: abp.appPath + 'api/cms-kit-admin/blogs/blog-posts' + abp.utils.buildQueryString([{ name: 'filter', value: input.filter }, { name: 'blogId', value: input.blogId }, { name: 'authorId', value: input.authorId }, { name: 'status', value: input.status }, { name: 'sorting', value: input.sorting }, { name: 'skipCount', value: input.skipCount }, { name: 'maxResultCount', value: input.maxResultCount }]) + '',
type: 'GET'
}, ajaxParams));
};
@ -391,6 +391,53 @@
}, ajaxParams));
};
volo.cmsKit.admin.blogs.blogPostAdmin.publish = function(id, ajaxParams) {
return abp.ajax($.extend(true, {
url: abp.appPath + 'api/cms-kit-admin/blogs/blog-posts/' + id + '/publish',
type: 'POST',
dataType: null
}, ajaxParams));
};
volo.cmsKit.admin.blogs.blogPostAdmin.draft = function(id, ajaxParams) {
return abp.ajax($.extend(true, {
url: abp.appPath + 'api/cms-kit-admin/blogs/blog-posts/' + id + '/draft',
type: 'POST',
dataType: null
}, ajaxParams));
};
volo.cmsKit.admin.blogs.blogPostAdmin.createAndPublish = function(input, ajaxParams) {
return abp.ajax($.extend(true, {
url: abp.appPath + 'api/cms-kit-admin/blogs/blog-posts/create-and-publish',
type: 'POST',
data: JSON.stringify(input)
}, ajaxParams));
};
volo.cmsKit.admin.blogs.blogPostAdmin.sendToReview = function(id, ajaxParams) {
return abp.ajax($.extend(true, {
url: abp.appPath + 'api/cms-kit-admin/blogs/blog-posts/' + id + '/send-to-review',
type: 'POST',
dataType: null
}, ajaxParams));
};
volo.cmsKit.admin.blogs.blogPostAdmin.createAndSendToReview = function(input, ajaxParams) {
return abp.ajax($.extend(true, {
url: abp.appPath + 'api/cms-kit-admin/blogs/blog-posts/create-and-send-to-review',
type: 'POST',
data: JSON.stringify(input)
}, ajaxParams));
};
volo.cmsKit.admin.blogs.blogPostAdmin.hasBlogPostWaitingForReview = function(ajaxParams) {
return abp.ajax($.extend(true, {
url: abp.appPath + 'api/cms-kit-admin/blogs/blog-posts/has-blogpost-waiting-for-review',
type: 'GET'
}, ajaxParams));
};
})();
})();

@ -0,0 +1,8 @@
namespace Volo.CmsKit.Blogs;
public enum BlogPostStatus
{
Draft,
Published,
WaitingForReview
}

@ -0,0 +1,17 @@
using JetBrains.Annotations;
using Volo.Abp.GlobalFeatures;
namespace Volo.CmsKit.GlobalFeatures;
[GlobalFeatureName(Name)]
public class BlogPostScrollIndexFeature : GlobalFeature
{
public const string Name = "CmsKit.BlogPost.ScrollIndex";
internal BlogPostScrollIndexFeature(
[NotNull] GlobalCmsKitFeatures cmsKit
) : base(cmsKit)
{
}
}

@ -26,6 +26,8 @@ public class GlobalCmsKitFeatures : GlobalModuleFeatures
public MenuFeature Menu => GetFeature<MenuFeature>();
public GlobalResourcesFeature GlobalResources => GetFeature<GlobalResourcesFeature>();
public BlogPostScrollIndexFeature BlogPostScrollIndex => GetFeature<BlogPostScrollIndexFeature>();
public GlobalCmsKitFeatures([NotNull] GlobalFeatureManager featureManager)
: base(featureManager)
@ -40,5 +42,6 @@ public class GlobalCmsKitFeatures : GlobalModuleFeatures
AddFeature(new CmsUserFeature(this));
AddFeature(new MenuFeature(this));
AddFeature(new GlobalResourcesFeature(this));
AddFeature(new BlogPostScrollIndexFeature(this));
}
}

@ -85,6 +85,7 @@
"Permission:BlogPostManagement.Create": "Create",
"Permission:BlogPostManagement.Delete": "Delete",
"Permission:BlogPostManagement.Update": "Update",
"Permission:BlogPostManagement.Publish": "Publish",
"Permission:CmsKit": "CmsKit",
"Permission:Comments": "Comment Management",
"Permission:Comments.Delete": "Delete",
@ -130,6 +131,7 @@
"SelectAll": "Select All",
"Send": "Send",
"SendMessage": "Send Message",
"SelectedAuthor": "Author",
"ShortDescription": "Short description",
"Slug": "Slug",
"Source": "Source",
@ -164,6 +166,23 @@
"GlobalResources": "Global Resources",
"Script": "Script",
"Style": "Style",
"SavedSuccessfully": "Saved successfully"
"SavedSuccessfully": "Saved successfully",
"CmsKit.BlogPost.Status.0": "Draft",
"CmsKit.BlogPost.Status.1": "Published",
"CmsKit.BlogPost.Status.2": "Waiting for review",
"BlogPostPublishConfirmationMessage": "Are you sure to publish the blog post \"{0}\"?",
"SuccessfullyPublished": "Successfully published!",
"Draft": "Draft",
"Publish": "Publish",
"BlogPostDraftConfirmationMessage": "Are you sure to set the blog post \"{0}\" as draft?",
"BlogPostSendToReviewConfirmationMessage": "Are you sure to send the blog post \"{0}\" to admin review for publishing?",
"SaveAsDraft": "Save as draft",
"SendToReview": "Send to review",
"SendToReviewToPublish": "Send to review to publish",
"BlogPostSendToReviewSuccessMessage": "The blog post \"{0}\" has been sent to admin review for publishing.",
"HasBlogPostWaitingForReviewMessage": "You have a blog post waiting for review. Click to list.",
"SelectAStatus": "Select a status",
"Status": "Status",
"CmsKit.BlogPost.ScrollIndex": "Quick navigation bar in blog posts"
}
}

@ -85,6 +85,7 @@
"Permission:BlogPostManagement.Create": "Oluşturma",
"Permission:BlogPostManagement.Delete": "Silme",
"Permission:BlogPostManagement.Update": "Güncelleme",
"Permission:BlogPostManagement.Publish": "Yayınlama",
"Permission:CmsKit": "CmsKit",
"Permission:Comments": "Yorum Yönetimi",
"Permission:Comments.Delete": "Silmek",
@ -129,6 +130,7 @@
"SelectAll": "Hepsini seç",
"Send": "Gönder",
"SendMessage": "Mesajı Gönder",
"SelectedAuthor": "Yazar",
"ShortDescription": "Kısa açıklama",
"Slug": "Etiket",
"Source": "Kaynak",
@ -163,6 +165,28 @@
"GlobalResources": "Global Kaynaklar",
"Script": "Script",
"Style": "Style",
"SavedSuccessfully": "Başarıyla kaydedildi"
"SavedSuccessfully": "Başarıyla kaydedildi",
"CmsKit.BlogPost.Status.0": "Taslak",
"CmsKit.BlogPost.Status.1": "Yayınlandı",
"CmsKit.BlogPost.Status.2": "İnceleme Bekliyor",
"BlogPostPublishConfirmationMessage": "\"{0}\" başlıklı gönderiyi yayınlamak istediğinize emin misiniz?",
"SuccessfullyPublished": "Başarıyla yayınlandı",
"Draft": "Taslak olarak kaydet",
"Publish": "Yayınla",
"BlogPostDraftConfirmationMessage": "\"{0}\" başlıklı gönderiyi taslak haline getirmek istediğinize emin misiniz?",
"BlogPostSendToReviewConfirmationMessage": "\"{0}\" başlıklı gönderiyi yayınlamak için admin incelemesine göndermek istediğinize emin misiniz?",
"SaveAsDraft": "Taslak olarak kaydet",
"SendToReview": "İncelemeye gönder",
"SendToReviewToPublish": "Yayınlamak için admin incelemesine gönder",
"BlogPostSendToReviewSuccessMessage": "\"{0}\" başlıklı gönderiyi yayınlamak için admin incelemesine gönderildi.",
"HasBlogPostWaitingForReviewMessage": "Yayınlanmak için onay bekleyen blog postlar var! Listelemek için tıklayın.",
"SelectAStatus": "Durum seçin",
"Status": "Durum",
"SelectAnAuthor": "Bir yazar seçin",
"GoToTop": "Yukarı Git",
"InThisDocument": "Bu belgede",
"RemoveCoverImageConfirmationMessage": "Kapak resmini kaldırmak istediğinize emin misiniz?",
"RemoveCoverImage": "Kapak resmini kaldır",
"CmsKit.BlogPost.ScrollIndex": "Blog yazılarında hızlı gezinme çubuğu"
}
}

@ -29,7 +29,9 @@ public class BlogPost : FullAuditedAggregateRoot<Guid>, IMultiTenant
public Guid AuthorId { get; set; }
public virtual CmsUser Author { get; set; }
public virtual BlogPostStatus Status { get; set; }
protected BlogPost()
{
}
@ -43,7 +45,9 @@ public class BlogPost : FullAuditedAggregateRoot<Guid>, IMultiTenant
[CanBeNull] string shortDescription = null,
[CanBeNull] string content = null,
[CanBeNull] Guid? coverImageMediaId = null,
[CanBeNull] Guid? tenantId = null) : base(id)
[CanBeNull] Guid? tenantId = null,
[CanBeNull] BlogPostStatus? state = null
) : base(id)
{
TenantId = tenantId;
BlogId = blogId;
@ -53,6 +57,7 @@ public class BlogPost : FullAuditedAggregateRoot<Guid>, IMultiTenant
SetShortDescription(shortDescription);
SetContent(content);
CoverImageMediaId = coverImageMediaId;
Status = state ?? BlogPostStatus.Draft;
}
public virtual void SetTitle(string title)
@ -76,4 +81,19 @@ public class BlogPost : FullAuditedAggregateRoot<Guid>, IMultiTenant
{
Content = Check.Length(content, nameof(content), BlogPostConsts.MaxContentLength);
}
public virtual void SetDraft()
{
Status = BlogPostStatus.Draft;
}
public virtual void SetPublished()
{
Status = BlogPostStatus.Published;
}
public virtual void SetWaitingForReview()
{
Status = BlogPostStatus.WaitingForReview;
}
}

@ -30,6 +30,7 @@ public class BlogPostManager : DomainService
[NotNull] Blog blog,
[NotNull] string title,
[NotNull] string slug,
[NotNull] BlogPostStatus status,
[CanBeNull] string shortDescription = null,
[CanBeNull] string content = null,
[CanBeNull] Guid? coverImageMediaId = null)
@ -40,17 +41,18 @@ public class BlogPostManager : DomainService
Check.NotNullOrEmpty(slug, nameof(slug));
var blogPost = new BlogPost(
GuidGenerator.Create(),
blog.Id,
author.Id,
title,
slug,
shortDescription,
content,
coverImageMediaId,
CurrentTenant.Id
);
GuidGenerator.Create(),
blog.Id,
author.Id,
title,
slug,
shortDescription,
content,
coverImageMediaId,
CurrentTenant.Id,
status
);
await CheckSlugExistenceAsync(blog.Id, blogPost.Slug);
return blogPost;

@ -16,6 +16,7 @@ public class DefaultBlogFeatureProvider : IDefaultBlogFeatureProvider, ITransien
new BlogFeature(blogId, ReactionsFeature.Name),
new BlogFeature(blogId, RatingsFeature.Name),
new BlogFeature(blogId, TagsFeature.Name),
new BlogFeature(blogId, BlogPostScrollIndexFeature.Name)
});
}
}

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;
@ -9,9 +10,9 @@ namespace Volo.CmsKit.Blogs;
public interface IBlogFeatureRepository : IBasicRepository<BlogFeature, Guid>
{
Task<List<BlogFeature>> GetListAsync(Guid blogId);
Task<List<BlogFeature>> GetListAsync(Guid blogId, CancellationToken cancellationToken = default);
Task<List<BlogFeature>> GetListAsync(Guid blogId, List<string> featureNames);
Task<List<BlogFeature>> GetListAsync(Guid blogId, List<string> featureNames, CancellationToken cancellationToken = default);
Task<BlogFeature> FindAsync(Guid blogId, string featureName);
Task<BlogFeature> FindAsync(Guid blogId, string featureName, CancellationToken cancellationToken = default);
}

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;
using Volo.CmsKit.Users;
namespace Volo.CmsKit.Blogs;
@ -11,11 +12,15 @@ public interface IBlogPostRepository : IBasicRepository<BlogPost, Guid>
Task<int> GetCountAsync(
string filter = null,
Guid? blogId = null,
Guid? authorId = null,
BlogPostStatus? statusFilter = null,
CancellationToken cancellationToken = default);
Task<List<BlogPost>> GetListAsync(
string filter = null,
Guid? blogId = null,
Guid? authorId = null,
BlogPostStatus? statusFilter = null,
int maxResultCount = int.MaxValue,
int skipCount = 0,
string sorting = null,
@ -24,4 +29,17 @@ public interface IBlogPostRepository : IBasicRepository<BlogPost, Guid>
Task<bool> SlugExistsAsync(Guid blogId, string slug, CancellationToken cancellationToken = default);
Task<BlogPost> GetBySlugAsync(Guid blogId, string slug, CancellationToken cancellationToken = default);
}
Task<List<CmsUser>> GetAuthorsHasBlogPostsAsync(
int skipCount,
int maxResultCount,
string sorting,
string filter,
CancellationToken cancellationToken = default);
Task<int> GetAuthorsHasBlogPostsCountAsync(string filter, CancellationToken cancellationToken = default);
Task<CmsUser> GetAuthorHasBlogPostAsync(Guid id, CancellationToken cancellationToken = default);
Task<bool> HasBlogPostWaitingForReviewAsync(CancellationToken cancellationToken = default);
}

@ -24,9 +24,13 @@ public interface ITagRepository : IBasicRepository<Tag, Guid>
[NotNull] string name,
CancellationToken cancellationToken = default);
Task<List<Tag>> GetListAsync(string filter);
Task<List<Tag>> GetListAsync(
string filter,
CancellationToken cancellationToken = default);
Task<int> GetCountAsync(string filter);
Task<int> GetCountAsync(
string filter,
CancellationToken cancellationToken = default);
Task<List<Tag>> GetAllRelatedTagsAsync(
[NotNull] string entityType,

@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
@ -15,22 +16,22 @@ public class EfCoreBlogFeatureRepository : EfCoreRepository<CmsKitDbContext, Blo
{
}
public Task<BlogFeature> FindAsync(Guid blogId, string featureName)
public Task<BlogFeature> FindAsync(Guid blogId, string featureName, CancellationToken cancellationToken = default)
{
return base.FindAsync(x => x.BlogId == blogId && x.FeatureName == featureName);
return base.FindAsync(x => x.BlogId == blogId && x.FeatureName == featureName, cancellationToken: cancellationToken);
}
public async Task<List<BlogFeature>> GetListAsync(Guid blogId)
public async Task<List<BlogFeature>> GetListAsync(Guid blogId, CancellationToken cancellationToken = default)
{
return await (await GetQueryableAsync())
.Where(x => x.BlogId == blogId)
.ToListAsync();
.ToListAsync(GetCancellationToken(cancellationToken));
}
public async Task<List<BlogFeature>> GetListAsync(Guid blogId, List<string> featureNames)
public async Task<List<BlogFeature>> GetListAsync(Guid blogId, List<string> featureNames, CancellationToken cancellationToken = default)
{
return await (await GetQueryableAsync())
.Where(x => x.BlogId == blogId && featureNames.Contains(x.FeatureName))
.ToListAsync();
.ToListAsync(GetCancellationToken(cancellationToken));
}
}

@ -7,6 +7,7 @@ using System.Linq.Dynamic.Core;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
using Volo.CmsKit.EntityFrameworkCore;
@ -41,11 +42,15 @@ public class EfCoreBlogPostRepository : EfCoreRepository<CmsKitDbContext, BlogPo
public virtual async Task<int> GetCountAsync(
string filter = null,
Guid? blogId = null,
Guid? authorId = null,
BlogPostStatus? statusFilter = null,
CancellationToken cancellationToken = default)
{
var queryable = (await GetDbSetAsync())
.WhereIf(blogId.HasValue, x => x.BlogId == blogId)
.WhereIf(!string.IsNullOrEmpty(filter), x => x.Title.Contains(filter) || x.Slug.Contains(filter));
.WhereIf(blogId.HasValue, x => x.BlogId == blogId)
.WhereIf(authorId.HasValue, x => x.AuthorId == authorId)
.WhereIf(statusFilter.HasValue, x => x.Status == statusFilter)
.WhereIf(!string.IsNullOrEmpty(filter), x => x.Title.Contains(filter) || x.Slug.Contains(filter));
var count = await queryable.CountAsync(GetCancellationToken(cancellationToken));
return count;
@ -54,6 +59,8 @@ public class EfCoreBlogPostRepository : EfCoreRepository<CmsKitDbContext, BlogPo
public virtual async Task<List<BlogPost>> GetListAsync(
string filter = null,
Guid? blogId = null,
Guid? authorId = null,
BlogPostStatus? statusFilter = null,
int maxResultCount = int.MaxValue,
int skipCount = 0,
string sorting = null,
@ -66,7 +73,9 @@ public class EfCoreBlogPostRepository : EfCoreRepository<CmsKitDbContext, BlogPo
var queryable = blogPostsDbSet
.WhereIf(blogId.HasValue, x => x.BlogId == blogId)
.WhereIf(!string.IsNullOrWhiteSpace(filter), x => x.Title.Contains(filter) || x.Slug.Contains(filter));
.WhereIf(!string.IsNullOrWhiteSpace(filter), x => x.Title.Contains(filter) || x.Slug.Contains(filter))
.WhereIf(authorId.HasValue, x => x.AuthorId == authorId)
.WhereIf(statusFilter.HasValue, x => x.Status == statusFilter);
queryable = queryable.OrderBy(sorting.IsNullOrEmpty() ? $"{nameof(BlogPost.CreationTime)} desc" : sorting);
@ -95,4 +104,38 @@ public class EfCoreBlogPostRepository : EfCoreRepository<CmsKitDbContext, BlogPo
return await (await GetDbSetAsync()).AnyAsync(x => x.BlogId == blogId && x.Slug.ToLower() == slug,
GetCancellationToken(cancellationToken));
}
public async Task<List<CmsUser>> GetAuthorsHasBlogPostsAsync(int skipCount, int maxResultCount, string sorting, string filter, CancellationToken cancellationToken = default)
{
return await (await CreateAuthorsQueryableAsync())
.Skip(skipCount)
.Take(maxResultCount)
.WhereIf(!filter.IsNullOrEmpty(), x => x.UserName.Contains(filter.ToLower()))
.OrderBy(sorting.IsNullOrEmpty() ? nameof(CmsUser.UserName) : sorting)
.ToListAsync(GetCancellationToken(cancellationToken));
}
public async Task<int> GetAuthorsHasBlogPostsCountAsync(string filter, CancellationToken cancellationToken = default)
{
return await (await CreateAuthorsQueryableAsync())
.WhereIf(!filter.IsNullOrEmpty(), x => x.UserName.Contains(filter.ToLower()))
.CountAsync(GetCancellationToken(cancellationToken));
}
public async Task<CmsUser> GetAuthorHasBlogPostAsync(Guid id, CancellationToken cancellationToken = default)
{
return await (await CreateAuthorsQueryableAsync()).FirstOrDefaultAsync(x => x.Id == id, GetCancellationToken(cancellationToken))
?? throw new EntityNotFoundException(typeof(CmsUser), id);
}
private async Task<IQueryable<CmsUser>> CreateAuthorsQueryableAsync()
{
return (await GetDbContextAsync()).BlogPosts.Select(x => x.Author).Distinct();
}
public virtual async Task<bool> HasBlogPostWaitingForReviewAsync(CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync())
.AnyAsync(x => x.Status == BlogPostStatus.WaitingForReview, GetCancellationToken(cancellationToken));
}
}

@ -80,14 +80,14 @@ public class EfCoreTagRepository : EfCoreRepository<ICmsKitDbContext, Tag, Guid>
return await query.ToListAsync(cancellationToken: GetCancellationToken(cancellationToken));
}
public async Task<List<Tag>> GetListAsync(string filter)
public async Task<List<Tag>> GetListAsync(string filter, CancellationToken cancellationToken = default)
{
return await (await GetQueryableByFilterAsync(filter)).ToListAsync();
return await (await GetQueryableByFilterAsync(filter)).ToListAsync(GetCancellationToken(cancellationToken));
}
public async Task<int> GetCountAsync(string filter)
public async Task<int> GetCountAsync(string filter, CancellationToken cancellationToken = default)
{
return await (await GetQueryableByFilterAsync(filter)).CountAsync();
return await (await GetQueryableByFilterAsync(filter)).CountAsync(GetCancellationToken(cancellationToken));
}
private async Task<IQueryable<Tag>> GetQueryableByFilterAsync(string filter)

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using MongoDB.Driver.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories.MongoDB;
using Volo.Abp.MongoDB;
@ -17,22 +18,22 @@ public class MongoBlogFeatureRepository : MongoDbRepository<ICmsKitMongoDbContex
{
}
public Task<BlogFeature> FindAsync(Guid blogId, string featureName)
public Task<BlogFeature> FindAsync(Guid blogId, string featureName, CancellationToken cancellationToken = default)
{
return base.FindAsync(x => x.BlogId == blogId && x.FeatureName == featureName);
return base.FindAsync(x => x.BlogId == blogId && x.FeatureName == featureName, cancellationToken: cancellationToken);
}
public virtual async Task<List<BlogFeature>> GetListAsync(Guid blogId)
public virtual async Task<List<BlogFeature>> GetListAsync(Guid blogId, CancellationToken cancellationToken = default)
{
return await (await GetMongoQueryableAsync())
return await (await GetMongoQueryableAsync(cancellationToken))
.Where(x => x.BlogId == blogId)
.ToListAsync();
.ToListAsync(GetCancellationToken(cancellationToken));
}
public virtual async Task<List<BlogFeature>> GetListAsync(Guid blogId, List<string> featureNames)
public virtual async Task<List<BlogFeature>> GetListAsync(Guid blogId, List<string> featureNames, CancellationToken cancellationToken = default)
{
return await (await GetMongoQueryableAsync())
return await (await GetMongoQueryableAsync(cancellationToken))
.Where(x => x.BlogId == blogId && featureNames.Contains(x.FeatureName))
.ToListAsync();
.ToListAsync(GetCancellationToken(cancellationToken));
}
}

@ -8,6 +8,7 @@ using System.Linq.Dynamic.Core;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories.MongoDB;
using Volo.Abp.MongoDB;
using Volo.CmsKit.Blogs;
@ -42,33 +43,41 @@ public class MongoBlogPostRepository : MongoDbRepository<CmsKitMongoDbContext, B
public virtual async Task<int> GetCountAsync(
string filter = null,
Guid? blogId = null,
Guid? authorId = null,
BlogPostStatus? statusFilter = null,
CancellationToken cancellationToken = default)
{
var token = GetCancellationToken(cancellationToken);
cancellationToken = GetCancellationToken(cancellationToken);
return await (await GetMongoQueryableAsync(token))
return await (await GetMongoQueryableAsync(cancellationToken))
.WhereIf<BlogPost, IMongoQueryable<BlogPost>>(!string.IsNullOrWhiteSpace(filter), x => x.Title.Contains(filter) || x.Slug.Contains(filter))
.WhereIf<BlogPost, IMongoQueryable<BlogPost>>(blogId.HasValue, x => x.BlogId == blogId)
.CountAsync(GetCancellationToken(cancellationToken));
.WhereIf<BlogPost, IMongoQueryable<BlogPost>>(authorId.HasValue, x => x.AuthorId == authorId)
.WhereIf<BlogPost, IMongoQueryable<BlogPost>>(statusFilter.HasValue, x => x.Status == statusFilter)
.CountAsync(cancellationToken);
}
public virtual async Task<List<BlogPost>> GetListAsync(
string filter = null,
Guid? blogId = null,
Guid? authorId = null,
BlogPostStatus? statusFilter = null,
int maxResultCount = int.MaxValue,
int skipCount = 0,
string sorting = null,
CancellationToken cancellationToken = default)
{
var token = GetCancellationToken(cancellationToken);
var dbContext = await GetDbContextAsync(token);
cancellationToken = GetCancellationToken(cancellationToken);
var dbContext = await GetDbContextAsync(cancellationToken);
var blogPostQueryable = await GetQueryableAsync();
var usersQueryable = dbContext.Collection<CmsUser>().AsQueryable();
var queryable = blogPostQueryable
.WhereIf(blogId.HasValue, x => x.BlogId == blogId)
.WhereIf(!string.IsNullOrWhiteSpace(filter), x => x.Title.Contains(filter) || x.Slug.Contains(filter));
.WhereIf(!string.IsNullOrWhiteSpace(filter), x => x.Title.Contains(filter) || x.Slug.Contains(filter))
.WhereIf(authorId.HasValue, x => x.AuthorId == authorId)
.WhereIf(statusFilter.HasValue, x => x.Status == statusFilter);
queryable = queryable.OrderBy(sorting.IsNullOrEmpty() ? $"{nameof(BlogPost.CreationTime)} desc" : sorting);
@ -81,7 +90,7 @@ public class MongoBlogPostRepository : MongoDbRepository<CmsKitMongoDbContext, B
.Skip(skipCount)
.Take(maxResultCount);
var combinedResult = await AsyncExecuter.ToListAsync(combinedQueryable, GetCancellationToken(cancellationToken));
var combinedResult = await AsyncExecuter.ToListAsync(combinedQueryable, cancellationToken);
return combinedResult.Select(s =>
{
@ -95,8 +104,59 @@ public class MongoBlogPostRepository : MongoDbRepository<CmsKitMongoDbContext, B
{
Check.NotNullOrEmpty(slug, nameof(slug));
var token = GetCancellationToken(cancellationToken);
var queryable = await GetMongoQueryableAsync(token);
return await queryable.AnyAsync(x => x.BlogId == blogId && x.Slug.ToLower() == slug, token);
cancellationToken = GetCancellationToken(cancellationToken);
var queryable = await GetMongoQueryableAsync(cancellationToken);
return await queryable.AnyAsync(x => x.BlogId == blogId && x.Slug.ToLower() == slug, cancellationToken);
}
public async Task<List<CmsUser>> GetAuthorsHasBlogPostsAsync(int skipCount, int maxResultCount, string sorting, string filter, CancellationToken cancellationToken = default)
{
var queryable = (await CreateAuthorsQueryableAsync(cancellationToken))
.Skip(skipCount)
.Take(maxResultCount)
.OrderBy(sorting.IsNullOrEmpty() ? nameof(CmsUser.UserName) : sorting)
.WhereIf(!filter.IsNullOrEmpty(), x => x.UserName.Contains(filter.ToLower()));
return await AsyncExecuter.ToListAsync(queryable, GetCancellationToken(cancellationToken));
}
public async Task<int> GetAuthorsHasBlogPostsCountAsync(string filter, CancellationToken cancellationToken = default)
{
return await AsyncExecuter.CountAsync(
(await CreateAuthorsQueryableAsync(cancellationToken))
.WhereIf(!filter.IsNullOrEmpty(), x => x.UserName.Contains(filter.ToLower())));
}
public async Task<CmsUser> GetAuthorHasBlogPostAsync(Guid id, CancellationToken cancellationToken = default)
{
return await AsyncExecuter.FirstOrDefaultAsync(await CreateAuthorsQueryableAsync(cancellationToken), x => x.Id == id)
?? throw new EntityNotFoundException(typeof(CmsUser), id);
}
private async Task<IQueryable<CmsUser>> CreateAuthorsQueryableAsync(CancellationToken cancellationToken = default)
{
cancellationToken = GetCancellationToken(cancellationToken);
var blogPostQueryable = (await GetQueryableAsync())
.Where(x => x.Status == BlogPostStatus.Published);
var usersQueryable = (await GetDbContextAsync(cancellationToken)).Collection<CmsUser>().AsQueryable();
return blogPostQueryable
.Join(
usersQueryable,
o => o.AuthorId,
i => i.Id,
(blogPost, user) => new { blogPost, user })
.Select(s => s.user)
.Distinct();
}
public virtual async Task<bool> HasBlogPostWaitingForReviewAsync(CancellationToken cancellationToken = default)
{
cancellationToken = GetCancellationToken(cancellationToken);
return await (await GetMongoQueryableAsync(cancellationToken))
.AnyAsync(x => x.Status == BlogPostStatus.WaitingForReview, cancellationToken);
}
}

@ -66,7 +66,7 @@ public class MongoBlogRepository : MongoDbRepository<ICmsKitMongoDbContext, Blog
protected virtual async Task<IQueryable<Blog>> GetListQueryAsync(string filter = null, CancellationToken cancellationToken = default)
{
return (await GetMongoQueryableAsync(GetCancellationToken(cancellationToken)))
return (await GetMongoQueryableAsync(cancellationToken))
.WhereIf(!filter.IsNullOrWhiteSpace(), b => b.Name.Contains(filter));
}
}

@ -82,19 +82,19 @@ public class MongoTagRepository : MongoDbRepository<ICmsKitMongoDbContext, Volo.
}
public async Task<List<Tag>> GetListAsync(string filter)
public async Task<List<Tag>> GetListAsync(string filter, CancellationToken cancellationToken = default)
{
return await (await GetQueryableByFilterAsync(filter)).ToListAsync();
return await (await GetQueryableByFilterAsync(filter, cancellationToken)).ToListAsync(GetCancellationToken(cancellationToken));
}
public async Task<int> GetCountAsync(string filter)
public async Task<int> GetCountAsync(string filter, CancellationToken cancellationToken = default)
{
return await (await GetQueryableByFilterAsync(filter)).CountAsync();
return await (await GetQueryableByFilterAsync(filter, cancellationToken)).CountAsync(GetCancellationToken(cancellationToken));
}
private async Task<IMongoQueryable<Tag>> GetQueryableByFilterAsync(string filter)
private async Task<IMongoQueryable<Tag>> GetQueryableByFilterAsync(string filter, CancellationToken cancellationToken = default)
{
var mongoQueryable = await GetMongoQueryableAsync();
var mongoQueryable = await GetMongoQueryableAsync(cancellationToken: cancellationToken);
if (!filter.IsNullOrWhiteSpace())
{

@ -0,0 +1,8 @@
using Volo.Abp.Application.Dtos;
namespace Volo.CmsKit.Public.Blogs;
public class BlogPostFilteredPagedAndSortedResultRequestDto : PagedAndSortedResultRequestDto
{
public string Filter { get; set; }
}

@ -0,0 +1,9 @@
using System;
using Volo.Abp.Application.Dtos;
namespace Volo.CmsKit.Public.Blogs;
public class BlogPostGetListInput : PagedAndSortedResultRequestDto
{
public Guid? AuthorId { get; set; }
}

@ -1,13 +1,20 @@
using JetBrains.Annotations;
using System.Collections.Generic;
using JetBrains.Annotations;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.CmsKit.Users;
using System;
namespace Volo.CmsKit.Public.Blogs;
public interface IBlogPostPublicAppService : IApplicationService
{
Task<PagedResultDto<BlogPostPublicDto>> GetListAsync([NotNull] string blogSlug, PagedAndSortedResultRequestDto input);
Task<PagedResultDto<BlogPostPublicDto>> GetListAsync([NotNull] string blogSlug, BlogPostGetListInput input);
Task<BlogPostPublicDto> GetAsync([NotNull] string blogSlug, [NotNull] string blogPostSlug);
Task<PagedResultDto<CmsUserDto>> GetAuthorsHasBlogPostsAsync(BlogPostFilteredPagedAndSortedResultRequestDto input);
Task<CmsUserDto> GetAuthorHasBlogPostAsync(Guid id);
}

@ -1,10 +1,12 @@
using JetBrains.Annotations;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.GlobalFeatures;
using Volo.CmsKit.Blogs;
using Volo.CmsKit.GlobalFeatures;
using Volo.CmsKit.Users;
namespace Volo.CmsKit.Public.Blogs;
@ -32,14 +34,32 @@ public class BlogPostPublicAppService : CmsKitPublicAppServiceBase, IBlogPostPub
return ObjectMapper.Map<BlogPost, BlogPostPublicDto>(blogPost);
}
public virtual async Task<PagedResultDto<BlogPostPublicDto>> GetListAsync([NotNull] string blogSlug, PagedAndSortedResultRequestDto input)
public virtual async Task<PagedResultDto<BlogPostPublicDto>> GetListAsync([NotNull] string blogSlug, BlogPostGetListInput input)
{
var blog = await BlogRepository.GetBySlugAsync(blogSlug);
var blogPosts = await BlogPostRepository.GetListAsync(null, blog.Id, input.MaxResultCount, input.SkipCount, input.Sorting);
var blogPosts = await BlogPostRepository.GetListAsync(null, blog.Id, input.AuthorId, BlogPostStatus.Published, input.MaxResultCount,
input.SkipCount, input.Sorting);
return new PagedResultDto<BlogPostPublicDto>(
await BlogPostRepository.GetCountAsync(blogId: blog.Id),
await BlogPostRepository.GetCountAsync(blogId: blog.Id, statusFilter: BlogPostStatus.Published, authorId: input.AuthorId),
ObjectMapper.Map<List<BlogPost>, List<BlogPostPublicDto>>(blogPosts));
}
public virtual async Task<PagedResultDto<CmsUserDto>> GetAuthorsHasBlogPostsAsync(BlogPostFilteredPagedAndSortedResultRequestDto input)
{
var authors = await BlogPostRepository.GetAuthorsHasBlogPostsAsync(input.SkipCount, input.MaxResultCount, input.Sorting, input.Filter);
var authorDtos = ObjectMapper.Map<List<CmsUser>, List<CmsUserDto>>(authors);
return new PagedResultDto<CmsUserDto>(
await BlogPostRepository.GetAuthorsHasBlogPostsCountAsync(input.Filter),
authorDtos);
}
public async Task<CmsUserDto> GetAuthorHasBlogPostAsync(Guid id)
{
var author = await BlogPostRepository.GetAuthorHasBlogPostAsync(id);
return ObjectMapper.Map<CmsUser, CmsUserDto>(author);
}
}

@ -7,6 +7,7 @@ using Volo.Abp.Http.Modeling;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client.ClientProxying;
using Volo.CmsKit.Public.Blogs;
using Volo.CmsKit.Users;
// ReSharper disable once CheckNamespace
namespace Volo.CmsKit.Public.Blogs.ClientProxies;
@ -24,12 +25,28 @@ public partial class BlogPostPublicClientProxy : ClientProxyBase<IBlogPostPublic
});
}
public virtual async Task<PagedResultDto<BlogPostPublicDto>> GetListAsync(string blogSlug, PagedAndSortedResultRequestDto input)
public virtual async Task<PagedResultDto<BlogPostPublicDto>> GetListAsync(string blogSlug, BlogPostGetListInput input)
{
return await RequestAsync<PagedResultDto<BlogPostPublicDto>>(nameof(GetListAsync), new ClientProxyRequestTypeValue
{
{ typeof(string), blogSlug },
{ typeof(PagedAndSortedResultRequestDto), input }
{ typeof(BlogPostGetListInput), input }
});
}
public virtual async Task<PagedResultDto<CmsUserDto>> GetAuthorsHasBlogPostsAsync(BlogPostFilteredPagedAndSortedResultRequestDto input)
{
return await RequestAsync<PagedResultDto<CmsUserDto>>(nameof(GetAuthorsHasBlogPostsAsync), new ClientProxyRequestTypeValue
{
{ typeof(BlogPostFilteredPagedAndSortedResultRequestDto), input }
});
}
public virtual async Task<CmsUserDto> GetAuthorHasBlogPostAsync(Guid id)
{
return await RequestAsync<CmsUserDto>(nameof(GetAuthorHasBlogPostAsync), new ClientProxyRequestTypeValue
{
{ typeof(Guid), id }
});
}
}

@ -957,9 +957,9 @@
},
{
"name": "input",
"typeAsString": "Volo.Abp.Application.Dtos.PagedAndSortedResultRequestDto, Volo.Abp.Ddd.Application.Contracts",
"type": "Volo.Abp.Application.Dtos.PagedAndSortedResultRequestDto",
"typeSimple": "Volo.Abp.Application.Dtos.PagedAndSortedResultRequestDto",
"typeAsString": "Volo.CmsKit.Public.Blogs.BlogPostGetListInput, Volo.CmsKit.Public.Application.Contracts",
"type": "Volo.CmsKit.Public.Blogs.BlogPostGetListInput",
"typeSimple": "Volo.CmsKit.Public.Blogs.BlogPostGetListInput",
"isOptional": false,
"defaultValue": null
}
@ -977,6 +977,30 @@
"bindingSourceId": "Path",
"descriptorName": ""
},
{
"nameOnMethod": "input",
"name": "AuthorId",
"jsonName": null,
"type": "System.Guid?",
"typeSimple": "string?",
"isOptional": false,
"defaultValue": null,
"constraintTypes": null,
"bindingSourceId": "ModelBinding",
"descriptorName": "input"
},
{
"nameOnMethod": "input",
"name": "Sorting",
"jsonName": null,
"type": "System.String",
"typeSimple": "string",
"isOptional": false,
"defaultValue": null,
"constraintTypes": null,
"bindingSourceId": "ModelBinding",
"descriptorName": "input"
},
{
"nameOnMethod": "input",
"name": "SkipCount",
@ -1000,6 +1024,43 @@
"constraintTypes": null,
"bindingSourceId": "ModelBinding",
"descriptorName": "input"
}
],
"returnValue": {
"type": "Volo.Abp.Application.Dtos.PagedResultDto<Volo.CmsKit.Public.Blogs.BlogPostPublicDto>",
"typeSimple": "Volo.Abp.Application.Dtos.PagedResultDto<Volo.CmsKit.Public.Blogs.BlogPostPublicDto>"
},
"allowAnonymous": null,
"implementFrom": "Volo.CmsKit.Public.Blogs.IBlogPostPublicAppService"
},
"GetAuthorsHasBlogPostsAsyncByInput": {
"uniqueName": "GetAuthorsHasBlogPostsAsyncByInput",
"name": "GetAuthorsHasBlogPostsAsync",
"httpMethod": "GET",
"url": "api/cms-kit-public/blog-posts/authors",
"supportedVersions": [],
"parametersOnMethod": [
{
"name": "input",
"typeAsString": "Volo.CmsKit.Public.Blogs.BlogPostFilteredPagedAndSortedResultRequestDto, Volo.CmsKit.Public.Application.Contracts",
"type": "Volo.CmsKit.Public.Blogs.BlogPostFilteredPagedAndSortedResultRequestDto",
"typeSimple": "Volo.CmsKit.Public.Blogs.BlogPostFilteredPagedAndSortedResultRequestDto",
"isOptional": false,
"defaultValue": null
}
],
"parameters": [
{
"nameOnMethod": "input",
"name": "Filter",
"jsonName": null,
"type": "System.String",
"typeSimple": "string",
"isOptional": false,
"defaultValue": null,
"constraintTypes": null,
"bindingSourceId": "ModelBinding",
"descriptorName": "input"
},
{
"nameOnMethod": "input",
@ -1012,11 +1073,72 @@
"constraintTypes": null,
"bindingSourceId": "ModelBinding",
"descriptorName": "input"
},
{
"nameOnMethod": "input",
"name": "SkipCount",
"jsonName": null,
"type": "System.Int32",
"typeSimple": "number",
"isOptional": false,
"defaultValue": null,
"constraintTypes": null,
"bindingSourceId": "ModelBinding",
"descriptorName": "input"
},
{
"nameOnMethod": "input",
"name": "MaxResultCount",
"jsonName": null,
"type": "System.Int32",
"typeSimple": "number",
"isOptional": false,
"defaultValue": null,
"constraintTypes": null,
"bindingSourceId": "ModelBinding",
"descriptorName": "input"
}
],
"returnValue": {
"type": "Volo.Abp.Application.Dtos.PagedResultDto<Volo.CmsKit.Public.Blogs.BlogPostPublicDto>",
"typeSimple": "Volo.Abp.Application.Dtos.PagedResultDto<Volo.CmsKit.Public.Blogs.BlogPostPublicDto>"
"type": "Volo.Abp.Application.Dtos.PagedResultDto<Volo.CmsKit.Users.CmsUserDto>",
"typeSimple": "Volo.Abp.Application.Dtos.PagedResultDto<Volo.CmsKit.Users.CmsUserDto>"
},
"allowAnonymous": null,
"implementFrom": "Volo.CmsKit.Public.Blogs.IBlogPostPublicAppService"
},
"GetAuthorHasBlogPostAsyncById": {
"uniqueName": "GetAuthorHasBlogPostAsyncById",
"name": "GetAuthorHasBlogPostAsync",
"httpMethod": "GET",
"url": "api/cms-kit-public/blog-posts/authors/{id}",
"supportedVersions": [],
"parametersOnMethod": [
{
"name": "id",
"typeAsString": "System.Guid, System.Private.CoreLib",
"type": "System.Guid",
"typeSimple": "string",
"isOptional": false,
"defaultValue": null
}
],
"parameters": [
{
"nameOnMethod": "id",
"name": "id",
"jsonName": null,
"type": "System.Guid",
"typeSimple": "string",
"isOptional": false,
"defaultValue": null,
"constraintTypes": [],
"bindingSourceId": "Path",
"descriptorName": ""
}
],
"returnValue": {
"type": "Volo.CmsKit.Users.CmsUserDto",
"typeSimple": "Volo.CmsKit.Users.CmsUserDto"
},
"allowAnonymous": null,
"implementFrom": "Volo.CmsKit.Public.Blogs.IBlogPostPublicAppService"

@ -1,11 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
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.Users;
namespace Volo.CmsKit.Public.Blogs;
@ -31,8 +33,22 @@ public class BlogPostPublicController : CmsKitPublicControllerBase, IBlogPostPub
[HttpGet]
[Route("{blogSlug}")]
public virtual Task<PagedResultDto<BlogPostPublicDto>> GetListAsync(string blogSlug, PagedAndSortedResultRequestDto input)
public virtual Task<PagedResultDto<BlogPostPublicDto>> GetListAsync(string blogSlug, BlogPostGetListInput input)
{
return BlogPostPublicAppService.GetListAsync(blogSlug, input);
}
}
[HttpGet]
[Route("authors")]
public Task<PagedResultDto<CmsUserDto>> GetAuthorsHasBlogPostsAsync(BlogPostFilteredPagedAndSortedResultRequestDto input)
{
return BlogPostPublicAppService.GetAuthorsHasBlogPostsAsync(input);
}
[HttpGet]
[Route("authors/{id}")]
public Task<CmsUserDto> GetAuthorHasBlogPostAsync(Guid id)
{
return BlogPostPublicAppService.GetAuthorHasBlogPostAsync(id);
}
}

@ -16,92 +16,133 @@
@inject IMarkdownToHtmlRenderer MarkdownRenderer
@{
string dummyImageSource = "https://dummyimage.com/1280x720/a3a3a3/fff.png?text=" + Model.BlogPost.Title;
var isScrollIndexEnabled = GlobalFeatureManager.Instance.IsEnabled<BlogPostScrollIndexFeature>() && Model.BlogPostScrollIndexFeature?.IsEnabled == true;
}
@section styles{
<abp-abp-style-bundle>
<abp-style src="/Pages/Public/CmsKit/Blogs/blogPost.css" />
<abp-style type="typeof(HighlightJsStyleContributor)" />
</abp-abp-style-bundle>
@if (isScrollIndexEnabled)
{
<abp-abp-style-bundle>
<abp-style src="/Pages/Public/CmsKit/Blogs/bootstrap-toc.css"/>
</abp-abp-style-bundle>
}
<abp-abp-style-bundle>
<abp-style src="/Pages/Public/CmsKit/Blogs/blogPost.css"/>
<abp-style type="typeof(HighlightJsStyleContributor)"/>
</abp-abp-style-bundle>
}
@section scripts{
<abp-script-bundle>
<abp-script type="typeof(HighlightJsScriptContributor)" />
<abp-script src="/Pages/Public/CmsKit/highlightOnLoad.js" />
</abp-script-bundle>
}
@if (isScrollIndexEnabled)
{
<abp-script-bundle>
<abp-style src="/Pages/Public/CmsKit/Blogs/bootstrap-toc.js"/>
<abp-script src="/Pages/Public/CmsKit/Blogs/blogpost-scroll-index.js"/>
</abp-script-bundle>
}
@{
string dummyImageSource = "https://dummyimage.com/1280x720/a3a3a3/fff.png?text=" + Model.BlogPost.Title;
<abp-script-bundle>
<abp-script type="typeof(HighlightJsScriptContributor)"/>
<abp-script src="/Pages/Public/CmsKit/highlightOnLoad.js"/>
</abp-script-bundle>
}
<abp-card class="mb-4">
<img src="/api/cms-kit/media/@Model.BlogPost.CoverImageMediaId" class="card-img-top" onerror="this.src='@dummyImageSource'" />
<abp-card-body>
<abp-row>
<div class="col-lg-8 col-md-10 mx-auto pb-4">
<h1 class="mt-lg-4 mt-md-3">@Model.BlogPost.Title</h1>
<p class="mb-lg-5 mb-md-3">
<span class="font-weight-bold">@@@Model.BlogPost.Author?.UserName</span>
<small style="opacity:.65;">@Model.BlogPost.CreationTime</small>
</p>
@if(!Model.BlogPost.Content.IsNullOrEmpty())
{
@Html.Raw(await MarkdownRenderer.RenderAsync(Model.BlogPost.Content))
}
<p class="mb-3">
@if (Model.BlogPost.LastModificationTime != null)
{
<small style="opacity:.65;">@L["LastModification"].Value : @Model.BlogPost.LastModificationTime</small>
}
</p>
<hr />
@if (GlobalFeatureManager.Instance.IsEnabled<TagsFeature>())
{
if (Model.TagsFeature?.IsEnabled == true)
{
@await Component.InvokeAsync(typeof(TagViewComponent), new
{
entityType = Volo.CmsKit.Blogs.BlogPostConsts.EntityType,
entityId = Model.BlogPost.Id.ToString()
})
}
}
</div>
</abp-row>
<abp-row class="row">
<abp-column size-lg="_6" size-md="_12">
@if (GlobalFeatureManager.Instance.IsEnabled<ReactionsFeature>())
{
if (Model.ReactionsFeature?.IsEnabled == true)
{
@await Component.InvokeAsync(typeof(ReactionSelectionViewComponent), new
<div class="row">
<div @Html.Raw(isScrollIndexEnabled ? "class=\"col-md-10 col-sm-12\"" : "class=\"col-md-12\"")>
<abp-card class="mb-4">
<img src="/api/cms-kit/media/@Model.BlogPost.CoverImageMediaId" class="card-img-top" onerror="this.src='@dummyImageSource'"/>
<abp-card-body>
<abp-row>
<div class="col-lg-8 col-md-10 mx-auto pb-4">
<h1 class="mt-lg-4 mt-md-3">@Model.BlogPost.Title</h1>
<p class="mb-lg-5 mb-md-3">
<a href="/blogs/@Model.BlogSlug?authorId=@Model.BlogPost.Author.Id">
<span class="font-weight-bold">@@@Model.BlogPost.Author?.UserName</span>
</a>
<small style="opacity:.65;">@Model.BlogPost.CreationTime</small>
</p>
@if (!Model.BlogPost.Content.IsNullOrEmpty())
{
@Html.Raw(await MarkdownRenderer.RenderAsync(Model.BlogPost.Content))
}
<p class="mb-3">
@if (Model.BlogPost.LastModificationTime != null)
{
<small style="opacity:.65;">@L["LastModification"].Value : @Model.BlogPost.LastModificationTime</small>
}
</p>
<hr/>
@if (GlobalFeatureManager.Instance.IsEnabled<TagsFeature>())
{
if (Model.TagsFeature?.IsEnabled == true)
{
@await Component.InvokeAsync(typeof(TagViewComponent), new
{
entityType = Volo.CmsKit.Blogs.BlogPostConsts.EntityType,
entityId = Model.BlogPost.Id.ToString()
})
}
}
</div>
</abp-row>
<abp-row class="row">
<abp-column size-lg="_6" size-md="_12">
@if (GlobalFeatureManager.Instance.IsEnabled<ReactionsFeature>())
{
entityType = Volo.CmsKit.Blogs.BlogPostConsts.EntityType,
entityId = Model.BlogPost.Id.ToString()
})
}
}
</abp-column>
<abp-column size-lg="_6" size-md="_12">
@if (GlobalFeatureManager.Instance.IsEnabled<RatingsFeature>())
{
if (Model.RatingsFeature?.IsEnabled == true)
{
@await Component.InvokeAsync(typeof(RatingViewComponent), new
if (Model.ReactionsFeature?.IsEnabled == true)
{
@await Component.InvokeAsync(typeof(ReactionSelectionViewComponent), new
{
entityType = Volo.CmsKit.Blogs.BlogPostConsts.EntityType,
entityId = Model.BlogPost.Id.ToString()
})
}
}
</abp-column>
<abp-column size-lg="_6" size-md="_12">
@if (GlobalFeatureManager.Instance.IsEnabled<RatingsFeature>())
{
entityType = Volo.CmsKit.Blogs.BlogPostConsts.EntityType,
entityId = Model.BlogPost.Id.ToString()
})
}
}
</abp-column>
</abp-row>
</abp-card-body>
</abp-card>
if (Model.RatingsFeature?.IsEnabled == true)
{
@await Component.InvokeAsync(typeof(RatingViewComponent), new
{
entityType = Volo.CmsKit.Blogs.BlogPostConsts.EntityType,
entityId = Model.BlogPost.Id.ToString()
})
}
}
</abp-column>
</abp-row>
</abp-card-body>
</abp-card>
</div>
@if (isScrollIndexEnabled)
{
<div class="col-md-2 d-sm-none d-md-block">
<div id="scroll-index" class="docs-inner-anchors mt-2">
<h5>@L["InThisDocument"]</h5>
<nav id="blog-post-sticky-index" class="navbar index-scroll pt-0">
</nav>
<div class="row">
<div class="col p-0 py-3">
<a href="#" class="scroll-top-btn">
<i class="fa fa-chevron-up"></i> @L["GoToTop"]
</a>
</div>
</div>
</div>
</div>
}
</div>
@if (GlobalFeatureManager.Instance.IsEnabled<CommentsFeature>())
{

@ -30,6 +30,8 @@ public class BlogPostModel : CmsKitPublicPageModelBase
public BlogFeatureDto RatingsFeature { get; private set; }
public BlogFeatureDto TagsFeature { get; private set; }
public BlogFeatureDto BlogPostScrollIndexFeature { get; private set; }
protected IBlogPostPublicAppService BlogPostPublicAppService { get; }
@ -66,5 +68,10 @@ public class BlogPostModel : CmsKitPublicPageModelBase
{
TagsFeature = await BlogFeatureAppService.GetOrDefaultAsync(BlogPost.BlogId, GlobalFeatures.TagsFeature.Name);
}
if (GlobalFeatureManager.Instance.IsEnabled<BlogPostScrollIndexFeature>())
{
BlogPostScrollIndexFeature = await BlogFeatureAppService.GetOrDefaultAsync(BlogPost.BlogId, GlobalFeatures.BlogPostScrollIndexFeature.Name);
}
}
}

@ -8,12 +8,42 @@
@model IndexModel
@section styles{
<abp-style src="/Pages/Public/CmsKit/Blogs/index.css" />
<abp-style src="/Pages/Public/CmsKit/Blogs/index.css"/>
}
@section scripts {
<abp-script-bundle>
<abp-script src="/Pages/Public/CmsKit/Blogs/index.js"/>
</abp-script-bundle>
}
@{
const string dummyImageSource = "https://dummyimage.com/320x180/a3a3a3/fff.png";
}
<abp-row id="blogs-filter-area">
<abp-column size="_4">
<div class="mb-3">
<label class="form-label" asp-for="@Model.SelectedAuthor"></label>
<select id="AuthorSelect" asp-for="@Model.AuthorId"
class="auto-complete-select"
data-placeholder="@L["SelectAnAuthor"]"
data-allow-clear="true"
data-autocomplete-api-url="/api/cms-kit-public/blog-posts/authors"
data-autocomplete-display-property="userName"
data-autocomplete-value-property="id"
data-autocomplete-items-property="items"
data-autocomplete-filter-param-name="filter">
@if(Model.SelectedAuthor != null)
{
<option selected value="@Model.AuthorId" selected="selected">@Model.SelectedAuthor.UserName</option>
}
</select>
</div>
</abp-column>
</abp-row>
<abp-row id="blogs-container">
@foreach (var blog in Model.Blogs.Items)
{
@ -21,16 +51,16 @@
<abp-card>
@if (blog.CoverImageMediaId != null)
{
<img src="/api/cms-kit/media/@blog.CoverImageMediaId" class="card-img-top" onerror="this.src='@dummyImageSource'" />
<img src="/api/cms-kit/media/@blog.CoverImageMediaId" class="card-img-top" onerror="this.src='@dummyImageSource'"/>
}
else
{
<img src="@(dummyImageSource)?text=@blog.Title" class="card-img-top" />
<img src="@(dummyImageSource)?text=@blog.Title" class="card-img-top"/>
}
<abp-card-body class="p-4">
<h5>@blog.Title</h5>
<p class="mb-2">
<span class="font-weight-bold">@@@blog.Author?.UserName</span>
<span class="font-weight-bold author-name-span" data-author-id="@blog.Author.Id">@@@blog.Author?.UserName</span>
<small style="opacity:.65;">@blog.CreationTime</small>
</p>
<p style="min-height: 60px;">@blog.ShortDescription</p>
@ -42,11 +72,10 @@
</abp-card-body>
</abp-card>
</abp-column>
}
</abp-row>
<abp-row>
<abp-column>
<abp-paginator model="Model.PagerModel" />
<abp-paginator model="Model.PagerModel"/>
</abp-column>
</abp-row>
</abp-row>

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination;
using Volo.CmsKit.Public.Blogs;
using Volo.CmsKit.Users;
namespace Volo.CmsKit.Public.Web.Pages.Public.CmsKit.Blogs;
@ -20,10 +21,15 @@ public class IndexModel : CmsKitPublicPageModelBase
[BindProperty(SupportsGet = true)]
public int CurrentPage { get; set; } = 1;
[BindProperty(SupportsGet = true)]
public Guid? AuthorId { get; set; }
public PagedResultDto<BlogPostPublicDto> Blogs { get; private set; }
public PagerModel PagerModel => new PagerModel(Blogs.TotalCount, Blogs.Items.Count, CurrentPage, PageSize, Request.Path.ToString());
public CmsUserDto SelectedAuthor { get; set; }
protected IBlogPostPublicAppService BlogPostPublicAppService { get; }
public IndexModel(IBlogPostPublicAppService blogPostPublicAppService)
@ -35,10 +41,16 @@ public class IndexModel : CmsKitPublicPageModelBase
{
Blogs = await BlogPostPublicAppService.GetListAsync(
BlogSlug,
new PagedAndSortedResultRequestDto
new BlogPostGetListInput
{
SkipCount = PageSize * (CurrentPage - 1),
MaxResultCount = PageSize
MaxResultCount = PageSize,
AuthorId = AuthorId
});
if (AuthorId != null)
{
SelectedAuthor = await BlogPostPublicAppService.GetAuthorHasBlogPostAsync(AuthorId.Value);
}
}
}

@ -20,4 +20,21 @@ span.area-title {
}
.popover {
min-width: 276px;
}
#scroll-index {
position: sticky;
top: 20px;
}
#scroll-index .scroll-top-btn.showup{
display: block;
}
#scroll-index .scroll-top-btn{
display: none;
font-size: .85em;
color: #aaa;
text-decoration: none;
padding-left: 18px;
}

@ -0,0 +1,46 @@
$(function () {
var $myNav = $('#blog-post-sticky-index');
var $scrollToTopBtn = $('.scroll-top-btn');
window.Toc.helpers.createNavList = function () {
return $('<ul class="nav nav-pills flex-column"></ul>');
};
window.Toc.helpers.createChildNavList = function ($parent) {
var $childList = this.createNavList();
$parent.append($childList);
return $childList;
};
window.Toc.helpers.generateNavEl = function (anchor, text) {
var $a = $('<a class="nav-link"></a>');
$a.attr('href', '#' + anchor);
$a.text(text);
var $li = $('<li class="nav-item"></li>');
$li.append($a);
return $li;
};
Toc.init($myNav);
$('body').scrollspy({
target: $myNav,
});
$scrollToTopBtn.click(function () {
$('html, body').animate({scrollTop: 0}, 'fast');
});
// When the user scrolls down 20px from the top of the document, show the button
window.onscroll = function () {
scrollFunction()
};
function scrollFunction() {
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {
$scrollToTopBtn.addClass('showup');
} else {
$scrollToTopBtn.removeClass('showup');
}
}
});

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

Loading…
Cancel
Save