Browse Source

Merge branch 'dev' into feat/build-command

pull/5418/head
İsmail ÇAĞDAŞ 6 years ago
parent
commit
835a9cbda2
  1. 2
      .github/workflows/build-and-test.yml
  2. 1
      abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json
  3. 1
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json
  4. 18
      docs/en/Authorization.md
  5. 447
      docs/en/Features.md
  6. 16
      docs/en/Index.md
  7. 2
      docs/en/Modules/Feature-Management.md
  8. 2
      docs/en/Modules/Permission-Management.md
  9. 3
      docs/en/Multi-Tenancy.md
  10. 77
      docs/en/UI/Angular/Component-Replacement.md
  11. 3
      docs/en/UI/Angular/Features.md
  12. 12
      docs/en/UI/Angular/Permission-Management-Component-Replacement.md
  13. 4
      docs/en/docs-nav.json
  14. BIN
      docs/en/images/features-action.png
  15. BIN
      docs/en/images/features-modal.png
  16. 86
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/TagHelpers/AbpComponentDemoSectionTagHelper.cs
  17. 9
      framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionManager.cs
  18. 4
      framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionManager.cs
  19. 46
      framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/abp/luxon/abp.luxon.js
  20. 2
      framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/DataFiltering/HardDelete_Tests.cs
  21. 6
      global.json
  22. 4
      modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json
  23. 4
      modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json
  24. 3
      modules/account/src/Volo.Abp.Account.Web/AbpAccountWebAutomapperProfile.cs
  25. 31
      modules/account/src/Volo.Abp.Account.Web/AbpAccountWebModule.cs
  26. 59
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/AccountProfilePasswordManagementGroupViewComponent.cs
  27. 17
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/Default.cshtml
  28. 31
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/Default.js
  29. 56
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/AccountProfilePersonalInfoManagementGroupViewComponent.cs
  30. 39
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.cshtml
  31. 19
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.js
  32. 73
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Manage.cshtml
  33. 72
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Manage.cshtml.cs
  34. 47
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Manage.js
  35. 1
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs
  36. 48
      modules/account/src/Volo.Abp.Account.Web/ProfileManagement/AccountProfileManagementPageContributor.cs
  37. 9
      modules/account/src/Volo.Abp.Account.Web/ProfileManagement/IProfileManagementPageContributor.cs
  38. 19
      modules/account/src/Volo.Abp.Account.Web/ProfileManagement/ProfileManagementPageCreationContext.cs
  39. 39
      modules/account/src/Volo.Abp.Account.Web/ProfileManagement/ProfileManagementPageGroup.cs
  40. 14
      modules/account/src/Volo.Abp.Account.Web/ProfileManagement/ProfileManagementPageOptions.cs
  41. 7
      modules/cms-kit/host/Volo.CmsKit.HttpApi.Host/CmsKitHttpApiHostModule.cs
  42. 3
      modules/cms-kit/host/Volo.CmsKit.HttpApi.Host/Volo.CmsKit.HttpApi.Host.csproj
  43. 4
      modules/cms-kit/host/Volo.CmsKit.IdentityServer/CmsKitIdentityServerModule.cs
  44. 1
      modules/cms-kit/host/Volo.CmsKit.IdentityServer/Volo.CmsKit.IdentityServer.csproj
  45. 1
      modules/cms-kit/host/Volo.CmsKit.Web.Unified/CmsKitWebUnifiedModule.cs
  46. 1235
      modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200826063729_CmsRatings_Added.Designer.cs
  47. 39
      modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200826063729_CmsRatings_Added.cs
  48. 2429
      modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/UnifiedDbContextModelSnapshot.cs
  49. 9
      modules/cms-kit/host/Volo.CmsKit.Web.Unified/Pages/Index.cshtml
  50. 37
      modules/cms-kit/host/Volo.CmsKit.Web.Unified/wwwroot/libs/star-rating-svg/css/star-rating-svg.css
  51. 1
      modules/cms-kit/host/Volo.CmsKit.Web.Unified/wwwroot/libs/star-rating-svg/js/jquery.star-rating-svg.min.js
  52. 3
      modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/GlobalFeatures/GlobalCmsKitFeatures.cs
  53. 18
      modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/GlobalFeatures/RatingsFeature.cs
  54. 6
      modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json
  55. 6
      modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/tr.json
  56. 15
      modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Ratings/RatingConsts.cs
  57. 25
      modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Ratings/IRatingRepository.cs
  58. 57
      modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Ratings/Rating.cs
  59. 9
      modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Ratings/RatingWithStarCountQueryResultItem.cs
  60. 17
      modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitDbContextModelCreatingExtensions.cs
  61. 2
      modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitEntityFrameworkCoreModule.cs
  62. 57
      modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Ratings/EfCoreRatingRepository.cs
  63. 3
      modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/CmsKitMongoDbContext.cs
  64. 6
      modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/CmsKitMongoDbContextExtensions.cs
  65. 3
      modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/CmsKitMongoDbModule.cs
  66. 3
      modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/ICmsKitMongoDbContext.cs
  67. 58
      modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Ratings/MongoRatingRepository.cs
  68. 30
      modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo.CmsKit.Public.Application.Contracts.csproj
  69. 13
      modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Ratings/CreateUpdateRatingInput.cs
  70. 19
      modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Ratings/IRatingPublicAppService.cs
  71. 19
      modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Ratings/RatingDto.cs
  72. 11
      modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Ratings/RatingWithStarCountDto.cs
  73. 32
      modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo.CmsKit.Public.Application.csproj
  74. 4
      modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/PublicApplicationAutoMapperProfile.cs
  75. 104
      modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Ratings/RatingPublicAppService.cs
  76. 32
      modules/cms-kit/src/Volo.CmsKit.Public.HttpApi/Volo.CmsKit.Public.HttpApi.csproj
  77. 51
      modules/cms-kit/src/Volo.CmsKit.Public.HttpApi/Volo/CmsKit/Public/Ratings/RatingPublicController.cs
  78. 6
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicWidgetsController.cs
  79. 2
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml
  80. 46
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/Default.cshtml
  81. 16
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/RatingScriptBundleContributor.cs
  82. 16
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/RatingStyleBundleContributor.cs
  83. 75
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/RatingViewComponent.cs
  84. 44
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/default.css
  85. 87
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/default.js
  86. 137
      modules/cms-kit/test/Volo.CmsKit.Application.Tests/Ratings/RatingPublicAppService_Tests.cs
  87. 9
      modules/cms-kit/test/Volo.CmsKit.EntityFrameworkCore.Tests/EntityFrameworkCore/Ratings/RatingRepository_Tests.cs
  88. 11
      modules/cms-kit/test/Volo.CmsKit.MongoDB.Tests/MongoDB/Ratings/RatingRepository_Tests.cs
  89. 47
      modules/cms-kit/test/Volo.CmsKit.TestBase/CmsKitDataSeedContributor.cs
  90. 41
      modules/cms-kit/test/Volo.CmsKit.TestBase/Ratings/RatingRepository_Tests.cs
  91. 18
      modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeatureGroupDto.cs
  92. 9
      modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeatureListDto.cs
  93. 9
      modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/GetFeatureListResultDto.cs
  94. 2
      modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/IFeatureAppService.cs
  95. 55
      modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs
  96. 2
      modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/FeaturesController.cs
  97. 98
      modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml
  98. 19
      modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml.cs
  99. 4
      modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/FeatureAppService_Tests.cs
  100. 82
      modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/StringValueJsonConverter_Tests.cs

2
.github/workflows/build-and-test.yml

@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-dotnet@master
with:
dotnet-version: 3.1.100
dotnet-version: 3.1.102
- name: Build All
run: .\build-all.ps1

1
abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json

@ -26,6 +26,7 @@
"ContributionGuide": "Contribution Guide",
"Blog": "Blog",
"Commercial": "Commercial",
"MyAccount": "My account",
"SeeDocuments": "See Documents"
}
}

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

@ -78,7 +78,6 @@
"Biography": "Biography",
"HasNoPublishedArticlesYet": "has no published articles yet",
"Author": "Author",
"MyAccount": "My account",
"LatestGithubAnnouncements": "Latest Github Announcements",
"SeeAllAnnouncements": "See All Announcements",
"LatestBlogPost": "Latest Blog Post",

18
docs/en/Authorization.md

@ -80,6 +80,8 @@ namespace Acme.BookStore.Permissions
> ABP automatically discovers this class. No additional configuration required!
> You typically define this class inside the `Application.Contracts` project of your [application](Startup-Templates/Application.md). The startup template already comes with an empty class named *YourProjectNamePermissionDefinitionProvider* that you can start with.
In the `Define` method, you first need to add a **permission group** or get an existing group then add **permissions** to this group.
When you define a permission, it becomes usable in the ASP.NET Core authorization system as a **policy** name. It also becomes visible in the UI. See permissions dialog for a role:
@ -276,14 +278,24 @@ public async Task CreateAsync(CreateAuthorDto input)
> Tip: Prefer to use the `Authorize` attribute wherever possible, since it is declarative & simple. Use `IAuthorizationService` if you need to conditionally check a permission and run a business code based on the permission check.
### Check a Permission in JavaScript
## Check a Permission in JavaScript
You may need to check a policy/permission on the client side.
### MVC UI
You may need to check a policy/permission on the client side. For ASP.NET Core MVC / Razor Pages applications, you can use the `abp.auth` API. Example:
For ASP.NET Core MVC / Razor Pages applications, you can use the `abp.auth` API.
**Example: Check if a given permission has been granted for the current user**
```js
abp.auth.isGranted('MyPermissionName');
```
### Angular UI
See the [permission management document](UI/Angular/Permission-Management.md) for the Angular UI.
## Permission Management
Permission management is normally done by an admin user using the permission management modal:
@ -374,7 +386,7 @@ Configure<AbpPermissionOptions>(options =>
### Permission Store
`IPermissionStore` is the only interface that needs to be implemented to read the value of permissions from a persistence source, generally a database system. Permission management module implements it. See the [permission management module documentation](Modules/Permission-Management.md) for more information
`IPermissionStore` is the only interface that needs to be implemented to read the value of permissions from a persistence source, generally a database system. The Permission Management module implements it and pre-installed in the application startup template. See the [permission management module documentation](Modules/Permission-Management.md) for more information
### AlwaysAllowAuthorizationService

447
docs/en/Features.md

@ -1,3 +1,448 @@
# Features
TODO
ABP Feature system is used to **enable**, **disable** or **change the behavior** of the application features **on runtime**.
The runtime value for a feature is generally a `boolean` value, like `true` (enabled) or `false` (disabled). However, you can get/set **any kind** of value for feature.
Feature system was originally designed to control the tenant features in a **[multi-tenant](Multi-Tenancy.md)** application. However, it is **extensible** and capable of determining the features by any condition.
> The feature system is implemented with the [Volo.Abp.Features](https://www.nuget.org/packages/Volo.Abp.Features) NuGet package. Most of the times you don't need to manually [install it](https://abp.io/package-detail/Volo.Abp.Features) since it comes pre-installed with the [application startup template](Startup-Templates/Application.md).
## Checking for the Features
Before explaining to define features, let's see how to check a feature value in your application code.
### RequiresFeature Attribute
`[RequiresFeature]` attribute (defined in the `Volo.Abp.Features` namespace) is used to declaratively check if a feature is `true` (enabled) or not. It is a useful shortcut for the `boolean` features.
**Example: Check if the "PDF Reporting" feature enabled**
```csharp
public class ReportingAppService : ApplicationService, IReportingAppService
{
[RequiresFeature("MyApp.PdfReporting")]
public async Task<PdfReportResultDto> GetPdfReportAsync()
{
//TODO...
}
}
```
* `RequiresFeature(...)` simply gets a feature name to check if it is enabled or not. If not enabled, an authorization [exception](Exception-Handling.md) is thrown and a proper response is returned to the client side.
* `[RequiresFeature]` can be used for a **method** or a **class**. When you use it for a class, all the methods of that class require the given feature.
* `RequiresFeature` may get multiple feature names, like `[RequiresFeature("Feature1", "Feature2")]`. In this case ABP checks if any of the features enabled. Use `RequiresAll` option, like `[RequiresFeature("Feature1", "Feature2", RequiresAll = true)]` to force to check all of the features to be enabled.
* Multiple usage of `[RequiresFeature]` attribute is supported for a method or class. ABP check checks all of them in that case.
> Feature name can be any arbitrary string. It should be unique for a feature.
#### About the Interception
ABP Framework uses the interception system to make the `[RequiresFeature]` attribute working. So, it can work with any class (application services, controllers...) that is injected from the [dependency injection](Dependency-Injection.md).
However, there are **some rules should be followed** in order to make it working;
* If you are **not injecting** the service over an interface (like `IMyService`), then the methods of the service must be `virtual`. Otherwise, [dynamic proxy / interception](Dynamic-Proxying-Interceptors.md) system can not work.
* Only `async` methods (methods returning a `Task` or `Task<T>`) are intercepted.
> There is an exception for the **controller and razor page methods**. They **don't require** the following the rules above, since ABP Framework uses the action/page filters to implement the feature checking in this case.
### IFeatureChecker Service
`IFeatureChecker` allows to check a feature in your application code.
#### IsEnabledAsync
Returns `true` if the given feature is enabled. So, you can conditionally execute your business flow.
**Example: Check if the "PDF Reporting" feature enabled**
```csharp
public class ReportingAppService : ApplicationService, IReportingAppService
{
private readonly IFeatureChecker _featureChecker;
public ReportingAppService(IFeatureChecker featureChecker)
{
_featureChecker = featureChecker;
}
public async Task<PdfReportResultDto> GetPdfReportAsync()
{
if (await _featureChecker.IsEnabledAsync("MyApp.PdfReporting"))
{
//TODO...
}
else
{
//TODO...
}
}
}
```
`IsEnabledAsync` has overloads to check multiple features in one method call.
#### GetOrNullAsync
Gets the current value for a feature. This method returns a `string`, so you store any kind of value inside it, by converting to or from `string`.
**Example: Check the maximum product count allowed**
```csharp
public class ProductController : AbpController
{
private readonly IFeatureChecker _featureChecker;
public ProductController(IFeatureChecker featureChecker)
{
_featureChecker = featureChecker;
}
public async Task<IActionResult> Create(CreateProductModel model)
{
var currentProductCount = await GetCurrentProductCountFromDatabase();
//GET THE FEATURE VALUE
var maxProductCountLimit =
await _featureChecker.GetOrNullAsync("MyApp.MaxProductCount");
if (currentProductCount >= Convert.ToInt32(maxProductCountLimit))
{
throw new BusinessException(
"MyApp:ReachToMaxProductCountLimit",
$"You can not create more than {maxProductCountLimit} products!"
);
}
//TODO: Create the product in the database...
}
private async Task<int> GetCurrentProductCountFromDatabase()
{
throw new System.NotImplementedException();
}
}
```
This example uses a numeric value as a feature limit product counts for a user/tenant in a SaaS application.
Instead of manually converting the value to `int`, you can use the generic overload of the `GetAsync` method:
```csharp
var maxProductCountLimit = await _featureChecker.GetAsync<int>("MyApp.MaxProductCount");
```
#### Extension Methods
There are some useful extension methods for the `IFeatureChecker` interface;
* `Task<T> GetAsync<T>(string name, T defaultValue = default)`: Used to get a value of a feature with the given type `T`. Allows to specify a `defaultValue` that is returned when the feature value is `null`.
* `CheckEnabledAsync(string name)`: Checks if given feature is enabled. Throws an `AbpAuthorizationException` if the feature was not `true` (enabled).
## Defining the Features
A feature should be defined to be able to check it.
### FeatureDefinitionProvider
Create a class inheriting the `FeatureDefinitionProvider` to define permissions.
**Example: Defining permissions**
```csharp
using Volo.Abp.Features;
namespace FeaturesDemo
{
public class MyFeatureDefinitionProvider : FeatureDefinitionProvider
{
public override void Define(IFeatureDefinitionContext context)
{
var myGroup = context.AddGroup("MyApp");
myGroup.AddFeature("MyApp.PdfReporting", defaultValue: "false");
myGroup.AddFeature("MyApp.MaxProductCount", defaultValue: "10");
}
}
}
```
> ABP automatically discovers this class and registers the features. No additional configuration required.
> This class is generally created in the `Application.Contracts` project of your solution.
* In the `Define` method, you first need to add a **feature group** for your application/module or get an existing group then add **features** to this group.
* First feature, named `MyApp.PdfReporting`, is a `boolean` feature with `false` as the default value.
* Second feature, named `MyApp.MaxProductCount`, is a numeric feature with `10` as the default value.
Default value is used if there is no other value set for the current user/tenant.
### Other Feature Properties
While these minimal definitions are enough to make the feature system working, you can specify the **optional properties** for the features;
* `DisplayName`: A localizable string that will be used to show the feature name on the user interface.
* `Description`: A longer localizable text to describe the feature.
* `ValueType`: Type of the feature value. Can be a class implementing the `IStringValueType`. Built-in types:
* `ToggleStringValueType`: Used to define `true`/`false`, `on`/`off`, `enabled`/`disabled` style features. A checkbox is shown on the UI.
* `FreeTextStringValueType`: Used to define free text values. A textbox is shown on the UI.
* `SelectionStringValueType`: Used to force the value to be selected from a list. A dropdown list is shown on the UI.
* `IsVisibleToClients` (default: `true`): Set false to hide the value of this feature from clients (browsers). Sharing the value with the clients helps them to conditionally show/hide/change the UI parts based on the feature value.
* `Properties`: A dictionary to set/get arbitrary key-value pairs related to this feature. This can be a point for customization.
So, based on these descriptions, it would be better to define these features as shown below:
```csharp
using FeaturesDemo.Localization;
using Volo.Abp.Features;
using Volo.Abp.Localization;
using Volo.Abp.Validation.StringValues;
namespace FeaturesDemo
{
public class MyFeatureDefinitionProvider : FeatureDefinitionProvider
{
public override void Define(IFeatureDefinitionContext context)
{
var myGroup = context.AddGroup("MyApp");
myGroup.AddFeature(
"MyApp.PdfReporting",
defaultValue: "false",
displayName: LocalizableString
.Create<FeaturesDemoResource>("PdfReporting"),
valueType: new ToggleStringValueType()
);
myGroup.AddFeature(
"MyApp.MaxProductCount",
defaultValue: "10",
displayName: LocalizableString
.Create<FeaturesDemoResource>("MaxProductCount"),
valueType: new FreeTextStringValueType(
new NumericValueValidator(0, 1000000))
);
}
}
}
```
* `FeaturesDemoResource` is the project name in this example code. See the [localization document](Localization.md) for details about the localization system.
* First feature is set to `ToggleStringValueType`, while the second one is set to `FreeTextStringValueType` with a numeric validator that allows to the values from `0` to `1,000,000`.
Remember to define the localization the keys in your localization file:
````json
"PdfReporting": "PDF Reporting",
"MaxProductCount": "Maximum number of products"
````
See the [localization document](Localization.md) for details about the localization system.
### Feature Management Modal
The [application startup template](Startup-Templates/Application.md) comes with the [tenant management](Modules/Tenant-Management.md) and the [feature management](Modules/Feature-Management.md) modules pre-installed.
Whenever you define a new feature, it will be available on the **feature management modal**. To open this modal, navigate to the **tenant management page** and select the `Features` action for a tenant (create a new tenant if there is no tenant yet):
![features-action](images/features-action.png)
This action opens a modal to manage the feature values for the selected tenant:
![features-modal](images/features-modal.png)
So, you can enable, disable and set values for a tenant. These values will be used whenever a user of this tenant uses the application.
See the *Feature Management* section below to learn more about managing the features.
### Child Features
A feature may have child features. This is especially useful if you want to create a feature that is selectable only if another feature was enabled.
**Example: Defining child features**
```csharp
using FeaturesDemo.Localization;
using Volo.Abp.Features;
using Volo.Abp.Localization;
using Volo.Abp.Validation.StringValues;
namespace FeaturesDemo
{
public class MyFeatureDefinitionProvider : FeatureDefinitionProvider
{
public override void Define(IFeatureDefinitionContext context)
{
var myGroup = context.AddGroup("MyApp");
var reportingFeature = myGroup.AddFeature(
"MyApp.Reporting",
defaultValue: "false",
displayName: LocalizableString
.Create<FeaturesDemoResource>("Reporting"),
valueType: new ToggleStringValueType()
);
reportingFeature.CreateChild(
"MyApp.PdfReporting",
defaultValue: "false",
displayName: LocalizableString
.Create<FeaturesDemoResource>("PdfReporting"),
valueType: new ToggleStringValueType()
);
reportingFeature.CreateChild(
"MyApp.ExcelReporting",
defaultValue: "false",
displayName: LocalizableString
.Create<FeaturesDemoResource>("ExcelReporting"),
valueType: new ToggleStringValueType()
);
}
}
}
```
The example above defines a *Reporting* feature with two children: *PDF Reporting* and *Excel Reporting*.
### Changing Features Definitions of a Depended Module
A class deriving from the `FeatureDefinitionProvider` (just like the example above) can also get the existing permission definitions (defined by the depended [modules](Module-Development-Basics.md)) and change their definitions.
**Example: Manipulate an existing feature definition**
```csharp
var someGroup = context.GetGroupOrNull("SomeModule");
var feature = someGroup.Features.FirstOrDefault(f => f.Name == "SomeFeature");
if (feature != null)
{
feature.Description = ...
feature.CreateChild(...);
}
```
## Check a Feature in the Client Side
A feature value is available at the client side too, unless you set `IsVisibleToClients` to `false` on the feature definition. The feature values are exposed from the [Application Configuration API](API/Application-Configuration.md) and usable via some services on the UI.
### ASP.NET Core MVC / Razor Pages UI
Use `abp.features` API to get the feature values.
**Example: Get feature values in the JavaScript code**
````js
var isEnabled = abp.features.values["MyApp.ExcelReporting"] === "true";
var count = abp.features.values["MyApp.MaxProductCount"];
````
### Angular UI
See the [features](Features.md) document for the Angular UI.
## Feature Management
Feature management is normally done by an admin user using the feature management modal:
![features-modal](images/features-modal.png)
This modal is available on the related entities, like tenants in a multi-tenant application. To open it, navigate to the **Tenant Management** page (for a multi-tenant application), click to the **Actions** button left to the Tenant and select the **Features** action.
If you need to manage features by code, inject the `IFeatureManager` service.
**Example: Enable PDF reporting for a tenant**
```csharp
public class MyService : ITransientDependency
{
private readonly IFeatureManager _featureManager;
public MyService(IFeatureManager featureManager)
{
_featureManager = featureManager;
}
public async Task EnablePdfReporting(Guid tenantId)
{
await _featureManager.SetForTenantAsync(
tenantId,
"MyApp.PdfReporting",
true.ToString()
);
}
}
```
`IFeatureManager` is defined by the Feature Management module. It comes pre-installed with the application startup template. See the [feature management module documentation](Modules/Feature-Management.md) for more information.
## Advanced Topics
### Feature Value Providers
Feature system is extensible. Any class derived from `FeatureValueProvider` (or implements `IFeatureValueProvider`) can contribute to the feature system. A value provider is responsible to **obtain the current value** of a given feature.
Feature value providers are **executed one by one**. If one of them return a non-null value, then this feature value is used and the other providers are not executed.
There are three pre-defined value providers, executed by the given order:
* `TenantFeatureValueProvider` tries to get if the feature value is explicitly set for the **current tenant**.
* `EditionFeatureValueProvider` tries to get the feature value for the current edition. Edition Id is obtained from the current principal identity (`ICurrentPrincipalAccessor`) with the claim name `editionid` (a constant defined as`AbpClaimTypes.EditionId`). Editions are not implemented for the [tenant management](Modules/Tenant-Management.md) module. You can implement it yourself or consider to use the [SaaS module](https://commercial.abp.io/modules/Volo.Saas) of the ABP Commercial.
* `DefaultValueFeatureValueProvider` gets the default value of the feature.
You can write your own provider by inheriting the `FeatureValueProvider`.
**Example: Enable all features for a user with "SystemAdmin" as a "User_Type" claim value**
```csharp
using System.Threading.Tasks;
using Volo.Abp.Features;
using Volo.Abp.Security.Claims;
using Volo.Abp.Validation.StringValues;
namespace FeaturesDemo
{
public class SystemAdminFeatureValueProvider : FeatureValueProvider
{
public override string Name => "SA";
private readonly ICurrentPrincipalAccessor _currentPrincipalAccessor;
public SystemAdminFeatureValueProvider(
IFeatureStore featureStore,
ICurrentPrincipalAccessor currentPrincipalAccessor)
: base(featureStore)
{
_currentPrincipalAccessor = currentPrincipalAccessor;
}
public override Task<string> GetOrNullAsync(FeatureDefinition feature)
{
if (feature.ValueType is ToggleStringValueType &&
_currentPrincipalAccessor.Principal?.FindFirst("User_Type")?.Value == "SystemAdmin")
{
return Task.FromResult("true");
}
return null;
}
}
}
```
If a provider returns `null`, then the next provider is executed.
Once a provider is defined, it should be added to the `AbpFeatureOptions` as shown below:
```csharp
Configure<AbpFeatureOptions>(options =>
{
options.ValueProviders.Add<SystemAdminFeatureValueProvider>();
});
```
Use this code inside the `ConfigureServices` of your [module](Module-Development-Basics.md) class.
### Feature Store
`IFeatureStore` is the only interface that needs to be implemented to read the value of features from a persistence source, generally a database system. The Feature Management module implements it and pre-installed in the application startup template. See the [feature management module documentation](https://docs.abp.io/en/abp/latest/Modules/Feature-Management) for more information

16
docs/en/Index.md

@ -6,9 +6,21 @@ Explore the navigation menu to deep dive in the documentation.
## Getting Started
The easiest way to start a new web application with the ABP Framework is to use the [getting started](Getting-Started.md) tutorial.
The easiest way to start a new web application with the ABP Framework is to use the [getting started](Getting-Started.md) guide.
Then you can continue with the [web application development tutorial](Tutorials/Part-1.md).
## Tutorials / Articles
### Web Application Development
[Web application development tutorial](Tutorials/Part-1.md) is a complete tutorial to develop a full stack application using the ABP Framework.
### ABP Community Articles
See also the [ABP Community](https://community.abp.io/) articles.
## Samples
See the [sample projects](Samples/Index.md) built with the ABP Framework.
## Source Code

2
docs/en/Modules/Feature-Management.md

@ -1,3 +1,5 @@
# Feature Management Module
> This module implements the `IFeatureStore` to store and manage feature values in a database. See the [Features System document](../Features.md) to understand the features first.
TODO

2
docs/en/Modules/Permission-Management.md

@ -1,3 +1,5 @@
# Permission Management Module
This module implements the `IPermissionStore` to store and manage feature values in a database. See the [Authorization document](../Authorization.md) to understand the authorization and permission systems first.
TODO

3
docs/en/Multi-Tenancy.md

@ -369,3 +369,6 @@ options.AddDomainTenantResolver("{0}.mydomain.com");
options.AddDomainTenantResolver("{0}.com");
````
## See Also
* [Features](Features.md)

77
docs/en/UI/Angular/Component-Replacement.md

@ -8,27 +8,22 @@ The reason that you **can replace** but **cannot customize** default ABP compone
Create a new component that you want to use instead of an ABP component. Add that component to `declarations` and `entryComponents` in the `AppModule`.
Then, open the `app.component.ts` and dispatch the `AddReplaceableComponent` action to replace your component with an ABP component as shown below:
Then, open the `app.component.ts` and execute the `add` method of `ReplaceableComponentsService` to replace your component with an ABP component as shown below:
```js
import { AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent action
import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum
import { Store } from '@ngxs/store'; // imported Store
//...
@Component(/* component metadata */)
export class AppComponent {
constructor(
private store: Store // injected Store
)
{
// dispatched the AddReplaceableComponent action
this.store.dispatch(
new AddReplaceableComponent({
component: YourNewRoleComponent,
key: eIdentityComponents.Roles,
}),
);
private replaceableComponents: ReplaceableComponentsService, // injected the service
) {
this.replaceableComponents.add({
component: YourNewRoleComponent,
key: eIdentityComponents.Roles,
});
}
}
```
@ -59,28 +54,24 @@ Add the following code in your layout template (`my-layout.component.html`) wher
Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js
import { AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent
import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents enum for component keys
import { Store } from '@ngxs/store'; // imported Store
import { MyApplicationLayoutComponent } from './my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent
@Component(/* component metadata */)
export class AppComponent {
constructor(
private store: Store, // injected Store
private replaceableComponents: ReplaceableComponentsService, // injected the service
) {
// dispatched the AddReplaceableComponent action
this.store.dispatch(
new AddReplaceableComponent({
component: MyApplicationLayoutComponent,
key: eThemeBasicComponents.ApplicationLayout,
}),
);
this.replaceableComponents.add({
component: MyApplicationLayoutComponent,
key: eThemeBasicComponents.ApplicationLayout,
});
}
}
```
> If you like to replace a layout component at runtime (e.g: changing the layout by pressing a button), pass the second parameter of the AddReplaceableComponent action as true. DynamicLayoutComponent loads content using a router-outlet. When the second parameter of AddReplaceableComponent is true, the route will be refreshed, so use it with caution. Your component state will be gone and any initiation logic (including HTTP requests) will be repeated.
> If you like to replace a layout component at runtime (e.g: changing the layout by pressing a button), pass the second parameter of the `add` method of `ReplaceableComponentsService` as true. DynamicLayoutComponent loads content using a router-outlet. When the second parameter of the `add` method is true, the route will be refreshed, so use it with caution. Your component state will be gone and any initiation logic (including HTTP requests) will be repeated.
### Layout Components
@ -123,26 +114,22 @@ export class LogoComponent {}
Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent
import { Store } from '@ngxs/store'; // imported Store
import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { LogoComponent } from './logo/logo.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//...
@Component(/* component metadata */)
export class AppComponent implements OnInit {
constructor(..., private store: Store) {} // injected Store
constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() {
//...
// added dispatch
this.store.dispatch(
new AddReplaceableComponent({
this.replaceableComponents.add({
component: LogoComponent,
key: eThemeBasicComponents.Logo,
}),
);
});
}
}
```
@ -166,7 +153,7 @@ yarn ng generate component routes --entryComponent
Open the generated `routes.component.ts` in `src/app/routes` folder and replace its content with the following:
```js
import { ABP, ReplaceableComponents } from '@abp/ng.core';
import { ABP } from '@abp/ng.core';
import {
Component,
HostBinding,
@ -309,26 +296,22 @@ Open the generated `routes.component.html` in `src/app/routes` folder and replac
Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent
import { Store } from '@ngxs/store'; // imported Store
import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { RoutesComponent } from './routes/routes.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//...
@Component(/* component metadata */)
export class AppComponent implements OnInit {
constructor(..., private store: Store) {} // injected Store
constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() {
//...
// added dispatch
this.store.dispatch(
new AddReplaceableComponent({
this.replaceableComponents.add({
component: RoutesComponent,
key: eThemeBasicComponents.Routes,
}),
);
});
}
}
```
@ -511,26 +494,22 @@ Open the generated `nav-items.component.html` in `src/app/nav-items` folder and
Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent
import { Store } from '@ngxs/store'; // imported Store
import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { NavItemsComponent } from './nav-items/nav-items.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//...
@Component(/* component metadata */)
export class AppComponent implements OnInit {
constructor(..., private store: Store) {} // injected Store
constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() {
//...
// added dispatch
this.store.dispatch(
new AddReplaceableComponent({
this.replaceableComponents.add({
component: NavItemsComponent,
key: eThemeBasicComponents.NavItems,
}),
);
});
}
}
```

3
docs/en/UI/Angular/Features.md

@ -0,0 +1,3 @@
# Angular UI: Features
> This document explains how to get feature values in an Angular application. See the [Features document](../../Features.md) to learn the feature system.

12
docs/en/UI/Angular/Permission-Management-Component-Replacement.md

@ -473,24 +473,20 @@ Open the generated `permission-management.component.html` in `src/app/permission
Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js
import { AddReplaceableComponent } from '@abp/ng.core';
import { ReplaceableComponentsService } from '@abp/ng.core';
import { ePermissionManagementComponents } from '@abp/ng.permission-management';
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngxs/store';
import { PermissionManagementComponent } from './permission-management/permission-management.component';
//...
export class AppComponent implements OnInit {
constructor(private store: Store) {} // injected store
constructor(private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() {
// added dispatching the AddReplaceableComponent action
this.store.dispatch(
new AddReplaceableComponent({
this.replaceableComponents.add({
component: PermissionManagementComponent,
key: ePermissionManagementComponents.PermissionManagement,
})
);
});
}
}
```

4
docs/en/docs-nav.json

@ -170,6 +170,10 @@
"text": "Settings",
"path": "Settings.md"
},
{
"text": "Features",
"path": "Features.md"
},
{
"text": "Data Filtering",
"path": "Data-Filtering.md"

BIN
docs/en/images/features-action.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/en/images/features-modal.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

86
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Views/Components/Themes/Shared/TagHelpers/AbpComponentDemoSectionTagHelper.cs

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.FileProviders;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers;
using Volo.Abp.Guids;
using Volo.Abp.VirtualFileSystem;
namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.Shared.TagHelpers
@ -17,10 +18,14 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.S
public string Title { get; set; }
private readonly IVirtualFileProvider _virtualFileProvider;
private readonly IGuidGenerator _guidGenerator;
public AbpComponentDemoSectionTagHelper(IVirtualFileProvider virtualFileProvider)
public AbpComponentDemoSectionTagHelper(
IVirtualFileProvider virtualFileProvider,
IGuidGenerator guidGenerator)
{
_virtualFileProvider = virtualFileProvider;
_guidGenerator = guidGenerator;
}
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
@ -29,34 +34,69 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.Views.Components.Themes.S
var content = await output.GetChildContentAsync();
output.PreContent.AppendHtml("<div class=\"abp-component-demo-section\">");
output.PreContent.AppendHtml($"<h2>{Title}</h2>");
output.PreContent.AppendHtml("<div class=\"abp-component-demo-section-body\">");
var previewId = _guidGenerator.Create();
var codeBlockId = _guidGenerator.Create();
var codeBlockTabId = _guidGenerator.Create();
var tagHelperCodeBlockId = _guidGenerator.Create();
var bootstrapCodeBlockId = _guidGenerator.Create();
output.PreContent.AppendHtml("<div class=\"card\">");
output.PreContent.AppendHtml("<div class=\"card-header\">");
output.PreContent.AppendHtml("<div class=\"row\">");
output.PreContent.AppendHtml($"<div class=\"col\"><h4 class=\"card-title my-1\">{Title}</h4></div>");
output.PreContent.AppendHtml("<div class=\"col-auto\">");
output.PreContent.AppendHtml("<nav>");
output.PreContent.AppendHtml("<div class=\"nav nav-pills\" role=\"tablist\">");
output.PreContent.AppendHtml($"<a class=\"nav-item nav-link active\" id=\"nav-preview-tab-{previewId}\" data-toggle=\"tab\" href=\"#nav-preview-{previewId}\" role=\"tab\" aria-controls=\"nav-preview-{previewId}\" aria-selected=\"true\">Preview</a>");
output.PreContent.AppendHtml($"<a class=\"nav-item nav-link\" id=\"nav-code-tab-{codeBlockId}\" data-toggle=\"tab\" href=\"#nav-code-{codeBlockId}\" role=\"tab\" aria-controls=\"nav-code-{codeBlockId}\" aria-selected=\"false\">Code</a>");
output.PreContent.AppendHtml("</div>");
output.PreContent.AppendHtml("</nav>");
output.PreContent.AppendHtml("</div>"); // col-auto
output.PreContent.AppendHtml("</div>"); // row
output.PreContent.AppendHtml("</div>"); // card-header
output.PreContent.AppendHtml("<div class=\"card-body\">");
output.PreContent.AppendHtml($"<div class=\"tab-content\" id=\"nav-preview-tabContent-{previewId}\">");
output.PreContent.AppendHtml($"<div class=\"tab-pane fade show active\" id=\"nav-preview-{previewId}\" role=\"tabpanel\" aria-labelledby=\"nav-preview-tab-{previewId}\">");
/* component rendering here */
output.PostContent.AppendHtml("</div>"); //abp-component-demo-section-body
AppendRawSource(output);
AppendBootstrapSource(output, content);
output.PostContent.AppendHtml("</div>"); //abp-component-demo-section
}
private static void AppendBootstrapSource(TagHelperOutput output, TagHelperContent content)
{
output.PostContent.AppendHtml("<div class=\"abp-component-demo-section-bs-source\">");
output.PostContent.AppendHtml("<h3>Bootstrap</h3>");
output.PostContent.AppendHtml("<pre>");
output.PostContent.Append(content.GetContent());
output.PostContent.AppendHtml("</div>"); // tab-pane
output.PostContent.AppendHtml($"<div class=\"tab-pane fade\" id=\"nav-code-{codeBlockId}\" role=\"tabpanel\" aria-labelledby=\"nav-code-tab-{codeBlockId}\">");
/* CodeBlock tabs */
output.PostContent.AppendHtml($"<ul class=\"nav nav-tabs\" id=\"code-block-tab-{codeBlockTabId}\" role=\"tablist\">");
output.PostContent.AppendHtml("<li class=\"nav-item\">");
output.PostContent.AppendHtml($"<a class=\"nav-link active\" id=\"tag-helper-tab-{tagHelperCodeBlockId}\" data-toggle=\"pill\" href=\"#tag-helper-{tagHelperCodeBlockId}\" role=\"tab\" aria-controls=\"tag-helper-{tagHelperCodeBlockId}\" aria-selected=\"true\">Abp Tag Helper</a>");
output.PostContent.AppendHtml("</li>");
output.PostContent.AppendHtml("<li class=\"nav-item\">");
output.PostContent.AppendHtml($"<a class=\"nav-link\" id=\"bootstrap-tab-{bootstrapCodeBlockId}\" data-toggle=\"pill\" href=\"#bootstrap-{bootstrapCodeBlockId}\" role=\"tab\" aria-controls=\"bootstrap-{tagHelperCodeBlockId}\" aria-selected=\"true\">Bootstrap</a>");
output.PostContent.AppendHtml("</li>");
output.PostContent.AppendHtml("</ul>");
output.PostContent.AppendHtml($"<div class=\"tab-content\" id=\"code-block-tabContent-{codeBlockTabId}\">");
output.PostContent.AppendHtml($"<div class=\"tab-pane fade show active\" id=\"tag-helper-{tagHelperCodeBlockId}\" role=\"tabpanel\" aria-labelledby=\"tag-helper-tab-{tagHelperCodeBlockId}\">");
output.PostContent.AppendHtml("<pre class=\"p-4\">");
output.PostContent.AppendHtml("<code>");
output.PostContent.Append(GetRawDemoSource());
output.PostContent.AppendHtml("</code>");
output.PostContent.AppendHtml("</pre>");
output.PostContent.AppendHtml("</div>");
}
private void AppendRawSource(TagHelperOutput output)
{
output.PostContent.AppendHtml("<div class=\"abp-component-demo-section-raw-source\">");
output.PostContent.AppendHtml("<h3>ABP Tag Helpers</h3>");
output.PostContent.AppendHtml("<pre>");
output.PostContent.Append(GetRawDemoSource());
output.PostContent.AppendHtml($"<div class=\"tab-pane fade\" id=\"bootstrap-{bootstrapCodeBlockId}\" role=\"tabpanel\" aria-labelledby=\"bootstrap-tab-{bootstrapCodeBlockId}\">");
output.PostContent.AppendHtml("<pre class=\"p-4\">");
output.PostContent.AppendHtml("<code>");
output.PostContent.Append(content.GetContent());
output.PostContent.AppendHtml("</code>");
output.PostContent.AppendHtml("</pre>");
output.PostContent.AppendHtml("</div>");
output.PostContent.AppendHtml("</div>"); // tab-content
output.PostContent.AppendHtml("</div>"); // tab-pane
output.PostContent.AppendHtml("</div>"); // tab-content
output.PostContent.AppendHtml("</div>"); // card-body
output.PostContent.AppendHtml("</div>"); // card
}
private string GetRawDemoSource()

9
framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionManager.cs

@ -62,6 +62,11 @@ namespace Volo.Abp.Features
return FeatureDefinitions.GetOrDefault(name);
}
public IReadOnlyList<FeatureGroupDefinition> GetGroups()
{
return FeatureGroupDefinitions.Values.ToImmutableList();
}
protected virtual Dictionary<string, FeatureDefinition> CreateFeatureDefinitions()
{
var features = new Dictionary<string, FeatureDefinition>();
@ -78,7 +83,7 @@ namespace Volo.Abp.Features
}
protected virtual void AddFeatureToDictionaryRecursively(
Dictionary<string, FeatureDefinition> features,
Dictionary<string, FeatureDefinition> features,
FeatureDefinition feature)
{
if (features.ContainsKey(feature.Name))
@ -114,4 +119,4 @@ namespace Volo.Abp.Features
return context.Groups;
}
}
}
}

4
framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionManager.cs

@ -11,5 +11,7 @@ namespace Volo.Abp.Features
IReadOnlyList<FeatureDefinition> GetAll();
FeatureDefinition GetOrNull(string name);
IReadOnlyList<FeatureGroupDefinition> GetGroups();
}
}
}

46
framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/wwwroot/libs/abp/luxon/abp.luxon.js

@ -0,0 +1,46 @@
var abp = abp || {};
(function () {
if (!luxon) {
throw "abp/luxon library requires the luxon library included to the page!";
}
/* TIMING *************************************************/
abp.timing = abp.timing || {};
var setObjectValue = function (obj, property, value) {
if (typeof property === "string") {
property = property.split('.');
}
if (property.length > 1) {
var p = property.shift();
setObjectValue(obj[p], property, value);
} else {
obj[property[0]] = value;
}
}
var getObjectValue = function (obj, property) {
return property.split('.').reduce((a, v) => a[v], obj)
}
abp.timing.convertFieldsToIsoDate = function (form, fields) {
for (var field of fields) {
var dateTime = luxon.DateTime
.fromFormat(
getObjectValue(form, field),
abp.localization.currentCulture.dateTimeFormat.shortDatePattern,
{locale: abp.localization.currentCulture.cultureName}
);
if (!dateTime.invalid) {
setObjectValue(form, field, dateTime.toFormat("yyyy-MM-dd HH:mm:ss"))
}
}
return form;
}
})(jQuery);

2
framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/DataFiltering/HardDelete_Tests.cs

@ -2,9 +2,11 @@
using System.Collections.Generic;
using System.Text;
using Volo.Abp.TestApp.Testing;
using Xunit;
namespace Volo.Abp.MongoDB.DataFiltering
{
[Collection(MongoTestCollection.Name)]
public class HardDelete_Tests : HardDelete_Tests<AbpMongoDbTestModule>
{
}

6
global.json

@ -0,0 +1,6 @@
{
"sdk": {
"version": "3.1.102",
"rollForward": "latestFeature"
}
}

4
modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json

@ -53,6 +53,8 @@
"ResetPassword_Information": "Please enter your new password.",
"YourPasswordIsSuccessfullyReset": "Your password is successfully reset.",
"GoToTheApplication": "Go to the application",
"BackToLogin": "Back to login"
"BackToLogin": "Back to login",
"ProfileTab:Password": "Change password",
"ProfileTab:PersonalInfo": "Personal info"
}
}

4
modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json

@ -52,6 +52,8 @@
"ResetPassword_Information": "Lütfen yeni şifrenizi belirleyin.",
"YourPasswordIsSuccessfullyReset": "Şifreniz başarıyla sıfırlandı.",
"GoToTheApplication": "Uygulamaya git",
"BackToLogin": "Girişe dön"
"BackToLogin": "Girişe dön",
"ProfileTab:Password": "Şifre değiştir",
"ProfileTab:PersonalInfo": "Kişisel bilgiler"
}
}

3
modules/account/src/Volo.Abp.Account.Web/AbpAccountWebAutomapperProfile.cs

@ -1,6 +1,7 @@
using Volo.Abp.Account.Web.Pages.Account;
using Volo.Abp.Identity;
using AutoMapper;
using Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.PersonalInfo;
namespace Volo.Abp.Account.Web
{
@ -8,7 +9,7 @@ namespace Volo.Abp.Account.Web
{
public AbpAccountWebAutoMapperProfile()
{
CreateMap<ProfileDto, PersonalSettingsInfoModel>();
CreateMap<ProfileDto, AccountProfilePersonalInfoManagementGroupViewComponent.PersonalInfoModel>();
}
}
}

31
modules/account/src/Volo.Abp.Account.Web/AbpAccountWebModule.cs

@ -1,7 +1,10 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Account.Localization;
using Volo.Abp.Account.Web.Pages.Account;
using Volo.Abp.Account.Web.ProfileManagement;
using Volo.Abp.AspNetCore.Mvc.Localization;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Toolbars;
using Volo.Abp.AutoMapper;
@ -50,16 +53,38 @@ namespace Volo.Abp.Account.Web
options.Contributors.Add(new AccountModuleToolbarContributor());
});
ConfigureProfileManagementPage();
context.Services.AddAutoMapperObjectMapper<AbpAccountWebModule>();
Configure<AbpAutoMapperOptions>(options =>
{
options.AddProfile<AbpAccountWebAutoMapperProfile>(validate: true);
});
}
private void ConfigureProfileManagementPage()
{
Configure<RazorPagesOptions>(options =>
{
options.Conventions.AuthorizePage("/Account/Manage");
});
context.Services.AddAutoMapperObjectMapper<AbpAccountWebModule>();
Configure<AbpAutoMapperOptions>(options =>
Configure<ProfileManagementPageOptions>(options =>
{
options.AddProfile<AbpAccountWebAutoMapperProfile>(validate: true);
options.Contributors.Add(new AccountProfileManagementPageContributor());
});
Configure<AbpBundlingOptions>(options =>
{
options.ScriptBundles
.Configure(typeof(ManageModel).FullName,
configuration =>
{
configuration.AddFiles("/Pages/Account/Components/ProfileManagementGroup/Password/Default.js");
configuration.AddFiles("/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.js");
});
});
}
}
}

59
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/AccountProfilePasswordManagementGroupViewComponent.cs

@ -0,0 +1,59 @@
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Auditing;
using Volo.Abp.Identity;
using Volo.Abp.Validation;
namespace Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.Password
{
public class AccountProfilePasswordManagementGroupViewComponent : AbpViewComponent
{
private readonly IProfileAppService _profileAppService;
public AccountProfilePasswordManagementGroupViewComponent(
IProfileAppService profileAppService)
{
_profileAppService = profileAppService;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var user = await _profileAppService.GetAsync();
var model = new ChangePasswordInfoModel
{
HideOldPasswordInput = !user.HasPassword
};
return View("~/Pages/Account/Components/ProfileManagementGroup/Password/Default.cshtml", model);
}
public class ChangePasswordInfoModel
{
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))]
[Display(Name = "DisplayName:CurrentPassword")]
[DataType(DataType.Password)]
[DisableAuditing]
public string CurrentPassword { get; set; }
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))]
[Display(Name = "DisplayName:NewPassword")]
[DataType(DataType.Password)]
[DisableAuditing]
public string NewPassword { get; set; }
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))]
[Display(Name = "DisplayName:NewPasswordConfirm")]
[DataType(DataType.Password)]
[DisableAuditing]
public string NewPasswordConfirm { get; set; }
public bool HideOldPasswordInput { get; set; }
}
}
}

17
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/Default.cshtml

@ -0,0 +1,17 @@
@using Volo.Abp.Account.Localization
@using Volo.Abp.Users
@using Microsoft.AspNetCore.Mvc.Localization
@inject IHtmlLocalizer<AccountResource> L
@inject ICurrentUser CurrentUser
@model Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.Password.AccountProfilePasswordManagementGroupViewComponent.ChangePasswordInfoModel
<h4>@L["ChangePassword"]</h4><hr/>
<form id="ChangePasswordForm">
@if (!Model.HideOldPasswordInput)
{
<abp-input asp-for="CurrentPassword"/>
}
<abp-input asp-for="NewPassword"/>
<abp-input asp-for="NewPasswordConfirm"/>
<abp-button type="submit" button-type="Primary" text="@L["Submit"].Value"/>
</form>

31
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/Default.js

@ -0,0 +1,31 @@
(function ($) {
$(function () {
var l = abp.localization.getResource("AbpAccount");
$('#ChangePasswordForm').submit(function (e) {
e.preventDefault();
if (!$('#ChangePasswordForm').valid()) {
return false;
}
var input = $('#ChangePasswordForm').serializeFormToObject();
if (
input.newPassword != input.newPasswordConfirm ||
input.newPassword == ''
) {
abp.message.error(l('NewPasswordConfirmFailed'));
return;
}
if (input.currentPassword && input.currentPassword == ''){
return;
}
volo.abp.identity.profile.changePassword(input).then(function (result) {
abp.message.success(l('PasswordChanged'));
});
});
});
})(jQuery);

56
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/AccountProfilePersonalInfoManagementGroupViewComponent.cs

@ -0,0 +1,56 @@
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Identity;
using Volo.Abp.Validation;
namespace Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.PersonalInfo
{
public class AccountProfilePersonalInfoManagementGroupViewComponent : AbpViewComponent
{
private readonly IProfileAppService _profileAppService;
public AccountProfilePersonalInfoManagementGroupViewComponent(
IProfileAppService profileAppService)
{
_profileAppService = profileAppService;
ObjectMapperContext = typeof(AbpAccountWebModule);
}
public async Task<IViewComponentResult> InvokeAsync()
{
var user = await _profileAppService.GetAsync();
var model = ObjectMapper.Map<ProfileDto, PersonalInfoModel>(user);
return View("~/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.cshtml", model);
}
public class PersonalInfoModel
{
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxUserNameLength))]
[Display(Name = "DisplayName:UserName")]
public string UserName { get; set; }
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxEmailLength))]
[Display(Name = "DisplayName:Email")]
public string Email { get; set; }
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxNameLength))]
[Display(Name = "DisplayName:Name")]
public string Name { get; set; }
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxSurnameLength))]
[Display(Name = "DisplayName:Surname")]
public string Surname { get; set; }
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPhoneNumberLength))]
[Display(Name = "DisplayName:PhoneNumber")]
public string PhoneNumber { get; set; }
}
}
}

39
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.cshtml

@ -0,0 +1,39 @@
@using Volo.Abp.Account.Localization
@using Volo.Abp.Users
@using Microsoft.AspNetCore.Mvc.Localization
@using Volo.Abp.AspNetCore.Mvc.UI.Theming
@using Volo.Abp.Identity.Settings
@using Volo.Abp.Settings
@inject IHtmlLocalizer<AccountResource> L
@inject ICurrentUser CurrentUser
@inject ISettingProvider SettingManager
@inject IThemeManager ThemeManager
@model Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.PersonalInfo.AccountProfilePersonalInfoManagementGroupViewComponent.PersonalInfoModel
@{
var isUserNameUpdateEnabled = string.Equals(await SettingManager.GetOrNullAsync(IdentitySettingNames.User.IsUserNameUpdateEnabled), "true",
StringComparison.OrdinalIgnoreCase);
var isEmailUpdateEnabled = string.Equals(await SettingManager.GetOrNullAsync(IdentitySettingNames.User.IsEmailUpdateEnabled), "true",
StringComparison.OrdinalIgnoreCase);
}
<h4>@L["PersonalSettings"]</h4><hr/>
<form method="post" id="PersonalSettingsForm">
<abp-input asp-for="UserName" readonly="!isUserNameUpdateEnabled"/>
<abp-row>
<abp-column size-md="_6">
<abp-input asp-for="Name"/>
</abp-column>
<abp-column size-md="_6">
<abp-input asp-for="Surname"/>
</abp-column>
</abp-row>
<abp-input asp-for="Email" readonly="!isEmailUpdateEnabled"/>
<abp-input asp-for="PhoneNumber"/>
<abp-button type="submit" button-type="Primary" text="@L["Submit"].Value"/>
</form>

19
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.js

@ -0,0 +1,19 @@
(function ($) {
$(function () {
var l = abp.localization.getResource("AbpAccount");
$('#PersonalSettingsForm').submit(function (e) {
e.preventDefault();
if (!$('#PersonalSettingsForm').valid()) {
return false;
}
var input = $('#PersonalSettingsForm').serializeFormToObject();
volo.abp.identity.profile.update(input).then(function (result) {
abp.notify.success(l('PersonalSettingsSaved'));
});
});
});
})(jQuery);

73
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Manage.cshtml

@ -2,66 +2,33 @@
@using Microsoft.AspNetCore.Mvc.Localization
@using Volo.Abp.Account.Localization
@using Volo.Abp.Account.Web.Pages.Account
@using Volo.Abp.Identity.Settings
@using Volo.Abp.Settings
@using Volo.Abp.AspNetCore.Mvc.UI.Theming
@inject ISettingProvider SettingManager
@inject IThemeManager ThemeManager
@inject IHtmlLocalizer<AccountResource> L
@model ManageModel
@{
Layout = ThemeManager.CurrentTheme.GetApplicationLayout();
var isUserNameUpdateEnabled = string.Equals(await SettingManager.GetOrNullAsync(IdentitySettingNames.User.IsUserNameUpdateEnabled), "true",
StringComparison.OrdinalIgnoreCase);
var isEmailUpdateEnabled = string.Equals(await SettingManager.GetOrNullAsync(IdentitySettingNames.User.IsEmailUpdateEnabled), "true",
StringComparison.OrdinalIgnoreCase);
}
@section scripts {
<abp-script-bundle name="@typeof(ManageModel).FullName">
<abp-script src="/Pages/Account/Manage.js" />
</abp-script-bundle>
<abp-script-bundle name="@typeof(ManageModel).FullName"/>
}
<abp-card>
<abp-card-body>
<abp-tabs tab-style="PillVertical">
@if (!Model.DisablePasswordChange)
{
<abp-tab title="@L["ChangePassword"].Value">
<h4>@L["ChangePassword"].Value</h4><hr/>
<form id="ChangePasswordForm">
@if (!Model.HideOldPasswordInput)
{
<abp-input asp-for="ChangePasswordInfoModel.CurrentPassword"/>
}
<abp-input asp-for="ChangePasswordInfoModel.NewPassword"/>
<abp-input asp-for="ChangePasswordInfoModel.NewPasswordConfirm"/>
<abp-button type="submit" button-type="Primary" text="@L["Submit"].Value"/>
</form>
</abp-tab>
}
<abp-tab title="@L["PersonalSettings"].Value">
<h4>@L["PersonalSettings"].Value</h4><hr/>
<form method="post" id="PersonalSettingsForm">
<abp-input asp-for="PersonalSettingsInfoModel.UserName" readonly="!isUserNameUpdateEnabled"/>
<abp-row>
<abp-column size-md="_6">
<abp-input asp-for="PersonalSettingsInfoModel.Name"/>
</abp-column>
<abp-column size-md="_6">
<abp-input asp-for="PersonalSettingsInfoModel.Surname"/>
</abp-column>
</abp-row>
<abp-input asp-for="PersonalSettingsInfoModel.Email" readonly="!isEmailUpdateEnabled"/>
<abp-input asp-for="PersonalSettingsInfoModel.PhoneNumber"/>
<abp-button type="submit" button-type="Primary" text="@L["Submit"].Value"/>
</form>
</abp-tab>
</abp-tabs>
</abp-card-body>
</abp-card>
<div id="ProfileManagementWrapper">
<abp-card>
<abp-card-body>
<abp-tabs tab-style="PillVertical" vertical-header-size="_3">
@foreach (var group in Model.ProfileManagementPageCreationContext.Groups)
{
<abp-tab title="@group.DisplayName">
<h2>@group.DisplayName</h2>
<hr class="my-4" />
@await Component.InvokeAsync(group.ComponentType, new
{
parameter = group.Parameter
})
</abp-tab>
}
</abp-tabs>
</abp-card-body>
</abp-card>
</div>

72
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Manage.cshtml.cs

@ -2,35 +2,31 @@
using System.Threading.Tasks;
using Volo.Abp.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Volo.Abp.Account.Web.ProfileManagement;
using Volo.Abp.Validation;
namespace Volo.Abp.Account.Web.Pages.Account
{
public class ManageModel : AccountPageModel
{
public ChangePasswordInfoModel ChangePasswordInfoModel { get; set; }
public ProfileManagementPageCreationContext ProfileManagementPageCreationContext { get; private set; }
public PersonalSettingsInfoModel PersonalSettingsInfoModel { get; set; }
protected ProfileManagementPageOptions Options { get; }
public bool DisablePasswordChange { get; set; }
public bool HideOldPasswordInput { get; set; }
protected IProfileAppService ProfileAppService { get; }
public ManageModel(IProfileAppService profileAppService)
public ManageModel(IOptions<ProfileManagementPageOptions> options)
{
ProfileAppService = profileAppService;
Options = options.Value;
}
public virtual async Task<IActionResult> OnGetAsync()
{
var user = await ProfileAppService.GetAsync();
PersonalSettingsInfoModel = ObjectMapper.Map<ProfileDto, PersonalSettingsInfoModel>(user);
ProfileManagementPageCreationContext = new ProfileManagementPageCreationContext(ServiceProvider);
DisablePasswordChange = user.IsExternal;
HideOldPasswordInput = !user.HasPassword;
foreach (var contributor in Options.Contributors)
{
await contributor.ConfigureAsync(ProfileManagementPageCreationContext);
}
return Page();
}
@ -40,50 +36,4 @@ namespace Volo.Abp.Account.Web.Pages.Account
return Task.FromResult<IActionResult>(Page());
}
}
public class ChangePasswordInfoModel
{
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))]
[Display(Name = "DisplayName:CurrentPassword")]
[DataType(DataType.Password)]
public string CurrentPassword { get; set; }
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))]
[Display(Name = "DisplayName:NewPassword")]
[DataType(DataType.Password)]
public string NewPassword { get; set; }
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPasswordLength))]
[Display(Name = "DisplayName:NewPasswordConfirm")]
[DataType(DataType.Password)]
public string NewPasswordConfirm { get; set; }
}
public class PersonalSettingsInfoModel
{
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxUserNameLength))]
[Display(Name = "DisplayName:UserName")]
public string UserName { get; set; }
[Required]
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxEmailLength))]
[Display(Name = "DisplayName:Email")]
public string Email { get; set; }
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxNameLength))]
[Display(Name = "DisplayName:Name")]
public string Name { get; set; }
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxSurnameLength))]
[Display(Name = "DisplayName:Surname")]
public string Surname { get; set; }
[DynamicStringLength(typeof(IdentityUserConsts), nameof(IdentityUserConsts.MaxPhoneNumberLength))]
[Display(Name = "DisplayName:PhoneNumber")]
public string PhoneNumber { get; set; }
}
}

47
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Manage.js

@ -1,47 +0,0 @@
(function ($) {
var l = abp.localization.getResource('AbpAccount');
var _profileService = volo.abp.identity.profile;
$('#ChangePasswordForm').submit(function (e) {
e.preventDefault();
if (!$('#ChangePasswordForm').valid()) {
return false;
}
var input = $('#ChangePasswordForm').serializeFormToObject()
.changePasswordInfoModel;
if (
input.newPassword != input.newPasswordConfirm ||
input.newPassword == ''
) {
abp.message.error(l('NewPasswordConfirmFailed'));
return;
}
if (input.currentPassword && input.currentPassword == ''){
return;
}
_profileService.changePassword(input).then(function (result) {
abp.message.success(l('PasswordChanged'));
});
});
$('#PersonalSettingsForm').submit(function (e) {
e.preventDefault();
if (!$('#PersonalSettingsForm').valid()) {
return false;
}
var input = $('#PersonalSettingsForm').serializeFormToObject()
.personalSettingsInfoModel;
_profileService.update(input).then(function (result) {
abp.notify.success(l('PersonalSettingsSaved'));
});
});
})(jQuery);

1
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs

@ -18,7 +18,6 @@ namespace Volo.Abp.Account.Web.Pages.Account
{
public class RegisterModel : AccountPageModel
{
protected IAccountAppService AccountAppService { get; }
[BindProperty(SupportsGet = true)]
public string ReturnUrl { get; set; }

48
modules/account/src/Volo.Abp.Account.Web/ProfileManagement/AccountProfileManagementPageContributor.cs

@ -0,0 +1,48 @@
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Volo.Abp.Account.Localization;
using Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.Password;
using Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.PersonalInfo;
using Volo.Abp.Identity;
using Volo.Abp.Users;
namespace Volo.Abp.Account.Web.ProfileManagement
{
public class AccountProfileManagementPageContributor : IProfileManagementPageContributor
{
public async Task ConfigureAsync(ProfileManagementPageCreationContext context)
{
var l = context.ServiceProvider.GetRequiredService<IStringLocalizer<AccountResource>>();
if (await IsPasswordChangeEnabled(context))
{
context.Groups.Add(
new ProfileManagementPageGroup(
"Volo.Abp.Account.Password",
l["ProfileTab:Password"],
typeof(AccountProfilePasswordManagementGroupViewComponent)
)
);
}
context.Groups.Add(
new ProfileManagementPageGroup(
"Volo.Abp.Account.PersonalInfo",
l["ProfileTab:PersonalInfo"],
typeof(AccountProfilePersonalInfoManagementGroupViewComponent)
)
);
}
protected virtual async Task<bool> IsPasswordChangeEnabled(ProfileManagementPageCreationContext context)
{
var userManager = context.ServiceProvider.GetRequiredService<IdentityUserManager>();
var currentUser = context.ServiceProvider.GetRequiredService<ICurrentUser>();
var user = await userManager.GetByIdAsync(currentUser.GetId());
return !user.IsExternal;
}
}
}

9
modules/account/src/Volo.Abp.Account.Web/ProfileManagement/IProfileManagementPageContributor.cs

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Volo.Abp.Account.Web.ProfileManagement
{
public interface IProfileManagementPageContributor
{
Task ConfigureAsync(ProfileManagementPageCreationContext context);
}
}

19
modules/account/src/Volo.Abp.Account.Web/ProfileManagement/ProfileManagementPageCreationContext.cs

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
namespace Volo.Abp.Account.Web.ProfileManagement
{
public class ProfileManagementPageCreationContext
{
public IServiceProvider ServiceProvider { get; }
public List<ProfileManagementPageGroup> Groups { get; }
public ProfileManagementPageCreationContext(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
Groups = new List<ProfileManagementPageGroup>();
}
}
}

39
modules/account/src/Volo.Abp.Account.Web/ProfileManagement/ProfileManagementPageGroup.cs

@ -0,0 +1,39 @@
using System;
using JetBrains.Annotations;
namespace Volo.Abp.Account.Web.ProfileManagement
{
public class ProfileManagementPageGroup
{
public string Id
{
get => _id;
set => _id = Check.NotNullOrWhiteSpace(value, nameof(Id));
}
private string _id;
public string DisplayName
{
get => _displayName;
set => _displayName = Check.NotNullOrWhiteSpace(value, nameof(DisplayName));
}
private string _displayName;
public Type ComponentType
{
get => _componentType;
set => _componentType = Check.NotNull(value, nameof(ComponentType));
}
private Type _componentType;
public object Parameter { get; set; }
public ProfileManagementPageGroup([NotNull] string id, [NotNull] string displayName, [NotNull] Type componentType, object parameter = null)
{
Id = id;
DisplayName = displayName;
ComponentType = componentType;
Parameter = parameter;
}
}
}

14
modules/account/src/Volo.Abp.Account.Web/ProfileManagement/ProfileManagementPageOptions.cs

@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace Volo.Abp.Account.Web.ProfileManagement
{
public class ProfileManagementPageOptions
{
public List<IProfileManagementPageContributor> Contributors { get; }
public ProfileManagementPageOptions()
{
Contributors = new List<IProfileManagementPageContributor>();
}
}
}

7
modules/cms-kit/host/Volo.CmsKit.HttpApi.Host/CmsKitHttpApiHostModule.cs

@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Security.Claims;
using IdentityModel;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.DataProtection;
@ -108,12 +109,12 @@ namespace Volo.CmsKit
AbpClaimTypes.Role = JwtClaimTypes.Role;
AbpClaimTypes.Email = JwtClaimTypes.Email;
context.Services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = configuration["AuthServer:Authority"];
options.RequireHttpsMetadata = false;
options.ApiName = "CmsKit";
options.Audience = "CmsKit";
});
Configure<AbpDistributedCacheOptions>(options =>

3
modules/cms-kit/host/Volo.CmsKit.HttpApi.Host/Volo.CmsKit.HttpApi.Host.csproj

@ -13,7 +13,8 @@
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
<PackageReference Include="IdentityModel" Version="4.3.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.6" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="3.1.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.6" />
<PackageReference Include="Volo.Abp.Autofac" Version="3.0.3" />

4
modules/cms-kit/host/Volo.CmsKit.IdentityServer/CmsKitIdentityServerModule.cs

@ -123,11 +123,11 @@ namespace Volo.CmsKit
});
context.Services.AddAuthentication()
.AddIdentityServerAuthentication(options =>
.AddJwtBearer(options =>
{
options.Authority = configuration["AuthServer:Authority"];
options.RequireHttpsMetadata = false;
options.ApiName = configuration["AuthServer:ApiName"];
options.Audience = configuration["AuthServer:ApiName"];
});
Configure<AbpDistributedCacheOptions>(options =>

1
modules/cms-kit/host/Volo.CmsKit.IdentityServer/Volo.CmsKit.IdentityServer.csproj

@ -11,7 +11,6 @@
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="3.1.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.6" />
<PackageReference Include="Volo.Abp.AspNetCore.Authentication.JwtBearer" Version="3.0.3" />

1
modules/cms-kit/host/Volo.CmsKit.Web.Unified/CmsKitWebUnifiedModule.cs

@ -1,4 +1,5 @@
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

1235
modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200826063729_CmsRatings_Added.Designer.cs

File diff suppressed because it is too large

39
modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200826063729_CmsRatings_Added.cs

@ -0,0 +1,39 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Volo.CmsKit.Migrations
{
public partial class CmsRatings_Added : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "CmsRatings",
columns: table => new
{
Id = table.Column<Guid>(nullable: false),
TenantId = table.Column<Guid>(nullable: true),
EntityType = table.Column<string>(maxLength: 64, nullable: false),
EntityId = table.Column<string>(maxLength: 64, nullable: false),
StarCount = table.Column<short>(nullable: false),
CreatorId = table.Column<Guid>(nullable: false),
CreationTime = table.Column<DateTime>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CmsRatings", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_CmsRatings_TenantId_EntityType_EntityId",
table: "CmsRatings",
columns: new[] { "TenantId", "EntityType", "EntityId" });
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CmsRatings");
}
}
}

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

File diff suppressed because it is too large

9
modules/cms-kit/host/Volo.CmsKit.Web.Unified/Pages/Index.cshtml

@ -5,6 +5,7 @@
@using Volo.CmsKit.GlobalFeatures
@using Volo.CmsKit.Pages
@using Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Commenting
@using Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Rating
@using Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.ReactionSelection
@model IndexModel
@inject IStringLocalizer<AbpUiResource> Localizer
@ -41,6 +42,12 @@
</abp-card-body>
</abp-card>
<hr>
@if (GlobalFeatureManager.Instance.IsEnabled<RatingsFeature>())
{
@await Component.InvokeAsync(typeof(RatingViewComponent), new { entityType = "quote", entityId = "2" })
}
<hr>
@if (GlobalFeatureManager.Instance.IsEnabled<ReactionsFeature>())
{
@await Component.InvokeAsync(typeof(ReactionSelectionViewComponent), new { entityType = "quote", entityId = "2" })
@ -49,4 +56,4 @@
{
@await Component.InvokeAsync(typeof(CommentingViewComponent), new { entityType = "quote", entityId = "2" })
}
<hr class="my-5" />
<hr class="my-5" />

37
modules/cms-kit/host/Volo.CmsKit.Web.Unified/wwwroot/libs/star-rating-svg/css/star-rating-svg.css

@ -0,0 +1,37 @@
.jq-stars {
display: inline-block;
}
.jq-rating-label {
font-size: 22px;
display: inline-block;
position: relative;
vertical-align: top;
font-family: helvetica, arial, verdana;
}
.jq-star {
width: 100px;
height: 100px;
display: inline-block;
cursor: pointer;
}
.jq-star-svg {
width: 100%;
height: 100% ;
}
.jq-star:hover .fs-star-svg polygon {
}
.jq-star-svg polygon {
stroke: #000;
stroke-linejoin: round;
}
/* un-used */
.jq-shadow {
-webkit-filter: drop-shadow( -2px -2px 2px #888 );
filter: drop-shadow( -2px -2px 2px #888 );
}

1
modules/cms-kit/host/Volo.CmsKit.Web.Unified/wwwroot/libs/star-rating-svg/js/jquery.star-rating-svg.min.js

@ -0,0 +1 @@
!function(a){"use strict";var b="starRating",c=function(){},d={totalStars:5,useFullStars:!1,emptyColor:"lightgray",hoverColor:"orange",activeColor:"gold",useGradient:!0,readOnly:!1,disableAfterRate:!0,starGradient:{start:"#FEF7CD",end:"#FF9511"},strokeWidth:0,strokeColor:"black",initialRating:0,starSize:40,callback:c,onHover:c,onLeave:c},e=function(c,e){var f;this.element=c,this.$el=a(c),this.settings=a.extend({},d,e),f=this.$el.data("rating")||this.settings.initialRating,this._state={rating:(Math.round(2*f)/2).toFixed(1)},this._uid=Math.floor(999*Math.random()),e.starGradient||this.settings.useGradient||(this.settings.starGradient.start=this.settings.starGradient.end=this.settings.activeColor),this._defaults=d,this._name=b,this.init()},f={init:function(){this.renderMarkup(),this.addListeners(),this.initRating()},addListeners:function(){this.settings.readOnly||(this.$stars.on("mouseover",this.hoverRating.bind(this)),this.$stars.on("mouseout",this.restoreState.bind(this)),this.$stars.on("click",this.handleRating.bind(this)))},hoverRating:function(a){var b=this.getIndex(a);this.paintStars(b,"hovered"),this.settings.onHover(b+1,this._state.rating,this.$el)},handleRating:function(a){var b=this.getIndex(a),c=b+1;this.applyRating(c,this.$el),this.executeCallback(c,this.$el),this.settings.disableAfterRate&&this.$stars.off()},applyRating:function(a){var b=a-1;this.paintStars(b,"active"),this._state.rating=b+1},restoreState:function(a){var b=this.getIndex(a),c=this._state.rating||-1;this.paintStars(c-1,"active"),this.settings.onLeave(b+1,this._state.rating,this.$el)},getIndex:function(b){var c=a(b.currentTarget),d=c.width(),e=a(b.target).attr("data-side");e=e?e:this.getOffsetByPixel(b,c,d),e=this.settings.useFullStars?"right":e;var f=c.index()-("left"===e?.5:0);return f=.5>f&&b.offsetX<d/4?-1:f},getOffsetByPixel:function(a,b,c){var d=a.pageX-b.offset().left;return c/2>=d&&!this.settings.useFullStars?"left":"right"},initRating:function(){this.paintStars(this._state.rating-1,"active")},paintStars:function(b,c){var d,e,f,g;a.each(this.$stars,function(h,i){d=a(i).find('polygon[data-side="left"]'),e=a(i).find('polygon[data-side="right"]'),f=g=b>=h?c:"empty",f=h-b===.5?c:f,d.attr("class","svg-"+f+"-"+this._uid),e.attr("class","svg-"+g+"-"+this._uid)}.bind(this))},renderMarkup:function(){for(var a='<div class="jq-star" style="width:'+this.settings.starSize+"px; height:"+this.settings.starSize+'px;"><svg version="1.0" class="jq-star-svg" shape-rendering="geometricPrecision" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="305px" height="305px" viewBox="60 -62 309 309" style="enable-background:new 64 -59 305 305; stroke-width:'+this.settings.strokeWidth+'px;" xml:space="preserve"><style type="text/css">.svg-empty-'+this._uid+"{fill:url(#"+this._uid+"_SVGID_1_);}.svg-hovered-"+this._uid+"{fill:url(#"+this._uid+"_SVGID_2_);}.svg-active-"+this._uid+"{fill:url(#"+this._uid+"_SVGID_3_);}</style>"+this.getLinearGradient(this._uid+"_SVGID_1_",this.settings.emptyColor,this.settings.emptyColor)+this.getLinearGradient(this._uid+"_SVGID_2_",this.settings.hoverColor,this.settings.hoverColor)+this.getLinearGradient(this._uid+"_SVGID_3_",this.settings.starGradient.start,this.settings.starGradient.end)+'<polygon data-side="left" class="svg-empty-'+this._uid+'" points="281.1,129.8 364,55.7 255.5,46.8 214,-59 172.5,46.8 64,55.4 146.8,129.7 121.1,241 213.9,181.1 213.9,181 306.5,241 " style="stroke: '+this.settings.strokeColor+'"/><polygon data-side="right" class="svg-empty-'+this._uid+'" points="364,55.7 255.5,46.8 214,-59 213.9,181 306.5,241 281.1,129.8 " style="stroke-dasharray: 230 232 210 0; stroke: '+this.settings.strokeColor+'"/></svg></div>',b="",c=0;c<this.settings.totalStars;c++)b+=a;this.$el.append(b),this.$stars=this.$el.find(".jq-star")},getLinearGradient:function(a,b,c){return'<linearGradient id="'+a+'" gradientUnits="userSpaceOnUse" x1="121.1501" y1="-70.35" x2="121.15" y2="125.0045"><stop offset="0" style="stop-color:'+b+'"/><stop offset="1" style="stop-color:'+c+'"/> </linearGradient>'},executeCallback:function(a,b){var c=this.settings.callback;c(a,b)}},g={unload:function(){var c="plugin_"+b,d=a(this),e=d.data(c).$stars;e.off(),d.removeData(c).remove()},setRating:function(c,d){var e="plugin_"+b,f=a(this),g=f.data(e);c>g.settings.totalStars||0>c||(d&&(c=Math.round(c)),g.applyRating(c))},getRating:function(){var c="plugin_"+b,d=a(this),e=d.data(c);return e._state.rating}};a.extend(e.prototype,f),a.fn[b]=function(c){if(!a.isPlainObject(c)){if(g.hasOwnProperty(c))return g[c].apply(this,Array.prototype.slice.call(arguments,1));a.error("Method "+c+" does not exist on "+b+".js")}return this.each(function(){a.data(this,"plugin_"+b)||a.data(this,"plugin_"+b,new e(this,c))})}}(jQuery,window,document);

3
modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/GlobalFeatures/GlobalCmsKitFeatures.cs

@ -11,11 +11,14 @@ namespace Volo.CmsKit.GlobalFeatures
public CommentsFeature Comments => GetFeature<CommentsFeature>();
public RatingsFeature Ratings => GetFeature<RatingsFeature>();
public GlobalCmsKitFeatures([NotNull] GlobalFeatureManager featureManager)
: base(featureManager)
{
AddFeature(new ReactionsFeature(this));
AddFeature(new CommentsFeature(this));
AddFeature(new RatingsFeature(this));
}
}
}

18
modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/GlobalFeatures/RatingsFeature.cs

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

6
modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json

@ -13,6 +13,10 @@
"LoginToAddComment": "Login to add comment",
"LoginToReply": "Login to reply",
"MessageDeletionConfirmationMessage": "This comment will be deleted completely.",
"CommentAuthorizationExceptionMessage": "Those comments are not allowed for public display."
"CommentAuthorizationExceptionMessage": "Those comments are not allowed for public display.",
"Undo": "Undo",
"RatingUndoMessage": "Your rating will be undo.",
"LoginToRate": "Login to rate",
"Star": "Star"
}
}

6
modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/tr.json

@ -13,6 +13,10 @@
"LoginToAddComment": "Yorum yapmak için giriş yap",
"LoginToReply": "Cevap vermek için giriş yap",
"MessageDeletionConfirmationMessage": "Bu yorum tamamen silinecektir",
"CommentAuthorizationExceptionMessage": "Bu yorumları görebilmek için yetki gerekir."
"CommentAuthorizationExceptionMessage": "Bu yorumları görebilmek için yetki gerekir.",
"Undo": "Geri al",
"RatingUndoMessage": "Oylamanız geri alınacak.",
"LoginToRate": "Oylamak için giriş yapın",
"Star": "Yıldız"
}
}

15
modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Ratings/RatingConsts.cs

@ -0,0 +1,15 @@
using Volo.CmsKit.Entities;
namespace Volo.CmsKit.Ratings
{
public static class RatingConsts
{
public static int MaxEntityTypeLength { get; set; } = CmsEntityConsts.MaxEntityTypeLength;
public static int MaxEntityIdLength { get; set; } = CmsEntityConsts.MaxEntityIdLength;
public static int MaxStarCount { get; set; } = 5;
public static int MinStarCount { get; set; } = 1;
}
}

25
modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Ratings/IRatingRepository.cs

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Volo.Abp.Domain.Repositories;
namespace Volo.CmsKit.Ratings
{
public interface IRatingRepository : IBasicRepository<Rating, Guid>
{
Task<Rating> GetCurrentUserRatingAsync(
[NotNull] string entityType,
[NotNull] string entityId,
Guid userId,
CancellationToken cancellationToken = default
);
Task<List<RatingWithStarCountQueryResultItem>> GetGroupedStarCountsAsync(
[NotNull] string entityType,
[NotNull] string entityId,
CancellationToken cancellationToken = default
);
}
}

57
modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Ratings/Rating.cs

@ -0,0 +1,57 @@
using System;
using JetBrains.Annotations;
using Volo.Abp;
using Volo.Abp.Auditing;
using Volo.Abp.Domain.Entities;
namespace Volo.CmsKit.Ratings
{
public class Rating : BasicAggregateRoot<Guid>, IHasCreationTime, IMustHaveCreator
{
public virtual Guid? TenantId { get; protected set; }
public virtual string EntityType { get; protected set; }
public virtual string EntityId { get; protected set; }
public virtual short StarCount { get; protected set; }
public virtual Guid CreatorId { get; set; }
public virtual DateTime CreationTime { get; set; }
protected Rating()
{
}
public Rating(
Guid id,
[NotNull] string entityType,
[NotNull] string entityId,
short starCount,
Guid creatorId,
Guid? tenantId = null
)
: base(id)
{
EntityType = Check.NotNullOrWhiteSpace(entityType, nameof(entityType), RatingConsts.MaxEntityTypeLength);
EntityId = Check.NotNullOrWhiteSpace(entityId, nameof(entityId), RatingConsts.MaxEntityIdLength);
SetStarCount(starCount);
CreatorId = creatorId;
TenantId = tenantId;
}
public virtual void SetStarCount(short starCount)
{
if(starCount <= RatingConsts.MaxStarCount && starCount >= RatingConsts.MinStarCount)
{
StarCount = starCount;
}
else
{
throw new ArgumentOutOfRangeException($"Choosen star must between {RatingConsts.MinStarCount} and {RatingConsts.MaxStarCount}");
}
}
}
}

9
modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Ratings/RatingWithStarCountQueryResultItem.cs

@ -0,0 +1,9 @@
namespace Volo.CmsKit.Ratings
{
public class RatingWithStarCountQueryResultItem
{
public short StarCount { get; set; }
public int Count { get; set; }
}
}

17
modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitDbContextModelCreatingExtensions.cs

@ -8,6 +8,7 @@ using Volo.CmsKit.Reactions;
using Volo.CmsKit.Users;
using Volo.Abp.Users.EntityFrameworkCore;
using Volo.CmsKit.GlobalFeatures;
using Volo.CmsKit.Ratings;
namespace Volo.CmsKit.EntityFrameworkCore
{
@ -71,6 +72,22 @@ namespace Volo.CmsKit.EntityFrameworkCore
b.HasIndex(x => new { x.TenantId, x.RepliedCommentId });
});
}
if (GlobalFeatureManager.Instance.IsEnabled<RatingsFeature>())
{
builder.Entity<Rating>(r =>
{
r.ToTable(options.TablePrefix + "Ratings", options.Schema);
r.ConfigureByConvention();
r.Property(x => x.StarCount).IsRequired();
r.Property(x => x.EntityType).IsRequired().HasMaxLength(RatingConsts.MaxEntityTypeLength);
r.Property(x => x.EntityId).IsRequired().HasMaxLength(RatingConsts.MaxEntityIdLength);
r.HasIndex(x => new {x.TenantId, x.EntityType, x.EntityId, x.CreatorId});
});
}
}
}
}

2
modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitEntityFrameworkCoreModule.cs

@ -3,6 +3,7 @@ using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Modularity;
using Volo.Abp.Users.EntityFrameworkCore;
using Volo.CmsKit.Comments;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Reactions;
using Volo.CmsKit.Users;
@ -22,6 +23,7 @@ namespace Volo.CmsKit.EntityFrameworkCore
options.AddRepository<CmsUser, EfCoreCmsUserRepository>();
options.AddRepository<UserReaction, EfCoreUserReactionRepository>();
options.AddRepository<Comment, EfCoreCommentRepository>();
options.AddRepository<Rating, EfCoreRatingRepository>();
});
}
}

57
modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Ratings/EfCoreRatingRepository.cs

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Volo.Abp;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
using Volo.CmsKit.EntityFrameworkCore;
namespace Volo.CmsKit.Ratings
{
public class EfCoreRatingRepository : EfCoreRepository<ICmsKitDbContext, Rating, Guid>, IRatingRepository
{
public EfCoreRatingRepository(IDbContextProvider<ICmsKitDbContext> dbContextProvider) : base(dbContextProvider)
{
}
public async Task<Rating> GetCurrentUserRatingAsync(string entityType, string entityId, Guid userId,
CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(entityType, nameof(entityType));
Check.NotNullOrWhiteSpace(entityId, nameof(entityId));
var rating = await DbSet.FirstOrDefaultAsync(
r => r.EntityType == entityType && r.EntityId == entityId && r.CreatorId == userId,
GetCancellationToken(cancellationToken));
return rating;
}
public async Task<List<RatingWithStarCountQueryResultItem>> GetGroupedStarCountsAsync(string entityType,
string entityId, CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(entityType, nameof(entityType));
Check.NotNullOrWhiteSpace(entityId, nameof(entityId));
var query = (
from rating in DbSet
where rating.EntityType == entityType && rating.EntityId == entityId
group rating by rating.StarCount
into g
select new RatingWithStarCountQueryResultItem
{
StarCount = g.Key,
Count = g.Count()
}
).OrderByDescending(r => r.StarCount);
var ratings = await query.ToListAsync(GetCancellationToken(cancellationToken));
return ratings;
}
}
}

3
modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/CmsKitMongoDbContext.cs

@ -2,6 +2,7 @@
using Volo.Abp.Data;
using Volo.Abp.MongoDB;
using Volo.CmsKit.Comments;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Reactions;
using Volo.CmsKit.Users;
@ -16,6 +17,8 @@ namespace Volo.CmsKit.MongoDB
public IMongoCollection<CmsUser> CmsUsers => Collection<CmsUser>();
public IMongoCollection<Rating> Ratings => Collection<Rating>();
protected override void CreateModel(IMongoModelBuilder modelBuilder)
{
base.CreateModel(modelBuilder);

6
modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/CmsKitMongoDbContextExtensions.cs

@ -2,6 +2,7 @@
using Volo.Abp;
using Volo.Abp.MongoDB;
using Volo.CmsKit.Comments;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Reactions;
using Volo.CmsKit.Users;
@ -35,6 +36,11 @@ namespace Volo.CmsKit.MongoDB
{
x.CollectionName = CmsKitDbProperties.DbTablePrefix + "Comments";
});
builder.Entity<Rating>(x =>
{
x.CollectionName = CmsKitDbProperties.DbTablePrefix + "Ratings";
});
}
}
}

3
modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/CmsKitMongoDbModule.cs

@ -4,8 +4,10 @@ using Volo.Abp.MongoDB;
using Volo.Abp.Users.MongoDB;
using Volo.CmsKit.Comments;
using Volo.CmsKit.MongoDB.Comments;
using Volo.CmsKit.MongoDB.Ratings;
using Volo.CmsKit.MongoDB.Reactions;
using Volo.CmsKit.MongoDB.Users;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Reactions;
using Volo.CmsKit.Users;
@ -25,6 +27,7 @@ namespace Volo.CmsKit.MongoDB
options.AddRepository<CmsUser, MongoCmsUserRepository>();
options.AddRepository<UserReaction, MongoUserReactionRepository>();
options.AddRepository<Comment, MongoCommentRepository>();
options.AddRepository<Rating, MongoRatingRepository>();
});
}
}

3
modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/ICmsKitMongoDbContext.cs

@ -2,6 +2,7 @@
using Volo.Abp.Data;
using Volo.Abp.MongoDB;
using Volo.CmsKit.Comments;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Reactions;
using Volo.CmsKit.Users;
@ -15,5 +16,7 @@ namespace Volo.CmsKit.MongoDB
IMongoCollection<Comment> Comments { get; }
IMongoCollection<CmsUser> CmsUsers { get; }
IMongoCollection<Rating> Ratings { get; }
}
}

58
modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Ratings/MongoRatingRepository.cs

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Volo.Abp;
using Volo.Abp.Domain.Repositories.MongoDB;
using Volo.Abp.MongoDB;
using Volo.CmsKit.Ratings;
namespace Volo.CmsKit.MongoDB.Ratings
{
public class MongoRatingRepository : MongoDbRepository<ICmsKitMongoDbContext, Rating, Guid>, IRatingRepository
{
public MongoRatingRepository(IMongoDbContextProvider<ICmsKitMongoDbContext> dbContextProvider) : base(
dbContextProvider)
{
}
public async Task<Rating> GetCurrentUserRatingAsync(string entityType, string entityId, Guid userId,
CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(entityType, nameof(entityType));
Check.NotNullOrWhiteSpace(entityId, nameof(entityId));
var rating = await GetMongoQueryable()
.FirstOrDefaultAsync(r => r.EntityType == entityType && r.EntityId == entityId && r.CreatorId == userId,
GetCancellationToken(cancellationToken));
return rating;
}
public async Task<List<RatingWithStarCountQueryResultItem>> GetGroupedStarCountsAsync(string entityType,
string entityId, CancellationToken cancellationToken = default)
{
Check.NotNullOrWhiteSpace(entityType, nameof(entityType));
Check.NotNullOrWhiteSpace(entityId, nameof(entityId));
var query = (
from rating in GetMongoQueryable()
where rating.EntityType == entityType && rating.EntityId == entityId
group rating by rating.StarCount
into g
select new RatingWithStarCountQueryResultItem
{
StarCount = g.Key,
Count = g.Count()
}
).OrderByDescending(r => r.StarCount);
var ratings = await query.ToListAsync(GetCancellationToken(cancellationToken));
return ratings;
}
}
}

30
modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo.CmsKit.Public.Application.Contracts.csproj

@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\common.props" />
<Import Project="..\..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.CmsKit.Common.Application.Contracts\Volo.CmsKit.Common.Application.Contracts.csproj" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\common.props" />
<Import Project="..\..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.CmsKit.Common.Application.Contracts\Volo.CmsKit.Common.Application.Contracts.csproj" />
</ItemGroup>
</Project>

13
modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Ratings/CreateUpdateRatingInput.cs

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
using Volo.Abp.Validation;
using Volo.CmsKit.Ratings;
namespace Volo.CmsKit.Public.Ratings
{
public class CreateUpdateRatingInput
{
[Required]
[DynamicRange(typeof(RatingConsts), typeof(int), nameof(RatingConsts.MinStarCount), nameof(RatingConsts.MaxStarCount))]
public short StarCount { get; set; }
}
}

19
modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Ratings/IRatingPublicAppService.cs

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Volo.CmsKit.Public.Ratings
{
public interface IRatingPublicAppService : IApplicationService
{
Task<RatingDto> CreateAsync(string entityType, string entityId, CreateUpdateRatingInput input);
Task DeleteAsync(string entityType, string entityId);
Task<RatingDto> GetCurrentUserRatingAsync(string entityType, string entityId);
Task<List<RatingWithStarCountDto>> GetGroupedStarCountsAsync(string entityType, string entityId);
}
}

19
modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Ratings/RatingDto.cs

@ -0,0 +1,19 @@
using System;
namespace Volo.CmsKit.Public.Ratings
{
public class RatingDto
{
public Guid Id { get; set; }
public string EntityType { get; set; }
public string EntityId { get; set; }
public short StarCount { get; set; }
public Guid CreatorId { get; set; }
public DateTime CreationTime { get; set; }
}
}

11
modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Ratings/RatingWithStarCountDto.cs

@ -0,0 +1,11 @@
namespace Volo.CmsKit.Public.Ratings
{
public class RatingWithStarCountDto
{
public short StarCount { get; set; }
public int Count { get; set; }
public bool IsSelectedByCurrentUser { get; set; }
}
}

32
modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo.CmsKit.Public.Application.csproj

@ -1,16 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\common.props" />
<Import Project="..\..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.CmsKit.Common.Application\Volo.CmsKit.Common.Application.csproj" />
<ProjectReference Include="..\Volo.CmsKit.Public.Application.Contracts\Volo.CmsKit.Public.Application.Contracts.csproj" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\common.props" />
<Import Project="..\..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.CmsKit.Common.Application\Volo.CmsKit.Common.Application.csproj" />
<ProjectReference Include="..\Volo.CmsKit.Public.Application.Contracts\Volo.CmsKit.Public.Application.Contracts.csproj" />
</ItemGroup>
</Project>

4
modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/PublicApplicationAutoMapperProfile.cs

@ -2,6 +2,8 @@
using Volo.Abp.AutoMapper;
using Volo.CmsKit.Comments;
using Volo.CmsKit.Public.Comments;
using Volo.CmsKit.Public.Ratings;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Users;
namespace Volo.CmsKit.Public
@ -18,6 +20,8 @@ namespace Volo.CmsKit.Public
CreateMap<Comment, CommentWithDetailsDto>()
.Ignore(x=> x.Replies)
.Ignore(x=> x.Author);
CreateMap<Rating, RatingDto>();
}
}
}

104
modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Ratings/RatingPublicAppService.cs

@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Authorization;
using Volo.Abp.Users;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Users;
namespace Volo.CmsKit.Public.Ratings
{
public class RatingPublicAppService : ApplicationService, IRatingPublicAppService
{
protected IRatingRepository RatingRepository { get; }
public ICmsUserLookupService CmsUserLookupService { get; }
public RatingPublicAppService(IRatingRepository ratingRepository, ICmsUserLookupService cmsUserLookupService)
{
RatingRepository = ratingRepository;
CmsUserLookupService = cmsUserLookupService;
}
[Authorize]
public virtual async Task<RatingDto> CreateAsync(string entityType, string entityId,
CreateUpdateRatingInput input)
{
var userId = CurrentUser.GetId();
var user = await CmsUserLookupService.GetByIdAsync(userId);
var currentUserRating = await RatingRepository.GetCurrentUserRatingAsync(entityType, entityId, userId);
if (currentUserRating != null)
{
currentUserRating.SetStarCount(input.StarCount);
var updatedRating = await RatingRepository.UpdateAsync(currentUserRating);
return ObjectMapper.Map<Rating, RatingDto>(updatedRating);
}
var rating = await RatingRepository.InsertAsync(
new Rating(
GuidGenerator.Create(),
entityType,
entityId,
input.StarCount,
user.Id,
CurrentTenant.Id
)
);
return ObjectMapper.Map<Rating, RatingDto>(rating);
}
[Authorize]
public virtual async Task DeleteAsync(string entityType, string entityId)
{
var rating = await RatingRepository.GetCurrentUserRatingAsync(entityType, entityId, CurrentUser.GetId());
if (rating.CreatorId != CurrentUser.GetId())
{
throw new AbpAuthorizationException();
}
await RatingRepository.DeleteAsync(rating.Id);
}
[Authorize]
public virtual async Task<RatingDto> GetCurrentUserRatingAsync(string entityType, string entityId)
{
var currentUserId = CurrentUser.GetId();
var rating = await RatingRepository.GetCurrentUserRatingAsync(entityType, entityId, currentUserId);
return ObjectMapper.Map<Rating, RatingDto>(rating);
}
public virtual async Task<List<RatingWithStarCountDto>> GetGroupedStarCountsAsync(string entityType,
string entityId)
{
var ratings = await RatingRepository.GetGroupedStarCountsAsync(entityType, entityId);
var userRatingOrNull = CurrentUser.IsAuthenticated
? await RatingRepository.GetCurrentUserRatingAsync(entityType, entityId, CurrentUser.GetId())
: null;
var ratingWithStarCountDto = new List<RatingWithStarCountDto>();
foreach (var rating in ratings)
{
ratingWithStarCountDto.Add(
new RatingWithStarCountDto
{
StarCount = rating.StarCount,
Count = rating.Count,
IsSelectedByCurrentUser = userRatingOrNull != null && userRatingOrNull.StarCount == rating.StarCount
});
}
return ratingWithStarCountDto;
}
}
}

32
modules/cms-kit/src/Volo.CmsKit.Public.HttpApi/Volo.CmsKit.Public.HttpApi.csproj

@ -1,16 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\common.props" />
<Import Project="..\..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.CmsKit.Common.HttpApi\Volo.CmsKit.Common.HttpApi.csproj" />
<ProjectReference Include="..\Volo.CmsKit.Public.Application.Contracts\Volo.CmsKit.Public.Application.Contracts.csproj" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\common.props" />
<Import Project="..\..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.CmsKit.Common.HttpApi\Volo.CmsKit.Common.HttpApi.csproj" />
<ProjectReference Include="..\Volo.CmsKit.Public.Application.Contracts\Volo.CmsKit.Public.Application.Contracts.csproj" />
</ItemGroup>
</Project>

51
modules/cms-kit/src/Volo.CmsKit.Public.HttpApi/Volo/CmsKit/Public/Ratings/RatingPublicController.cs

@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp;
using Volo.Abp.GlobalFeatures;
using Volo.CmsKit.GlobalFeatures;
namespace Volo.CmsKit.Public.Ratings
{
[RequiresGlobalFeature(typeof(RatingsFeature))]
[RemoteService(Name = CmsKitPublicRemoteServiceConsts.RemoteServiceName)]
[Area("cms-kit")]
[Route("api/cms-kit-public/ratings")]
public class RatingPublicController : CmsKitPublicControllerBase, IRatingPublicAppService
{
protected IRatingPublicAppService RatingPublicAppService { get; }
public RatingPublicController(IRatingPublicAppService ratingPublicAppService)
{
RatingPublicAppService = ratingPublicAppService;
}
[HttpPut]
[Route("{entityType}/{entityId}")]
public virtual Task<RatingDto> CreateAsync(string entityType, string entityId, CreateUpdateRatingInput input)
{
return RatingPublicAppService.CreateAsync(entityType, entityId, input);
}
[HttpDelete]
[Route("{entityType}/{entityId}")]
public virtual Task DeleteAsync(string entityType, string entityId)
{
return RatingPublicAppService.DeleteAsync(entityType, entityId);
}
[HttpGet]
[Route("{entityType}/{entityId}")]
public virtual Task<RatingDto> GetCurrentUserRatingAsync(string entityType, string entityId)
{
return RatingPublicAppService.GetCurrentUserRatingAsync(entityType, entityId);
}
[HttpGet]
[Route("{entityType}/{entityId}")]
public virtual Task<List<RatingWithStarCountDto>> GetGroupedStarCountsAsync(string entityType, string entityId)
{
return RatingPublicAppService.GetGroupedStarCountsAsync(entityType, entityId);
}
}
}

6
modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicWidgetsController.cs

@ -1,6 +1,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Commenting;
using Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Rating;
using Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.ReactionSelection;
namespace Volo.CmsKit.Public.Web.Controllers
@ -16,5 +17,10 @@ namespace Volo.CmsKit.Public.Web.Controllers
{
return Task.FromResult((IActionResult)ViewComponent(typeof(CommentingViewComponent), new {entityType, entityId}));
}
public Task<IActionResult> Rating(string entityType, string entityId)
{
return Task.FromResult((IActionResult) ViewComponent(typeof(RatingViewComponent), new {entityType, entityId}));
}
}
}

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

@ -175,7 +175,7 @@
</div>
</div>
}
@if (comment.Replies.Count >= 5)
@if (comment.Replies.Count >= 3)
{
<div class="text-right mt-4">
@if (CurrentUser.IsAuthenticated)

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

@ -0,0 +1,46 @@
@using Microsoft.AspNetCore.Mvc.Localization
@using Volo.Abp.Users
@using Volo.CmsKit.Localization
@inject ICurrentUser CurrentUser
@model Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Rating.RatingViewModel
@inject IHtmlLocalizer<CmsKitResource> L
<div class="cms-rating-area container" data-entity-type="@Model.EntityType" data-entity-id="@Model.EntityId" id="cms-rating_{@Model.EntityType}_{@Model.EntityId}">
@if (CurrentUser.IsAuthenticated)
{
<div class="m-auto">
<span class="my-rating-5" data-rating="@(Model.CurrentRating ?? 0)" data-authenticated="@(Model.CurrentRating != null)"></span>
<span class="live-rating">@(Model.CurrentRating != null ? Model.CurrentRating + " | " : 0 + "")</span>
@if (Model.CurrentRating != null)
{
<a href="#" class="rating-undo-link m-auto">
<i class="fa fa-undo"></i> @L["Undo"]
</a>
}
</div>
if (Model.Ratings != null)
{
<div class="row text-center col-6 mt-3">
@foreach (var rating in Model.Ratings)
{
<div class="side col-2">
<div>@rating.StarCount @L["Star"]</div>
</div>
<div class="middle col-8">
<div class="bar-container">
<div class="bar bar-@rating.StarCount" style="width: @(rating.Count * 100 / Model.TotalRating)%"></div>
</div>
</div>
<div class="side col-2">
<div>@rating.Count</div>
</div>
}
</div>
}
}
else
{
<span class="my-rating-5" data-authenticated="True" data-toggle="popover" data-placement="right" data-html="true" data-content="<div class='text-center'><a href='@Model.LoginUrl' class='btn btn-primary btn-block'>@L["LoginToRate"]</a></div>"></span>
<span class="rating-login"></span>
}
</div>

16
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/RatingScriptBundleContributor.cs

@ -0,0 +1,16 @@
using System.Collections.Generic;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Packages.StarRatingSvg;
using Volo.Abp.Modularity;
namespace Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Rating
{
[DependsOn(typeof(StarRatingSvgScriptContributor))]
public class RatingScriptBundleContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("/Pages/CmsKit/Shared/Components/Rating/default.js");
}
}
}

16
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/RatingStyleBundleContributor.cs

@ -0,0 +1,16 @@
using System.Collections.Generic;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Packages.StarRatingSvg;
using Volo.Abp.Modularity;
namespace Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Rating
{
[DependsOn(typeof(StarRatingSvgStyleContributor))]
public class RatingStyleBundleContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("/Pages/CmsKit/Shared/Components/Rating/default.css");
}
}
}

75
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/RatingViewComponent.cs

@ -0,0 +1,75 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.UI;
using Volo.Abp.AspNetCore.Mvc.UI.Widgets;
using Volo.Abp.Users;
using Volo.CmsKit.Public.Ratings;
namespace Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Rating
{
[ViewComponent(Name = "CmsRating")]
[Widget(
StyleTypes = new[] {typeof(RatingStyleBundleContributor)},
ScriptTypes = new[] {typeof(RatingScriptBundleContributor)},
RefreshUrl = "/CmsKitPublicWidgets/Rating"
)]
public class RatingViewComponent : AbpViewComponent
{
public IRatingPublicAppService RatingPublicAppService { get; }
public AbpMvcUiOptions AbpMvcUiOptions { get; }
public ICurrentUser CurrentUser { get; }
public RatingViewComponent(IRatingPublicAppService ratingPublicAppService, IOptions<AbpMvcUiOptions> options, ICurrentUser currentUser)
{
RatingPublicAppService = ratingPublicAppService;
AbpMvcUiOptions = options.Value;
CurrentUser = currentUser;
}
public virtual async Task<IViewComponentResult> InvokeAsync(string entityType, string entityId)
{
var ratings = await RatingPublicAppService.GetGroupedStarCountsAsync(entityType, entityId);
var totalRating = ratings.Sum(x => x.Count);
short? currentUserRating = null;
if (CurrentUser.IsAuthenticated)
{
currentUserRating = ratings.Find(x => x.IsSelectedByCurrentUser)?.StarCount;
}
var loginUrl =
$"{AbpMvcUiOptions.LoginUrl}?returnUrl={HttpContext.Request.Path.ToString()}&returnUrlHash=#cms-rating_{entityType}_{entityId}";
var viewModel = new RatingViewModel
{
EntityId = entityId,
EntityType = entityType,
LoginUrl = loginUrl,
Ratings = ratings,
CurrentRating = currentUserRating,
TotalRating = totalRating
};
return View("~/Pages/CmsKit/Shared/Components/Rating/Default.cshtml", viewModel);
}
}
public class RatingViewModel
{
public string EntityType { get; set; }
public string EntityId { get; set; }
public string LoginUrl { get; set; }
public List<RatingWithStarCountDto> Ratings { get; set; }
public short? CurrentRating { get; set; }
public int TotalRating { get; set; }
}
}

44
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/default.css

@ -0,0 +1,44 @@
.side {
width: 15%;
margin-top:10px;
}
.middle {
float: left;
width: 70%;
margin-top:10px;
padding-top: 4px;
}
.row:after {
content: "";
display: table;
clear: both;
}
.bar-container {
width: 100%;
background-color: #f1f1f1;
text-align: center;
color: white;
}
.bar {
height: 18px;
}
/* Individual bars */
.bar-5 { background-color: #173F5F; }
.bar-4 { background-color: #20639B; }
.bar-3 { background-color: #3CAEA3; }
.bar-2 { background-color: #F6D55C; }
.bar-1 { background-color: #ED553B; }
@media (max-width: 400px) {
.side, .middle {
width: 100%;
}
.right {
display: none;
}
}

87
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Rating/default.js

@ -0,0 +1,87 @@
(function () {
var l = abp.localization.getResource("CmsKit");
$(document).ready(function () {
abp.widgets.CmsRating = function ($widget) {
var widgetManager = $widget.data("abp-widget-manager");
var $ratingArea = $widget.find(".cms-rating-area");
function getFilters() {
return {
entityType: $ratingArea.attr("data-entity-type"),
entityId: $ratingArea.attr("data-entity-id")
};
}
function registerCreateOfNewRating() {
$widget.find(".my-rating-5").each(function () {
var authenticated = $(this).attr("data-authenticated");
$(this).starRating({
initialRating: 0,
disableAfterRate: true,
useFullStars: true,
readOnly: authenticated === "True",
onHover: function (currentIndex, currentRating, $el) {
$widget.find(".live-rating").text(currentIndex);
},
onLeave: function (currentIndex, currentRating, $el) {
$widget.find(".live-rating").text(currentRating);
},
callback: function (currentRating, $el) {
volo.cmsKit.public.ratings.ratingPublic.create(
$ratingArea.attr("data-entity-type"),
$ratingArea.attr("data-entity-id"),
{
starCount: parseInt(currentRating)
}
).then(function () {
widgetManager.refresh($widget);
})
}
});
}
);
}
function registerUndoLink() {
$widget.find(".rating-undo-link").each(function () {
$(this).on('click', '', function (e) {
e.preventDefault();
abp.message.confirm(l("RatingUndoMessage"), function (ok) {
if (ok) {
volo.cmsKit.public.ratings.ratingPublic.delete(
$ratingArea.attr("data-entity-type"),
$ratingArea.attr("data-entity-id")
).then(function () {
widgetManager.refresh($widget);
});
}
})
});
});
}
function init() {
registerCreateOfNewRating();
registerUndoLink();
}
return {
init: init,
getFilters: getFilters
}
};
$('.abp-widget-wrapper[data-widget-name="CmsRating"]')
.each(function () {
var widgetManager = new abp.WidgetManager({
wrapper: $(this),
});
widgetManager.init($(this));
});
});
})
(jQuery);

137
modules/cms-kit/test/Volo.CmsKit.Application.Tests/Ratings/RatingPublicAppService_Tests.cs

@ -0,0 +1,137 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Shouldly;
using Volo.Abp.Users;
using Volo.CmsKit.Public.Ratings;
using Xunit;
namespace Volo.CmsKit.Ratings
{
public class RatingPublicAppService_Tests : CmsKitApplicationTestBase
{
private readonly IRatingPublicAppService _ratingAppService;
private ICurrentUser _currentUser;
private readonly CmsKitTestData _cmsKitTestData;
public RatingPublicAppService_Tests()
{
_ratingAppService = GetRequiredService<IRatingPublicAppService>();
_cmsKitTestData = GetRequiredService<CmsKitTestData>();
}
protected override void AfterAddApplication(IServiceCollection services)
{
_currentUser = Substitute.For<ICurrentUser>();
services.AddSingleton(_currentUser);
}
[Fact]
public async Task CreateAsync()
{
_currentUser.Id.Returns(_cmsKitTestData.User1Id);
var newRating = await _ratingAppService.CreateAsync(
_cmsKitTestData.EntityType1,
_cmsKitTestData.EntityId2,
new CreateUpdateRatingInput
{
StarCount = 4
});
UsingDbContext(context =>
{
var ratings = context.Set<Rating>().Where(x =>
x.EntityId == _cmsKitTestData.EntityId1 && x.EntityType == _cmsKitTestData.EntityType1).ToList();
ratings
.Any(c => c.Id == newRating.Id && c.CreatorId == newRating.CreatorId &&
c.StarCount == newRating.StarCount)
.ShouldBeTrue();
});
}
[Fact]
public async Task CreateAsync_Should_Update_If_Rating_Is_Exist()
{
_currentUser.Id.Returns(_cmsKitTestData.User1Id);
var entity =
await _ratingAppService.GetCurrentUserRatingAsync(_cmsKitTestData.EntityType1,
_cmsKitTestData.EntityId1);
var updatedEntity = await _ratingAppService.CreateAsync(
_cmsKitTestData.EntityType1,
_cmsKitTestData.EntityId1,
new CreateUpdateRatingInput
{
StarCount = 5
});
entity.Id.ShouldBe(updatedEntity.Id);
entity.EntityId.ShouldBe(updatedEntity.EntityId);
entity.EntityType.ShouldBe(updatedEntity.EntityType);
entity.StarCount.ShouldBe(updatedEntity.StarCount);
}
[Fact]
public async Task GetCurrentUserRatingAsync()
{
_currentUser.Id.Returns(_cmsKitTestData.User1Id);
var rating = await _ratingAppService.GetCurrentUserRatingAsync(
_cmsKitTestData.EntityType1,
_cmsKitTestData.EntityId1
);
UsingDbContext(context =>
{
var ratings = context.Set<Rating>().Where(x =>
x.EntityId == _cmsKitTestData.EntityId1 && x.EntityType == _cmsKitTestData.EntityType1).ToList();
ratings
.Any(c => c.Id == rating.Id && c.EntityId == rating.EntityId && c.EntityType == rating.EntityType)
.ShouldBeTrue();
});
}
[Fact]
public async Task GetGroupedStarCountsAsync()
{
_currentUser.Id.Returns(_cmsKitTestData.User1Id);
var ratings = await _ratingAppService.GetGroupedStarCountsAsync(
_cmsKitTestData.EntityType1,
_cmsKitTestData.EntityId1
);
ratings.ShouldNotBeNull();
ratings.Count.ShouldBeGreaterThan(0);
}
[Fact]
public async Task DeleteAsync()
{
_currentUser.Id.Returns(_cmsKitTestData.User1Id);
var rating = await _ratingAppService.CreateAsync(
_cmsKitTestData.EntityType1,
_cmsKitTestData.EntityId1,
new CreateUpdateRatingInput
{
StarCount = 4
});
await _ratingAppService.DeleteAsync(_cmsKitTestData.EntityType1, _cmsKitTestData.EntityId1);
UsingDbContext(context =>
{
var deletedComment = context.Set<Rating>().FirstOrDefault(x => x.Id == rating.Id);
deletedComment.ShouldBeNull();
});
}
}
}

9
modules/cms-kit/test/Volo.CmsKit.EntityFrameworkCore.Tests/EntityFrameworkCore/Ratings/RatingRepository_Tests.cs

@ -0,0 +1,9 @@
using Volo.CmsKit.Ratings;
namespace Volo.CmsKit.EntityFrameworkCore.Ratings
{
public class RatingRepository_Tests : RatingRepository_Tests<CmsKitEntityFrameworkCoreTestModule>
{
}
}

11
modules/cms-kit/test/Volo.CmsKit.MongoDB.Tests/MongoDB/Ratings/RatingRepository_Tests.cs

@ -0,0 +1,11 @@
using Volo.CmsKit.Ratings;
using Xunit;
namespace Volo.CmsKit.MongoDB.Ratings
{
[Collection(MongoTestCollection.Name)]
public class RatingRepository_Tests : RatingRepository_Tests<CmsKitMongoDbTestModule>
{
}
}

47
modules/cms-kit/test/Volo.CmsKit.TestBase/CmsKitDataSeedContributor.cs

@ -4,6 +4,7 @@ using Volo.Abp.DependencyInjection;
using Volo.Abp.Guids;
using Volo.Abp.Users;
using Volo.CmsKit.Comments;
using Volo.CmsKit.Ratings;
using Volo.CmsKit.Reactions;
using Volo.CmsKit.Users;
@ -16,19 +17,22 @@ namespace Volo.CmsKit
private readonly CmsKitTestData _cmsKitTestData;
private readonly ICommentRepository _commentRepository;
private readonly ReactionManager _reactionManager;
private readonly IRatingRepository _ratingRepository;
public CmsKitDataSeedContributor(
IGuidGenerator guidGenerator,
ICmsUserRepository cmsUserRepository,
CmsKitTestData cmsKitTestData,
ICommentRepository commentRepository,
ReactionManager reactionManager)
ReactionManager reactionManager,
IRatingRepository ratingRepository)
{
_guidGenerator = guidGenerator;
_cmsUserRepository = cmsUserRepository;
_cmsKitTestData = cmsKitTestData;
_commentRepository = commentRepository;
_reactionManager = reactionManager;
_ratingRepository = ratingRepository;
}
public async Task SeedAsync(DataSeedContext context)
@ -38,13 +42,17 @@ namespace Volo.CmsKit
await SeedCommentsAsync();
await SeedReactionsAsync();
await SeedRatingsAsync();
}
private async Task SeedUsersAsync()
{
await _cmsUserRepository.InsertAsync(new CmsUser(new UserData(_cmsKitTestData.User1Id, "user1", "user1@volo.com",
await _cmsUserRepository.InsertAsync(new CmsUser(new UserData(_cmsKitTestData.User1Id, "user1",
"user1@volo.com",
"user", "1")));
await _cmsUserRepository.InsertAsync(new CmsUser(new UserData(_cmsKitTestData.User2Id, "user2", "user2@volo.com",
await _cmsUserRepository.InsertAsync(new CmsUser(new UserData(_cmsKitTestData.User2Id, "user2",
"user2@volo.com",
"user", "2")));
}
@ -131,5 +139,36 @@ namespace Volo.CmsKit
_cmsKitTestData.EntityId1,
StandardReactions.ThumbsUp);
}
private async Task SeedRatingsAsync()
{
await _ratingRepository.InsertAsync(new Rating(_guidGenerator.Create(),
_cmsKitTestData.EntityType1,
_cmsKitTestData.EntityId1,
4,
_cmsKitTestData.User1Id
));
await _ratingRepository.InsertAsync(new Rating(_guidGenerator.Create(),
_cmsKitTestData.EntityType1,
_cmsKitTestData.EntityId1,
5,
_cmsKitTestData.User1Id
));
await _ratingRepository.InsertAsync(new Rating(_guidGenerator.Create(),
_cmsKitTestData.EntityType2,
_cmsKitTestData.EntityId2,
5,
_cmsKitTestData.User2Id
));
await _ratingRepository.InsertAsync(new Rating(_guidGenerator.Create(),
_cmsKitTestData.EntityType2,
_cmsKitTestData.EntityId2,
1,
_cmsKitTestData.User2Id
));
}
}
}
}

41
modules/cms-kit/test/Volo.CmsKit.TestBase/Ratings/RatingRepository_Tests.cs

@ -0,0 +1,41 @@
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Modularity;
using Xunit;
namespace Volo.CmsKit.Ratings
{
public abstract class RatingRepository_Tests<TStartupModule> : CmsKitTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
private readonly CmsKitTestData _cmsKitTestData;
private readonly IRatingRepository _ratingRepository;
public RatingRepository_Tests()
{
_cmsKitTestData = GetRequiredService<CmsKitTestData>();
_ratingRepository = GetRequiredService<IRatingRepository>();
}
[Fact]
public async Task GetCurrentUserRatingAsync()
{
var userRating = await _ratingRepository.GetCurrentUserRatingAsync(_cmsKitTestData.EntityType1,
_cmsKitTestData.EntityId1, _cmsKitTestData.User1Id);
userRating.ShouldNotBeNull();
userRating.EntityId.ShouldBe(_cmsKitTestData.EntityId1);
userRating.EntityType.ShouldBe(_cmsKitTestData.EntityType1);
userRating.CreatorId.ShouldBe(_cmsKitTestData.User1Id);
}
[Fact]
public async Task GetGroupedStarCountsAsync()
{
var list = await _ratingRepository.GetGroupedStarCountsAsync(_cmsKitTestData.EntityType1,
_cmsKitTestData.EntityId1);
list.ShouldNotBeNull();
}
}
}

18
modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeatureGroupDto.cs

@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace Volo.Abp.FeatureManagement
{
public class FeatureGroupDto
{
public string Name { get; set; }
public string DisplayName { get; set; }
public List<FeatureDto> Features { get; set; }
public string GetNormalizedGroupName()
{
return Name.Replace(".", "_");
}
}
}

9
modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/FeatureListDto.cs

@ -1,9 +0,0 @@
using System.Collections.Generic;
namespace Volo.Abp.FeatureManagement
{
public class FeatureListDto
{
public List<FeatureDto> Features { get; set; }
}
}

9
modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/GetFeatureListResultDto.cs

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace Volo.Abp.FeatureManagement
{
public class GetFeatureListResultDto
{
public List<FeatureGroupDto> Groups { get; set; }
}
}

2
modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/IFeatureAppService.cs

@ -6,7 +6,7 @@ namespace Volo.Abp.FeatureManagement
{
public interface IFeatureAppService : IApplicationService
{
Task<FeatureListDto> GetAsync([NotNull] string providerName, [NotNull] string providerKey);
Task<GetFeatureListResultDto> GetAsync([NotNull] string providerName, [NotNull] string providerKey);
Task UpdateAsync([NotNull] string providerName, [NotNull] string providerKey, UpdateFeaturesDto input);
}

55
modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Features;
namespace Volo.Abp.FeatureManagement
@ -24,35 +25,49 @@ namespace Volo.Abp.FeatureManagement
Options = options.Value;
}
public virtual async Task<FeatureListDto> GetAsync([NotNull] string providerName, [NotNull] string providerKey)
public virtual async Task<GetFeatureListResultDto> GetAsync([NotNull] string providerName, [NotNull] string providerKey)
{
await CheckProviderPolicy(providerName);
var featureDefinitions = FeatureDefinitionManager.GetAll();
var features = new List<FeatureDto>();
var result = new GetFeatureListResultDto
{
Groups = new List<FeatureGroupDto>()
};
foreach (var featureDefinition in featureDefinitions)
foreach (var group in FeatureDefinitionManager.GetGroups())
{
var feature = await FeatureManager.GetOrNullWithProviderAsync(featureDefinition.Name, providerName, providerKey);
features.Add(new FeatureDto
var groupDto = new FeatureGroupDto
{
Name = group.Name,
DisplayName = group.DisplayName.Localize(StringLocalizerFactory),
Features = new List<FeatureDto>()
};
foreach (var featureDefinition in group.GetFeaturesWithChildren())
{
Name = featureDefinition.Name,
DisplayName = featureDefinition.DisplayName?.Localize(StringLocalizerFactory),
ValueType = featureDefinition.ValueType,
Description = featureDefinition.Description?.Localize(StringLocalizerFactory),
ParentName = featureDefinition.Parent?.Name,
Value = feature.Value,
Provider = new FeatureProviderDto
var feature = await FeatureManager.GetOrNullWithProviderAsync(featureDefinition.Name, providerName, providerKey);
groupDto.Features.Add(new FeatureDto
{
Name = feature.Provider?.Name,
Key = feature.Provider?.Key
}
});
}
Name = featureDefinition.Name,
DisplayName = featureDefinition.DisplayName?.Localize(StringLocalizerFactory),
ValueType = featureDefinition.ValueType,
Description = featureDefinition.Description?.Localize(StringLocalizerFactory),
ParentName = featureDefinition.Parent?.Name,
Value = feature.Value,
Provider = new FeatureProviderDto
{
Name = feature.Provider?.Name,
Key = feature.Provider?.Key
}
});
}
SetFeatureDepth(groupDto.Features, providerName, providerKey);
SetFeatureDepth(features, providerName, providerKey);
result.Groups.Add(groupDto);
}
return new FeatureListDto { Features = features };
return result;
}
public virtual async Task UpdateAsync([NotNull] string providerName, [NotNull] string providerKey, UpdateFeaturesDto input)

2
modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/FeaturesController.cs

@ -17,7 +17,7 @@ namespace Volo.Abp.FeatureManagement
}
[HttpGet]
public virtual Task<FeatureListDto> GetAsync(string providerName, string providerKey)
public virtual Task<GetFeatureListResultDto> GetAsync(string providerName, string providerKey)
{
return FeatureAppService.GetAsync(providerName, providerKey);
}

98
modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml

@ -4,7 +4,6 @@
@using Volo.Abp.FeatureManagement.Localization
@using Volo.Abp.Validation.StringValues
@using Volo.Abp.FeatureManagement.Web.Pages.FeatureManagement
@using Volo.Abp.Features
@model FeatureManagementModal
@inject IHtmlLocalizer<AbpFeatureManagementResource> L
@{
@ -13,53 +12,67 @@
<form method="post" asp-page="/FeatureManagement/FeatureManagementModal" data-script-class="abp.modals.FeatureManagement">
<abp-modal size="Large">
<abp-modal-header title="@(L["Features"].Value)"></abp-modal-header>
@if (Model.FeatureListDto?.Features != null && Model.FeatureListDto.Features.Any())
@if (Model.FeatureListResultDto != null && Model.FeatureListResultDto.Groups.Any())
{
<abp-modal-body class="ml-4">
<input asp-for="@Model.ProviderKey" />
<input asp-for="@Model.ProviderName" />
@for (var i = 0; i < Model.FeatureListDto.Features.Count; i++)
{
var feature = Model.FeatureListDto.Features[i];
var disabled = Model.IsDisabled(feature.Provider.Name);
<div class="mt-2" style="padding-left: @(feature.Depth * 20)px">
<input asp-for="@Model.ProviderKey"/>
<input asp-for="@Model.ProviderName"/>
<abp-tabs name="FeaturesTabs" tab-style="PillVertical" vertical-header-size="_4" class="custom-scroll-container">
@for (var i = 0; i < Model.FeatureListResultDto.Groups.Count; i++)
{
<abp-tab title="@Model.FeatureListResultDto.Groups[i].DisplayName" name="v-pills-tab-@Model.FeatureListResultDto.Groups[i].GetNormalizedGroupName()">
<h4>@Model.FeatureListResultDto.Groups[i].DisplayName</h4>
<hr class="mt-2 mb-3"/>
<div class="custom-scroll-content">
<div class="pl-1 pt-1">
@for (var j = 0; j < Model.FeatureListResultDto.Groups[i].Features.Count; j++)
{
var feature = Model.FeatureListResultDto.Groups[i].Features[j];
var disabled = Model.IsDisabled(feature.Provider.Name);
<div class="mt-2" style="padding-left: @(feature.Depth * 20)px">
<spam class="mr-2">@feature.DisplayName @(disabled ? $"({feature.Provider.Name})" : "")</spam>
<spam class="mr-2">@feature.DisplayName @(disabled ? $"({feature.Provider.Name})" : "")</spam>
<input type="text" name="Features[@i].ProviderName" value="@feature.Provider.Name" hidden />
<input type="text" name="Features[@i].Type" value="@feature.ValueType?.Name" hidden />
@if (feature.ValueType is FreeTextStringValueType)
{
<input type="text" name="Features[@i].Name" value="@feature.Name" hidden />
<input disabled="@disabled" type="text" name="Features[@i].Value" value="@feature.Value" />
}
@if (feature.ValueType is SelectionStringValueType)
{
<input type="text" name="Features[@i].Name" value="@feature.Name" hidden />
<select disabled="@disabled" name="Features[@i].Value">
@foreach (var item in (feature.ValueType as SelectionStringValueType).ItemSource.Items)
{
if (item.Value == feature.Value)
{
<option value="@item.Value" selected="selected"> @L.GetString(item.DisplayText.Name) </option>
}
else
{
<option value="@item.Value"> @L.GetString(item.DisplayText.Name) </option>
<input type="text" name="FeatureGroups[@i].Features[@j].ProviderName" value="@feature.Provider.Name" hidden/>
<input type="text" name="FeatureGroups[@i].Features[@j].Type" value="@feature.ValueType?.Name" hidden/>
@if (feature.ValueType is FreeTextStringValueType)
{
<input type="text" name="FeatureGroups[@i].Features[@j].Name" value="@feature.Name" hidden/>
<input disabled="@disabled" type="text" name="FeatureGroups[@i].Features[@j].Value" value="@feature.Value"/>
}
@if (feature.ValueType is SelectionStringValueType)
{
<input type="text" name="FeatureGroups[@i].Features[@j].Name" value="@feature.Name" hidden/>
<select disabled="@disabled" name="FeatureGroups[@i].Features[@j].Value">
@foreach (var item in (feature.ValueType as SelectionStringValueType).ItemSource.Items)
{
if (item.Value == feature.Value)
{
<option value="@item.Value" selected="selected"> @L.GetString(item.DisplayText.Name) </option>
}
else
{
<option value="@item.Value"> @L.GetString(item.DisplayText.Name) </option>
}
}
</select>
}
@if (feature.ValueType is ToggleStringValueType)
{
<input type="text" name="FeatureGroups[@i].Features[@j].Name" value="@feature.Name" hidden/>
<input disabled="@disabled" type="checkbox" class="FeatureValueCheckbox" name="FeatureGroups[@i].Features[@j].BoolValue" value="@feature.Value"
@Html.Raw(feature.Value == "True" ? "checked" : "")/>
}
</div>
}
}
</select>
}
@if (feature.ValueType is ToggleStringValueType)
{
<input type="text" name="Features[@i].Name" value="@feature.Name" hidden />
<input disabled="@disabled" type="checkbox" class="FeatureValueCheckbox" name="Features[@i].BoolValue" value="@feature.Value"
@Html.Raw(feature.Value == "True" ? "checked" : "") />
}
</div>
}
</div>
</div>
</abp-tab>
}
</abp-tabs>
</abp-modal-body>
<abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer>
<abp-modal-footer buttons="@(AbpModalButtons.Cancel | AbpModalButtons.Save)"></abp-modal-footer>
}
else
{
@ -69,4 +82,3 @@
}
</abp-modal>
</form>

19
modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml.cs

@ -22,9 +22,9 @@ namespace Volo.Abp.FeatureManagement.Web.Pages.FeatureManagement
public string ProviderKey { get; set; }
[BindProperty]
public List<FeatureViewModel> Features { get; set; }
public List<FeatureGroupViewModel> FeatureGroups { get; set; }
public FeatureListDto FeatureListDto { get; set; }
public GetFeatureListResultDto FeatureListResultDto { get; set; }
protected IFeatureAppService FeatureAppService { get; }
@ -35,16 +35,20 @@ namespace Volo.Abp.FeatureManagement.Web.Pages.FeatureManagement
FeatureAppService = featureAppService;
}
public virtual async Task OnGetAsync()
public virtual async Task<IActionResult> OnGetAsync()
{
FeatureListDto = await FeatureAppService.GetAsync(ProviderName, ProviderKey);
ValidateModel();
FeatureListResultDto = await FeatureAppService.GetAsync(ProviderName, ProviderKey);
return Page();
}
public virtual async Task<IActionResult> OnPostAsync()
{
var features = new UpdateFeaturesDto
{
Features = Features.Select(f => new UpdateFeatureDto
Features = FeatureGroups.SelectMany(g => g.Features).Select(f => new UpdateFeatureDto
{
Name = f.Name,
Value = f.Type == nameof(ToggleStringValueType) ? f.BoolValue.ToString() : f.Value
@ -68,6 +72,11 @@ namespace Volo.Abp.FeatureManagement.Web.Pages.FeatureManagement
public string ProviderKey { get; set; }
}
public class FeatureGroupViewModel
{
public List<FeatureViewModel> Features { get; set; }
}
public class FeatureViewModel
{
public string Name { get; set; }

4
modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/FeatureAppService_Tests.cs

@ -41,7 +41,7 @@ namespace Volo.Abp.FeatureManagement
TestEditionIds.Regular.ToString());
featureList.ShouldNotBeNull();
featureList.Features.ShouldContain(feature => feature.Name == TestFeatureDefinitionProvider.SocialLogins);
featureList.Groups.SelectMany(g =>g .Features).ShouldContain(feature => feature.Name == TestFeatureDefinitionProvider.SocialLogins);
}
[Fact]
@ -63,7 +63,7 @@ namespace Volo.Abp.FeatureManagement
});
(await _featureAppService.GetAsync(EditionFeatureValueProvider.ProviderName,
TestEditionIds.Regular.ToString())).Features.Any(x =>
TestEditionIds.Regular.ToString())).Groups.SelectMany(g => g.Features).Any(x =>
x.Name == TestFeatureDefinitionProvider.SocialLogins &&
x.Value == false.ToString().ToLowerInvariant())
.ShouldBeTrue();

82
modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/StringValueJsonConverter_Tests.cs

@ -20,38 +20,46 @@ namespace Volo.Abp.FeatureManagement
[Fact]
public void Should_Serialize_And_Deserialize()
{
var featureListDto = new FeatureListDto
var featureListDto = new GetFeatureListResultDto
{
Features = new List<FeatureDto>
Groups = new List<FeatureGroupDto>
{
new FeatureDto
new FeatureGroupDto
{
ValueType = new FreeTextStringValueType
Name = "MyGroup",
DisplayName = "MyGroup",
Features = new List<FeatureDto>
{
Validator = new BooleanValueValidator()
}
},
new FeatureDto
{
ValueType = new SelectionStringValueType
{
ItemSource = new StaticSelectionStringValueItemSource(
new LocalizableSelectionStringValueItem
new FeatureDto
{
ValueType = new FreeTextStringValueType
{
Value = "TestValue",
DisplayText = new LocalizableStringInfo("TestResourceName", "TestName")
}),
Validator = new AlwaysValidValueValidator()
}
},
new FeatureDto
{
ValueType = new ToggleStringValueType
{
Validator = new NumericValueValidator
Validator = new BooleanValueValidator()
}
},
new FeatureDto
{
MaxValue = 1000,
MinValue = 10
ValueType = new SelectionStringValueType
{
ItemSource = new StaticSelectionStringValueItemSource(
new LocalizableSelectionStringValueItem
{
Value = "TestValue",
DisplayText = new LocalizableStringInfo("TestResourceName", "TestName")
}),
Validator = new AlwaysValidValueValidator()
}
},
new FeatureDto
{
ValueType = new ToggleStringValueType
{
Validator = new NumericValueValidator
{
MaxValue = 1000,
MinValue = 10
}
}
}
}
}
@ -59,22 +67,22 @@ namespace Volo.Abp.FeatureManagement
};
var serialized = _jsonSerializer.Serialize(featureListDto, indented: true);
var featureListDto2 = _jsonSerializer.Deserialize<FeatureListDto>(serialized);
var featureListDto2 = _jsonSerializer.Deserialize<GetFeatureListResultDto>(serialized);
featureListDto2.Features[0].ValueType.ShouldBeOfType<FreeTextStringValueType>();
featureListDto2.Features[0].ValueType.Validator.ShouldBeOfType<BooleanValueValidator>();
featureListDto2.Groups[0].Features[0].ValueType.ShouldBeOfType<FreeTextStringValueType>();
featureListDto2.Groups[0].Features[0].ValueType.Validator.ShouldBeOfType<BooleanValueValidator>();
featureListDto2.Features[1].ValueType.ShouldBeOfType<SelectionStringValueType>();
featureListDto2.Features[1].ValueType.Validator.ShouldBeOfType<AlwaysValidValueValidator>();
featureListDto2.Features[1].ValueType.As<SelectionStringValueType>().ItemSource.Items.ShouldBeOfType<LocalizableSelectionStringValueItem[]>();
featureListDto2.Features[1].ValueType.As<SelectionStringValueType>().ItemSource.Items.ShouldContain(x =>
featureListDto2.Groups[0].Features[1].ValueType.ShouldBeOfType<SelectionStringValueType>();
featureListDto2.Groups[0].Features[1].ValueType.Validator.ShouldBeOfType<AlwaysValidValueValidator>();
featureListDto2.Groups[0].Features[1].ValueType.As<SelectionStringValueType>().ItemSource.Items.ShouldBeOfType<LocalizableSelectionStringValueItem[]>();
featureListDto2.Groups[0].Features[1].ValueType.As<SelectionStringValueType>().ItemSource.Items.ShouldContain(x =>
x.Value == "TestValue" && x.DisplayText.ResourceName == "TestResourceName" &&
x.DisplayText.Name == "TestName");
featureListDto2.Features[2].ValueType.ShouldBeOfType<ToggleStringValueType>();
featureListDto2.Features[2].ValueType.Validator.ShouldBeOfType<NumericValueValidator>();
featureListDto2.Features[2].ValueType.Validator.As<NumericValueValidator>().MaxValue.ShouldBe(1000);
featureListDto2.Features[2].ValueType.Validator.As<NumericValueValidator>().MinValue.ShouldBe(10);
featureListDto2.Groups[0].Features[2].ValueType.ShouldBeOfType<ToggleStringValueType>();
featureListDto2.Groups[0].Features[2].ValueType.Validator.ShouldBeOfType<NumericValueValidator>();
featureListDto2.Groups[0].Features[2].ValueType.Validator.As<NumericValueValidator>().MaxValue.ShouldBe(1000);
featureListDto2.Groups[0].Features[2].ValueType.Validator.As<NumericValueValidator>().MinValue.ShouldBe(10);
}
}
}

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

Loading…
Cancel
Save