Browse Source

Merge branch 'dev' into blazor-ui

pull/5399/head
Halil İbrahim Kalkan 6 years ago
parent
commit
342826bd56
  1. 24
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json
  2. 2
      common.props
  3. 23
      docs/en/CLI.md
  4. 31
      docs/en/Community-Articles/2020-08-07-Passwordless-Authentication/POST.md
  5. 81
      docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/POST.md
  6. BIN
      docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/add-simple-property.jpg
  7. BIN
      docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/add-user-navigation.jpg
  8. BIN
      docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/create-appuserdto.jpg
  9. BIN
      docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/create-mapping.jpg
  10. BIN
      docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/create-note-entity.jpg
  11. BIN
      docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/final-page.jpg
  12. BIN
      docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/ui-pick-type-modal.jpg
  13. BIN
      docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/ui-pick-type-modal2.jpg
  14. 167
      docs/en/Distributed-Event-Bus-Kafka-Integration.md
  15. 1
      docs/en/Distributed-Event-Bus.md
  16. 8
      docs/en/Tutorials/Part-10.md
  17. 4
      docs/en/docs-nav.json
  18. 163
      docs/zh-Hans/Distributed-Event-Bus-Kafka-Integration.md
  19. 1
      docs/zh-Hans/Distributed-Event-Bus.md
  20. 4
      docs/zh-Hans/docs-nav.json
  21. 4
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
  22. 137
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateProxyCommand.cs
  23. 144
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ProxyCommandBase.cs
  24. 11
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/RemoveProxyCommand.cs
  25. 87
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SuiteCommand.cs
  26. 3
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingViewComponent.cs
  27. 176
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml
  28. 7
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.js
  29. 49
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs
  30. 8
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/OrganizationUnitManager.cs
  31. 104
      modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs
  32. 13
      npm/ng-packs/packages/core/src/lib/strategies/auth-flow.strategy.ts
  33. 18
      npm/ng-packs/packages/schematics/src/collection.json
  34. 0
      npm/ng-packs/packages/schematics/src/commands/api/files-enum/proxy/__namespace@dir__/__name@kebab__.enum.ts.template
  35. 0
      npm/ng-packs/packages/schematics/src/commands/api/files-model/proxy/__namespace@dir__/models.ts.template
  36. 0
      npm/ng-packs/packages/schematics/src/commands/api/files-service/proxy/__namespace@dir__/__name@kebab__.service.ts.template
  37. 22
      npm/ng-packs/packages/schematics/src/commands/api/index.ts
  38. 16
      npm/ng-packs/packages/schematics/src/commands/api/schema.ts
  39. 60
      npm/ng-packs/packages/schematics/src/commands/proxy-add/index.ts
  40. 12
      npm/ng-packs/packages/schematics/src/commands/proxy-add/schema.json
  41. 45
      npm/ng-packs/packages/schematics/src/commands/proxy-refresh/index.ts
  42. 39
      npm/ng-packs/packages/schematics/src/commands/proxy-refresh/schema.json
  43. 51
      npm/ng-packs/packages/schematics/src/commands/proxy-remove/index.ts
  44. 39
      npm/ng-packs/packages/schematics/src/commands/proxy-remove/schema.json
  45. 42
      npm/ng-packs/packages/schematics/src/commands/proxy/index.ts
  46. 16
      npm/ng-packs/packages/schematics/src/commands/proxy/schema.ts
  47. 1
      npm/ng-packs/packages/schematics/src/constants/index.ts
  48. 18
      npm/ng-packs/packages/schematics/src/constants/proxy.ts
  49. 4
      npm/ng-packs/packages/schematics/src/enums/exception.ts
  50. 16
      npm/ng-packs/packages/schematics/src/models/generate-proxy-schema.ts
  51. 3
      npm/ng-packs/packages/schematics/src/models/index.ts
  52. 5
      npm/ng-packs/packages/schematics/src/models/proxy-config.ts
  53. 1
      npm/ng-packs/packages/schematics/src/models/tree.ts
  54. 6
      npm/ng-packs/packages/schematics/src/utils/api.ts
  55. 48
      npm/ng-packs/packages/schematics/src/utils/barrel.ts
  56. 2
      npm/ng-packs/packages/schematics/src/utils/enum.ts
  57. 8
      npm/ng-packs/packages/schematics/src/utils/file.ts
  58. 3
      npm/ng-packs/packages/schematics/src/utils/index.ts
  59. 32
      npm/ng-packs/packages/schematics/src/utils/path.ts
  60. 9
      npm/ng-packs/packages/schematics/src/utils/rule.ts
  61. 127
      npm/ng-packs/packages/schematics/src/utils/source.ts
  62. 4
      npm/ng-packs/scripts/build-schematics.ts
  63. 4
      npm/ng-packs/tsconfig.base.json
  64. 6
      templates/app/angular/tsconfig.base.json
  65. 4
      templates/module/angular/tsconfig.base.json

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

@ -37,7 +37,7 @@
"CreateArticleTitleInfo": "Title of the article to be shown on the article list.",
"CreateArticleUrlInfo": "Original GitHub/External URL of the article.",
"CreateArticleSummaryInfo": "A short summary of the article to be shown on the article list.",
"CreateArticleCoverInfo": "For creating an effective article, add a cover photo. Only 16:9 aspect ratio pictures will be accepted!",
"CreateArticleCoverInfo": "For creating an effective article, add a cover photo. Upload 16:9 aspect ratio pictures for the best view.",
"ThisExtensionIsNotAllowed": "This extension is not allowed.",
"TheFileIsTooLarge": "The file is too large.",
"GoToTheArticle": "Go to the Article",
@ -61,10 +61,26 @@
"Oops": "Oops!",
"CreateArticleSuccessMessage": "The Article has been successfully submitted. It will be published after a review from the site admin.",
"ChooseCoverImage": "Choose a cover image...",
"PictureUploadedIsNotInExpectedAspectRatio": "The picture you uploaded is not in 16:9 aspect ratio!",
"HeightAndWidthMustNotExceed": "Height and Width must not exceed 1920*1080.",
"CoverImage": "Cover Image",
"ShareYourExperiencesWithTheABPFramework": "Share your experiences with the ABP Framework!",
"Optional": "Optional"
"Optional": "Optional",
"UpdateUserWebSiteInfo": "Example: https://johndoe.com",
"UpdateUserTwitterInfo": "Example: johndoe",
"UpdateUserGithubInfo": "Example: johndoe",
"UpdateUserLinkedinInfo": "Example: https://www.linkedin.com/...",
"UpdateUserCompanyInfo": "Example: Volosoft",
"UpdateUserJobTitleInfo": "Example: Software Developer",
"UserName": "UserName",
"Company": "Company",
"PersonalWebsite": "Personal Website",
"RegistrationDate": "Registration Date",
"Social": "Social",
"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"
}
}

2
common.props

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

23
docs/en/CLI.md

@ -32,6 +32,7 @@ Here, the list of all available commands before explaining their details:
* **`add-package`**: Adds an ABP package to a project.
* **`add-module`**: Adds a [multi-package application module](https://docs.abp.io/en/abp/latest/Modules/Index) to a solution.
* **`generate-proxy`**: Generates client side proxies to use HTTP API endpoints.
* **`remove-proxy`**: Removes previously generated client side proxies.
* **`switch-to-preview`**: Switches to the latest preview version of the ABP Framework.
* **`switch-to-nightly`**: Switches to the latest [nightly builds](Nightly-Builds.md) of the ABP related packages on a solution.
* **`switch-to-stable`**: Switches to the latest stable versions of the ABP related packages on a solution.
@ -179,7 +180,7 @@ abp add-module Volo.Blogging
### generate-proxy
Generates Angular service proxies for your HTTP APIs to make easy to consume your services from the client side. Before running `generate-proxy` command, your host must be up and running.
Generates Angular service proxies for your HTTP APIs to make easy to consume your services from the client side. Your host (server) application must be up and running before running this command.
Usage:
@ -196,6 +197,26 @@ abp generate-proxy
> See the [Angular Service Proxies document](UI/Angular/Service-Proxies.md) for more.
### remove-proxy
Removes previously generated proxy code from the Angular application. Your host (server) application must be up and running before running this command.
This can be especially useful when you generate proxies for multiple modules before and need to remove one of them later.
Usage:
````bash
abp remove-proxy
````
#### Options
* `--module` or `-m`: Specifies the name of the backend module you wish to remove proxies for. Default value: `app`.
* `--source` or `-s`: Specifies the Angular project name to resolve the root namespace & API definition URL from. Default value: `defaultProject`.
* `--target` or `-t`: Specifies the Angular project name to place generated code in. Default value: `defaultProject`.
* `--prompt` or `-p`: Asks the options from the command line prompt (for the unspecified options).
> See the [Angular Service Proxies document](UI/Angular/Service-Proxies.md) for more.
### switch-to-preview

31
docs/en/Community-Articles/2020-08-07-Passwordless-Authentication/POST.md

@ -2,15 +2,17 @@
## Introduction
To allow a user login with a magic URL, you need to implement a custom token provider. In this tutorial, we will show you how to add a custom token provider to authenticate a user with a link, instead of entering a password.
In this tutorial, we will show you how to add a custom token provider to authenticate a user with a link, instead of entering the password.
This can be useful especially if you want to make someone login to the application with your user, without sharing your secret password. The generated link will be for a single use.
### Source Code
The completed sample is available on [the GitHub repository](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication).
The completed sample is available on [GitHub repository](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication).
## Creating the Solution
Before starting to the development, create a new solution named `PasswordlessAuthentication` and run it by following the [getting started tutorial](https://docs.abp.io/en/abp/latest/Getting-Started?UI=MVC&DB=EF&Tiered=No).
Before starting the development, create a new solution named `PasswordlessAuthentication` and run it by following the [getting started tutorial](https://docs.abp.io/en/abp/latest/Getting-Started?UI=MVC&DB=EF&Tiered=No).
## Step-1
@ -155,36 +157,33 @@ Open your **Index.cshtml** and set the content as below. We added a form that po
```html
@page
@inject IHtmlLocalizer<PasswordlessAuthenticationResource> L
@using Microsoft.AspNetCore.Mvc.Localization
@using PasswordlessAuthentication.Localization
@using PasswordlessAuthentication.Web.Menus
@using MyBookStore.Web.Menus
@using Volo.Abp.AspNetCore.Mvc.UI.Layout
@model PasswordlessAuthentication.Web.Pages.IndexModel
@model MyBookStore.Web.Pages.IndexModel
@using Microsoft.AspNetCore.Mvc.Localization
@using MyBookStore.Localization
@inject IHtmlLocalizer<MyBookStoreResource> L
@{
ViewBag.PageTitle = "Home";
}
@inject IPageLayout PageLayout;
@inject IPageLayout PageLayout
@{
PageLayout.Content.Title = L["Home"].Value;
PageLayout.Content.BreadCrumb.Add(L["Menu:Home"].Value);
PageLayout.Content.MenuItemName = PasswordlessAuthenticationMenus.Home;
PageLayout.Content.MenuItemName = MyBookStoreMenus.Home;
}
<abp-card>
<abp-card-body>
<form asp-page-handler="GeneratePasswordlessToken" method="post">
<abp-button button-type="Dark" type="submit">Generate passwordless token link</abp-button>
@if (Model.PasswordlessLoginUrl != null)
{
<abp-card class="mt-3 p-3">
[@Model.PasswordlessLoginUrl](/en/commercial/latest/how-to/@Model.PasswordlessLoginUrl)
<a href="@Model.PasswordlessLoginUrl">@Model.PasswordlessLoginUrl</a>
</abp-card>
}
</form>
</abp-card-body>
</abp-card>
@ -273,4 +272,4 @@ That's all! We created a passwordless login with 7 steps.
## Source Code
The completed sample is available on [the GitHub repository](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication).
The completed sample is available on [GitHub repository](https://github.com/abpframework/abp-samples/tree/master/PasswordlessAuthentication).

81
docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/POST.md

@ -0,0 +1,81 @@
# ABP Suite: How to Add the User Entity as a Navigation Property of Another Entity
## Introduction
[ABP Suite](https://commercial.abp.io/tools/suite), a part of the [ABP Commercial](https://commercial.abp.io/), is a productivity tool developed by the team behind the ABP Framework. The main functionality of the ABP Suite is to generate code for you.
In this post, I'll show you how to add the user entity as a navigation property in your new entity, by the help of the ABP Suite.
> In the sample project MVC UI is used, but the same steps are applicable to the Angular UI as well.
## Code Generation
### Create a New Entity
Open the ABP Suite ([see how](https://docs.abp.io/en/commercial/latest/abp-suite/index)). Create a new entity called `Note`, as an example entity.
![create-note-entity](create-note-entity.jpg)
Then add a string property called `Title`, as an example property.
![add-simple-property](add-simple-property.jpg)
### Create AppUserDto
ABP Suite needs a DTO for the target entity (user, in this case) in order to define a navigation property.
To do this, create a new folder called "Users" in `*.Application.Contracts` then add a new class called `AppUserDto` inherited from `IdentityUserDto`.
![create-appuserdto](create-appuserdto.jpg)
We should define the [object mapping](https://docs.abp.io/en/abp/latest/Object-To-Object-Mapping) to be able to convert the `AppUser` objects to `AppUserDto` objects. To do this, open `YourProjectApplicationAutoMapperProfile.cs` and add the below line:
```csharp
CreateMap<AppUser, AppUserDto>().Ignore(x => x.ExtraProperties);
```
![create-mapping](create-mapping.jpg)
> Creating such a DTO class may not be needed for another entity than the `AppUser`, since it will probably be already available, especially if you had created the other entity using the ABP Suite.
### Define the Navigation Property
Get back to ABP Suite, open the **Navigation Properties** tab of the ABP Suite, click the **Add Navigation Property** button. Browse `AppUser.cs` in `*.Domain\Users` folder. Then choose the `Name` item as display property. Browse `AppUserDto.cs` in `*.Contracts\Users` folder. Choose `Users` from Collection Names dropdown.
![add-user-navigation](add-user-navigation.jpg)
### Generate the Code!
That's it! Click **Save and generate** button to create your page. You'll see the following page if everything goes well.
![final-page](final-page.jpg)
This is the new page that has been created by the ABP Suite. It can perform the fundamental CRUD operations. Also, it has the "App user" column that shows the related user name (you can easily change the automatically created "App user" title from the **Entity Name** field of the navigation property creation screen).
**Picking Users from Look Up Table**
We used dropdown element to select a user from the user list. If you have a lot of users, then it's good to pick a user from a look up table. A look up table is a modal window that lets you filter data and pick one. To do this, get back to Suite and click **Edit** button of user navigation which is set as `AppUserId` name. Choose "Modal" from the "UI Pick Type" field. Then click **Save and generate** button to recreate your page.
![ui-pick-type-modal](ui-pick-type-modal.jpg)
After successful code generation, you'll see the the user can be picked from user table.
![ui-pick-type-modal2](ui-pick-type-modal2.jpg)
## About the ABP Commercial RC
This example has been implemented with **ABP Commercial 3.1.0-rc.3**. This is a RC version. If you want to install the CLI and Suite RC version follow the next steps:
1- Uninstall the current version of the CLI and install the specific RC version:
```bash
dotnet tool uninstall --global Volo.Abp.Cli && dotnet tool install --global Volo.Abp.Cli --version 3.1.0-rc.3
```
2- Uninstall the current version of the Suite and install the specific RC version:
```bash
dotnet tool uninstall --global Volo.Abp.Suite && dotnet tool install -g Volo.Abp.Suite --version 3.1.0-rc.3 --add-source https://nuget.abp.io/<YOUR-API-KEY>/v3/index.json
```
Don't forget to replace the `<YOUR-API-KEY>` with your own key!

BIN
docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/add-simple-property.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/add-user-navigation.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

BIN
docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/create-appuserdto.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

BIN
docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/create-mapping.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

BIN
docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/create-note-entity.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

BIN
docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/final-page.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/ui-pick-type-modal.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 KiB

BIN
docs/en/Community-Articles/2020-08-31-Adding-User-Navigation-In-Suite/ui-pick-type-modal2.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

167
docs/en/Distributed-Event-Bus-Kafka-Integration.md

@ -0,0 +1,167 @@
# Distributed Event Bus Kafka Integration
> This document explains **how to configure the [Kafka](https://kafka.apache.org/)** as the distributed event bus provider. See the [distributed event bus document](Distributed-Event-Bus.md) to learn how to use the distributed event bus system
## Installation
Use the ABP CLI to add [Volo.Abp.EventBus.Kafka](https://www.nuget.org/packages/Volo.Abp.EventBus.Kafka) NuGet package to your project:
* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before.
* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.EventBus.Kafka` package.
* Run `abp add-package Volo.Abp.EventBus.Kafka` command.
If you want to do it manually, install the [Volo.Abp.EventBus.Kafka](https://www.nuget.org/packages/Volo.Abp.EventBus.Kafka) NuGet package to your project and add `[DependsOn(typeof(AbpEventBusKafkaModule))]` to the [ABP module](Module-Development-Basics.md) class inside your project.
## Configuration
You can configure using the standard [configuration system](Configuration.md), like using the `appsettings.json` file, or using the [options](Options.md) classes.
### `appsettings.json` file configuration
This is the simplest way to configure the Kafka settings. It is also very strong since you can use any other configuration source (like environment variables) that is [supported by the AspNet Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/).
**Example: The minimal configuration to connect to a local kafka server with default configurations**
````json
{
"Kafka": {
"Connections": {
"Default": {
"BootstrapServers": "localhost:9092"
}
},
"EventBus": {
"GroupId": "MyGroupId",
"TopicName": "MyTopicName"
}
}
}
````
* `MyGroupId` is the name of this application, which is used as the **GroupId** on the Kakfa.
* `MyTopicName` is the **topic name**.
See [the Kafka document](https://docs.confluent.io/current/clients/confluent-kafka-dotnet/api/Confluent.Kafka.html) to understand these options better.
#### Connections
If you need to connect to another server than the localhost, you need to configure the connection properties.
**Example: Specify the host name (as an IP address)**
````json
{
"Kafka": {
"Connections": {
"Default": {
"BootstrapServers": "123.123.123.123:9092"
}
},
"EventBus": {
"GroupId": "MyGroupId",
"TopicName": "MyTopicName"
}
}
}
````
Defining multiple connections is allowed. In this case, you can specify the connection that is used for the event bus.
**Example: Declare two connections and use one of them for the event bus**
````json
{
"Kafka": {
"Connections": {
"Default": {
"BootstrapServers": "123.123.123.123:9092"
},
"SecondConnection": {
"BootstrapServers": "321.321.321.321:9092"
}
},
"EventBus": {
"GroupId": "MyGroupId",
"TopicName": "MyTopicName",
"ConnectionName": "SecondConnection"
}
}
}
````
This allows you to use multiple RabbitMQ server in your application, but select one of them for the event bus.
You can use any of the [ClientConfig](https://docs.confluent.io/current/clients/confluent-kafka-dotnet/api/Confluent.Kafka.ClientConfig.html) properties as the connection properties.
**Example: Specify the socket timeout**
````json
{
"Kafka": {
"Connections": {
"Default": {
"BootstrapServers": "123.123.123.123:9092",
"SocketTimeoutMs": 60000
}
}
}
}
````
### The Options Classes
`AbpRabbitMqOptions` and `AbpRabbitMqEventBusOptions` classes can be used to configure the connection strings and event bus options for the RabbitMQ.
You can configure this options inside the `ConfigureServices` of your [module](Module-Development-Basics.md).
**Example: Configure the connection**
````csharp
Configure<AbpKafkaOptions>(options =>
{
options.Connections.Default.BootstrapServers = "123.123.123.123:9092";
options.Connections.Default.SaslUsername = "user";
options.Connections.Default.SaslPassword = "pwd";
});
````
**Example: Configure the consumer config**
````csharp
Configure<AbpKafkaOptions>(options =>
{
options.ConfigureConsumer = config =>
{
config.GroupId = "MyGroupId";
config.EnableAutoCommit = false;
};
});
````
**Example: Configure the producer config**
````csharp
Configure<AbpKafkaOptions>(options =>
{
options.ConfigureProducer = config =>
{
config.MessageTimeoutMs = 6000;
config.Acks = Acks.All;
};
});
````
**Example: Configure the topic specification**
````csharp
Configure<AbpKafkaOptions>(options =>
{
options.ConfigureTopic = specification =>
{
specification.ReplicationFactor = 3;
specification.NumPartitions = 3;
};
});
````
Using these options classes can be combined with the `appsettings.json` way. Configuring an option property in the code overrides the value in the configuration file.

1
docs/en/Distributed-Event-Bus.md

@ -8,6 +8,7 @@ Distributed event bus system provides an **abstraction** that can be implemented
* `LocalDistributedEventBus` is the default implementation that implements the distributed event bus to work as in-process. Yes! The **default implementation works just like the [local event bus](Local-Event-Bus.md)**, if you don't configure a real distributed provider.
* `RabbitMqDistributedEventBus` implements the distributed event bus with the [RabbitMQ](https://www.rabbitmq.com/). See the [RabbitMQ integration document](Distributed-Event-Bus-RabbitMQ-Integration.md) to learn how to configure it.
* `KafkaDistributedEventBus` implements the distributed event bus with the [RabbitMQ](https://kafka.apache.org/). See the [Kafka integration document](Distributed-Event-Bus-Kafka-Integration.md) to learn how to configure it.
Using a local event bus as default has a few important advantages. The most important one is that: It allows you to write your code compatible to distributed architecture. You can write a monolithic application now that can be split into microservices later. It is a good practice to communicate between bounded contexts (or between application modules) via distributed events instead of local events.

8
docs/en/Tutorials/Part-10.md

@ -376,6 +376,8 @@ namespace Acme.BookStore.Books
public override async Task<BookDto> GetAsync(Guid id)
{
await CheckGetPolicyAsync();
//Prepare a query to join books and authors
var query = from book in Repository
join author in _authorRepository on book.AuthorId equals author.Id
@ -397,6 +399,8 @@ namespace Acme.BookStore.Books
public override async Task<PagedResultDto<BookDto>>
GetListAsync(PagedAndSortedResultRequestDto input)
{
await CheckGetListPolicyAsync();
//Prepare a query to join books and authors
var query = from book in Repository
join author in _authorRepository on book.AuthorId equals author.Id
@ -495,6 +499,8 @@ namespace Acme.BookStore.Books
public override async Task<BookDto> GetAsync(Guid id)
{
await CheckGetPolicyAsync();
var book = await Repository.GetAsync(id);
var bookDto = ObjectMapper.Map<Book, BookDto>(book);
@ -507,6 +513,8 @@ namespace Acme.BookStore.Books
public override async Task<PagedResultDto<BookDto>>
GetListAsync(PagedAndSortedResultRequestDto input)
{
await CheckGetListPolicyAsync();
//Set a default sorting, if not provided
if (input.Sorting.IsNullOrWhiteSpace())
{

4
docs/en/docs-nav.json

@ -198,6 +198,10 @@
{
"text": "RabbitMQ Integration",
"path": "Distributed-Event-Bus-RabbitMQ-Integration.md"
},
{
"text": "Kafka Integration",
"path": "Distributed-Event-Bus-Kafka-Integration.md"
}
]
}

163
docs/zh-Hans/Distributed-Event-Bus-Kafka-Integration.md

@ -0,0 +1,163 @@
# 分布式事件总线Kafka集成
> 本文解释了**如何配置[Kafka](https://kafka.apache.org/)**做为分布式总线提供程序. 参阅[分布式事件总线文档](Distributed-Event-Bus.md)了解如何使用分布式事件总线系统.
## 安装
使用ABP CLI添加[Volo.Abp.EventBus.Kafka[Volo.Abp.EventBus.Kafka](https://www.nuget.org/packages/Volo.Abp.EventBus.Kafka)NuGet包到你的项目:
* 安装[ABP CLI](https://docs.abp.io/en/abp/latest/CLI),如果你还没有安装.
* 在你想要安装 `Volo.Abp.EventBus.Kafka` 包的 `.csproj` 文件目录打开命令行(终端).
* 运行 `abp add-package Volo.Abp.EventBus.Kafka` 命令.
如果你想要手动安装,安装[Volo.Abp.EventBus.Kafka](https://www.nuget.org/packages/Volo.Abp.EventBus.Kafka) NuGet 包到你的项目然后添加 `[DependsOn(typeof(AbpEventBusKafkaModule))]` 到你的项目[模块](Module-Development-Basics.md)类.
## 配置
可以使用配置使用标准的[配置系统](Configuration.md),如 `appsettings.json` 文件,或[选项](Options.md)类.
### `appsettings.json` 文件配置
这是配置Kafka设置最简单的方法. 它也非常强大,因为你可以使用[由AspNet Core支持的](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/)的任何其他配置源(如环境变量).
**示例:最小化配置与默认配置连接到本地的Kafka服务器**
````json
{
"Kafka": {
"EventBus": {
"GroupId": "MyGroupId",
"TopicName": "MyTopicName"
}
}
}
````
* `MyGroupId` 是应用程序的名称,用于Kafka的**GroupId**.
* `MyTopicName` 是**topic名称**.
参阅[Kafka文档](https://docs.confluent.io/current/clients/confluent-kafka-dotnet/api/Confluent.Kafka.html)更好的了解这些选项.
#### 连接
如果需要连接到本地主机以外的另一台服务器,需要配置连接属性.
**示例: 指定主机名 (如IP地址)**
````json
{
"Kafka": {
"Connections": {
"Default": {
"BootstrapServers": "123.123.123.123:9092"
}
},
"EventBus": {
"GroupId": "MyGroupId",
"TopicName": "MyTopicName"
}
}
}
````
允许定义多个连接. 在这种情况下,你可以指定用于事件总线的连接.
**示例: 声明两个连接并将其中一个用于事件总线**
````json
{
"Kafka": {
"Connections": {
"Default": {
"BootstrapServers": "123.123.123.123:9092"
},
"SecondConnection": {
"BootstrapServers": "321.321.321.321:9092"
}
},
"EventBus": {
"GroupId": "MyGroupId",
"TopicName": "MyTopicName",
"ConnectionName": "SecondConnection"
}
}
}
````
这允许你可以在你的应用程序使用多个Kafka服务器,但将其中一个做为事件总线.
你可以使用任何[ClientConfig](https://docs.confluent.io/current/clients/confluent-kafka-dotnet/api/Confluent.Kafka.ClientConfig.html)属性作为连接属性.
**示例: 指定socket超时时间**
````json
{
"Kafka": {
"Connections": {
"Default": {
"BootstrapServers": "123.123.123.123:9092",
"SocketTimeoutMs": 60000
}
}
}
}
````
### 选项类
`AbpKafkaOptions``AbpKafkaEventBusOptions` 类用于配置Kafka的连接字符串和事件总线选项.
你可以在你的[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法配置选项.
**示例: 配置连接**
````csharp
Configure<AbpKafkaOptions>(options =>
{
options.Connections.Default.BootstrapServers = "123.123.123.123:9092";
options.Connections.Default.SaslUsername = "user";
options.Connections.Default.SaslPassword = "pwd";
});
````
**示例: 配置 consumer config**
````csharp
Configure<AbpKafkaOptions>(options =>
{
options.ConfigureConsumer = config =>
{
config.GroupId = "MyGroupId";
config.EnableAutoCommit = false;
};
});
````
**示例: 配置 producer config**
````csharp
Configure<AbpKafkaOptions>(options =>
{
options.ConfigureProducer = config =>
{
config.MessageTimeoutMs = 6000;
config.Acks = Acks.All;
};
});
````
**示例: 配置 topic specification**
````csharp
Configure<AbpKafkaOptions>(options =>
{
options.ConfigureTopic = specification =>
{
specification.ReplicationFactor = 3;
specification.NumPartitions = 3;
};
});
````
使用这些选项类可以与 `appsettings.json` 组合在一起. 在代码中配置选项属性会覆盖配置文件中的值.

1
docs/zh-Hans/Distributed-Event-Bus.md

@ -8,6 +8,7 @@
* `LocalDistributedEventBus` 是默认实现,实现作为进程内工作的分布式事件总线. 是的!如果没有配置真正的分布式提供程序,**默认实现的工作方式与[本地事件总线](Local-Event-Bus.md)一样**.
* `RabbitMqDistributedEventBus` 通过[RabbitMQ](https://www.rabbitmq.com/)实现分布式事件总线. 请参阅[RabbitMQ集成文档](Distributed-Event-Bus-RabbitMQ-Integration.md)了解如何配置它.
* `KafkaDistributedEventBus` 通过[Kafka](https://kafka.apache.org/)实现分布式事件总线. 请参阅[Kafka集成文档](Distributed-Event-Bus-Kafka-Integration.md)了解如何配置它.
使用本地事件总线作为默认具有一些重要的优点. 最重要的是:它允许你编写与分布式体系结构兼容的代码. 您现在可以编写一个整体应用程序,以后可以拆分成微服务. 最好通过分布式事件而不是本地事件在边界上下文之间(或在应用程序模块之间)进行通信.

4
docs/zh-Hans/docs-nav.json

@ -180,6 +180,10 @@
{
"text": "RabbitMQ 集成",
"path": "Distributed-Event-Bus-RabbitMQ-Integration.md"
},
{
"text": "Kafka 集成",
"path": "Distributed-Event-Bus-Kafka-Integration.md"
}
]
}

4
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs

@ -20,6 +20,7 @@ namespace Volo.Abp.Cli
Configure<AbpCliOptions>(options =>
{
//TODO: Define constants like done for GenerateProxyCommand.Name.
options.Commands["help"] = typeof(HelpCommand);
options.Commands["new"] = typeof(NewCommand);
options.Commands["get-source"] = typeof(GetSourceCommand);
@ -28,7 +29,8 @@ namespace Volo.Abp.Cli
options.Commands["add-module"] = typeof(AddModuleCommand);
options.Commands["login"] = typeof(LoginCommand);
options.Commands["logout"] = typeof(LogoutCommand);
options.Commands["generate-proxy"] = typeof(GenerateProxyCommand);
options.Commands[GenerateProxyCommand.Name] = typeof(GenerateProxyCommand);
options.Commands[RemoveProxyCommand.Name] = typeof(RemoveProxyCommand);
options.Commands["suite"] = typeof(SuiteCommand);
options.Commands["switch-to-preview"] = typeof(SwitchToPreviewCommand);
options.Commands["switch-to-stable"] = typeof(SwitchToStableCommand);

137
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateProxyCommand.cs

@ -1,140 +1,11 @@
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Commands
{
public class GenerateProxyCommand : IConsoleCommand, ITransientDependency
public class GenerateProxyCommand : ProxyCommandBase
{
public Task ExecuteAsync(CommandLineArgs commandLineArgs)
{
CheckAngularJsonFile();
CheckNgSchematics();
var prompt = commandLineArgs.Options.ContainsKey("p") || commandLineArgs.Options.ContainsKey("prompt");
var defaultValue = prompt ? null : "__default";
var module = commandLineArgs.Options.GetOrNull(Options.Module.Short, Options.Module.Long) ?? defaultValue;
var source = commandLineArgs.Options.GetOrNull(Options.Source.Short, Options.Source.Long) ?? defaultValue;
var target = commandLineArgs.Options.GetOrNull(Options.Target.Short, Options.Target.Long) ?? defaultValue;
var commandBuilder = new StringBuilder("npx ng g @abp/ng.schematics:proxy");
if (module != null)
{
commandBuilder.Append($" --module {module}");
}
if (source != null)
{
commandBuilder.Append($" --source {source}");
}
if (target != null)
{
commandBuilder.Append($" --target {target}");
}
CmdHelper.RunCmd(commandBuilder.ToString());
return Task.CompletedTask;
}
private void CheckNgSchematics()
{
var packageJsonPath = $"package.json";
if (!File.Exists(packageJsonPath))
{
throw new CliUsageException(
"package.json file not found" +
Environment.NewLine +
GetUsageInfo()
);
}
var schematicsPackageNode =
(string) JObject.Parse(File.ReadAllText(packageJsonPath))["devDependencies"]?["@abp/ng.schematics"];
if (schematicsPackageNode == null)
{
throw new CliUsageException(
"\"@abp/ng.schematics\" NPM package should be installed to the devDependencies before running this command!" +
Environment.NewLine +
GetUsageInfo()
);
}
}
private void CheckAngularJsonFile()
{
var angularPath = $"angular.json";
if (!File.Exists(angularPath))
{
throw new CliUsageException(
"angular.json file not found. You must run this command in the angular folder." +
Environment.NewLine + Environment.NewLine +
GetUsageInfo()
);
}
}
public string GetUsageInfo()
{
var sb = new StringBuilder();
sb.AppendLine("");
sb.AppendLine("Usage:");
sb.AppendLine("");
sb.AppendLine(" abp generate-proxy");
sb.AppendLine("");
sb.AppendLine("Options:");
sb.AppendLine("");
sb.AppendLine("-m|--module <module-name> (default: 'app') The name of the backend module you wish to generate proxies for.");
sb.AppendLine("-s|--source <source-name> (default: 'defaultProject') Angular project name to resolve the root namespace & API definition URL from.");
sb.AppendLine("-t|--target <target-name> (default: 'defaultProject') Angular project name to place generated code in.");
sb.AppendLine("-p|--prompt Asks the options from the command line prompt (for the missing options)");
sb.AppendLine("");
sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI");
return sb.ToString();
}
public string GetShortDescription()
{
return "Generates Angular service proxies and DTOs to consume HTTP APIs.";
}
public static class Options
{
public static class Module
{
public const string Short = "m";
public const string Long = "module";
}
public static class Source
{
public const string Short = "s";
public const string Long = "source";
}
public const string Name = "generate-proxy";
public static class Target
{
public const string Short = "t";
public const string Long = "target";
}
protected override string CommandName => Name;
public static class Prompt
{
public const string Short = "p";
public const string Long = "prompt";
}
}
protected override string SchematicsCommandName => "proxy-add";
}
}

144
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ProxyCommandBase.cs

@ -0,0 +1,144 @@
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Commands
{
public abstract class ProxyCommandBase : IConsoleCommand, ITransientDependency
{
protected abstract string CommandName { get; }
protected abstract string SchematicsCommandName { get; }
public Task ExecuteAsync(CommandLineArgs commandLineArgs)
{
CheckAngularJsonFile();
CheckNgSchematics();
var prompt = commandLineArgs.Options.ContainsKey("p") || commandLineArgs.Options.ContainsKey("prompt");
var defaultValue = prompt ? null : "__default";
var module = commandLineArgs.Options.GetOrNull(Options.Module.Short, Options.Module.Long) ?? defaultValue;
var source = commandLineArgs.Options.GetOrNull(Options.Source.Short, Options.Source.Long) ?? defaultValue;
var target = commandLineArgs.Options.GetOrNull(Options.Target.Short, Options.Target.Long) ?? defaultValue;
var commandBuilder = new StringBuilder("npx ng g @abp/ng.schematics:" + SchematicsCommandName);
if (module != null)
{
commandBuilder.Append($" --module {module}");
}
if (source != null)
{
commandBuilder.Append($" --source {source}");
}
if (target != null)
{
commandBuilder.Append($" --target {target}");
}
CmdHelper.RunCmd(commandBuilder.ToString());
return Task.CompletedTask;
}
private void CheckNgSchematics()
{
var packageJsonPath = $"package.json";
if (!File.Exists(packageJsonPath))
{
throw new CliUsageException(
"package.json file not found" +
Environment.NewLine +
GetUsageInfo()
);
}
var schematicsPackageNode =
(string) JObject.Parse(File.ReadAllText(packageJsonPath))["devDependencies"]?["@abp/ng.schematics"];
if (schematicsPackageNode == null)
{
throw new CliUsageException(
"\"@abp/ng.schematics\" NPM package should be installed to the devDependencies before running this command!" +
Environment.NewLine +
GetUsageInfo()
);
}
}
private void CheckAngularJsonFile()
{
var angularPath = $"angular.json";
if (!File.Exists(angularPath))
{
throw new CliUsageException(
"angular.json file not found. You must run this command in the angular folder." +
Environment.NewLine + Environment.NewLine +
GetUsageInfo()
);
}
}
public string GetUsageInfo()
{
var sb = new StringBuilder();
sb.AppendLine("");
sb.AppendLine("Usage:");
sb.AppendLine("");
sb.AppendLine($" abp {CommandName}");
sb.AppendLine("");
sb.AppendLine("Options:");
sb.AppendLine("");
sb.AppendLine("-m|--module <module-name> (default: 'app') The name of the backend module you wish to generate proxies for.");
sb.AppendLine("-s|--source <source-name> (default: 'defaultProject') Angular project name to resolve the root namespace & API definition URL from.");
sb.AppendLine("-t|--target <target-name> (default: 'defaultProject') Angular project name to place generated code in.");
sb.AppendLine("-p|--prompt Asks the options from the command line prompt (for the missing options)");
sb.AppendLine("");
sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI");
return sb.ToString();
}
public string GetShortDescription()
{
return "Generates Angular service proxies and DTOs to consume HTTP APIs.";
}
public static class Options
{
public static class Module
{
public const string Short = "m";
public const string Long = "module";
}
public static class Source
{
public const string Short = "s";
public const string Long = "source";
}
public static class Target
{
public const string Short = "t";
public const string Long = "target";
}
public static class Prompt
{
public const string Short = "p";
public const string Long = "prompt";
}
}
}
}

11
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/RemoveProxyCommand.cs

@ -0,0 +1,11 @@
namespace Volo.Abp.Cli.Commands
{
public class RemoveProxyCommand : ProxyCommandBase
{
public const string Name = "remove-proxy";
protected override string CommandName => Name;
protected override string SchematicsCommandName => "proxy-remove";
}
}

87
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SuiteCommand.cs

@ -1,10 +1,13 @@
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using NuGet.Versioning;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.Commands.Services;
using Volo.Abp.Cli.NuGet;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
@ -13,12 +16,14 @@ namespace Volo.Abp.Cli.Commands
public class SuiteCommand : IConsoleCommand, ITransientDependency
{
private readonly AbpNuGetIndexUrlService _nuGetIndexUrlService;
private readonly NuGetService _nuGetService;
private const string SuitePackageName = "Volo.Abp.Suite";
public ILogger<SuiteCommand> Logger { get; set; }
public SuiteCommand(AbpNuGetIndexUrlService nuGetIndexUrlService)
public SuiteCommand(AbpNuGetIndexUrlService nuGetIndexUrlService, NuGetService nuGetService)
{
_nuGetIndexUrlService = nuGetIndexUrlService;
_nuGetService = nuGetService;
Logger = NullLogger<SuiteCommand>.Instance;
}
@ -26,21 +31,22 @@ namespace Volo.Abp.Cli.Commands
{
var operationType = NamespaceHelper.NormalizeNamespace(commandLineArgs.Target);
var preview = commandLineArgs.Options.ContainsKey(Options.Preview.Long);
switch (operationType)
{
case "":
case null:
await InstallSuiteIfNotInstalledAsync();
RunSuite();
break;
case "install":
Logger.LogInformation("Installing ABP Suite...");
await InstallSuiteAsync();
await InstallSuiteAsync(preview);
break;
case "update":
Logger.LogInformation("Updating ABP Suite...");
await UpdateSuiteAsync();
await UpdateSuiteAsync(preview);
break;
case "remove":
@ -50,8 +56,34 @@ namespace Volo.Abp.Cli.Commands
}
}
private async Task InstallSuiteAsync()
private async Task InstallSuiteIfNotInstalledAsync()
{
var currentSuiteVersionAsString = GetCurrentSuiteVersion();
if (string.IsNullOrEmpty(currentSuiteVersionAsString))
{
await InstallSuiteAsync();
}
}
private string GetCurrentSuiteVersion()
{
var dotnetToolList = CmdHelper.RunCmdAndGetOutput("dotnet tool list -g");
var suiteLine = dotnetToolList.Split(Environment.NewLine).FirstOrDefault(l => l.ToLower().StartsWith("volo.abp.suite "));
if (string.IsNullOrEmpty(suiteLine))
{
return null;
}
return suiteLine.Split(" ", StringSplitOptions.RemoveEmptyEntries)[1];
}
private async Task InstallSuiteAsync(bool preview = false)
{
Logger.LogInformation("Installing ABP Suite...");
var nugetIndexUrl = await _nuGetIndexUrlService.GetAsync();
if (nugetIndexUrl == null)
@ -61,7 +93,9 @@ namespace Volo.Abp.Cli.Commands
try
{
var result = CmdHelper.RunCmd("dotnet tool install " + SuitePackageName + " --add-source " + nugetIndexUrl + " -g");
var versionOption = await GetVersionOption(preview);
var result = CmdHelper.RunCmd($"dotnet tool install {SuitePackageName} {versionOption} --add-source {nugetIndexUrl} -g");
if (result == 0)
{
@ -86,8 +120,10 @@ namespace Volo.Abp.Cli.Commands
Logger.LogInformation("dotnet tool install -g Volo.Abp.Suite");
}
private async Task UpdateSuiteAsync()
private async Task UpdateSuiteAsync(bool preview = false)
{
Logger.LogInformation("Updating ABP Suite...");
var nugetIndexUrl = await _nuGetIndexUrlService.GetAsync();
if (nugetIndexUrl == null)
@ -97,7 +133,9 @@ namespace Volo.Abp.Cli.Commands
try
{
var result = CmdHelper.RunCmd("dotnet tool update " + SuitePackageName + " --add-source " + nugetIndexUrl + " -g");
var versionOption = await GetVersionOption(preview);
var result = CmdHelper.RunCmd($"dotnet tool update {SuitePackageName} {versionOption} --add-source {nugetIndexUrl} -g");
if (result != 0)
{
@ -111,6 +149,25 @@ namespace Volo.Abp.Cli.Commands
}
}
private async Task<string> GetVersionOption(bool preview)
{
if (preview)
{
var latestVersion = await GetLatestSuiteVersioAsync(true);
if (latestVersion.IsPrerelease)
{
return $"--version {latestVersion.ToString()}";
}
}
return "";
}
private async Task<SemanticVersion> GetLatestSuiteVersioAsync(bool preview)
{
return await _nuGetService.GetLatestVersionOrNullAsync(SuitePackageName, includeReleaseCandidates: preview);
}
private void ShowSuiteManualUpdateCommand()
{
Logger.LogError("You can also run the following command to update ABP Suite.");
@ -160,7 +217,9 @@ namespace Volo.Abp.Cli.Commands
sb.AppendLine("");
sb.AppendLine(" abp suite");
sb.AppendLine(" abp suite install");
sb.AppendLine(" abp suite install --preview");
sb.AppendLine(" abp suite update");
sb.AppendLine(" abp suite update --preview");
sb.AppendLine(" abp suite remove");
sb.AppendLine("");
@ -171,5 +230,13 @@ namespace Volo.Abp.Cli.Commands
{
return "Install, update, remove or start ABP Suite. See https://commercial.abp.io/tools/suite.";
}
public static class Options
{
public static class Preview
{
public const string Long = "preview";
}
}
}
}
}

3
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingViewComponent.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
@ -44,7 +45,7 @@ namespace Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Commenting
EntityId = entityId,
EntityType = entityType,
LoginUrl = loginUrl,
Comments = result.Items
Comments = result.Items.OrderByDescending(i=> i.CreationTime).ToList()
};
return View("~/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml", viewModel);

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

@ -15,54 +15,57 @@
@{
Func<dynamic, IHtmlContent> GetCommentTitle(CmsUserDto author, DateTime creationTime) =>
@<span>
<i class="far fa-comment-alt mr-2"></i>
@((string.IsNullOrWhiteSpace(author.Name)
@<span>
<i class="far fa-comment-alt mr-2"></i>
@((string.IsNullOrWhiteSpace(author.Name)
? author.UserName
: author.Name + " " + author.Surname).Trim())
<small class="text-muted float-right" style="opacity: .5;">@creationTime.ToString()</small>
</span>;
<small class="text-muted float-right" style="opacity: .5;">@creationTime.ToString()</small>
</span>;
}
@{
Func<dynamic, IHtmlContent> GetCommentArea(Guid? repliedCommentId, bool cancelButton = false) =>
@<div class="cms-comment-form-area bg-light card p-4 mx-0 mt-4 @(repliedCommentId.HasValue ? "my-1" : "mb-0")"
data-reply-id="@(repliedCommentId?.ToString() ?? "")"
style="@(string.IsNullOrEmpty(repliedCommentId?.ToString() ?? "") ? "" : "display:none")">
<form class="cms-comment-form">
<input hidden value="@(repliedCommentId?.ToString() ?? "")" name="repliedCommentId" />
<div class="form-row">
<div class="col">
<div class="form-group">
<textarea class="form-control" name="commentText" placeholder="@(string.IsNullOrEmpty(repliedCommentId?.ToString() ?? "") ? L["YourComment"] : L["YourReply"])..." rows="3"></textarea>
@<div class="cms-comment-form-area bg-light card p-4 mx-0 mt-4 @(repliedCommentId.HasValue ? "my-1" : "mb-0")"
data-reply-id="@(repliedCommentId?.ToString() ?? "")"
style="@(string.IsNullOrEmpty(repliedCommentId?.ToString() ?? "") ? "" : "display:none")">
<form class="cms-comment-form">
<input hidden value="@(repliedCommentId?.ToString() ?? "")" name="repliedCommentId"/>
<div class="form-row">
<div class="col">
<div class="form-group">
<textarea class="form-control" name="commentText" placeholder="@(string.IsNullOrEmpty(repliedCommentId?.ToString() ?? "") ? L["YourComment"] : L["YourReply"])..." rows="3"></textarea>
</div>
</div>
</div>
<div class="col-auto">
<div class="text-right">
@if (cancelButton)
{
<abp-button type="button" button-type="Outline_Secondary" class="reply-cancel-button mb-2" size="Block" data-reply-id="@(repliedCommentId?.ToString() ?? "")"><i class="fa fa-times mr-1"></i> @L["Cancel"] </abp-button>
}
<abp-button type="submit" button-type="Primary" size="Block"><i class="fa fa-comment-alt mr-1"></i> @L["Send"]</abp-button>
<div class="col-auto">
<div class="text-right">
@if (cancelButton)
{
<abp-button type="button" button-type="Outline_Secondary" class="reply-cancel-button mb-2" size="Block" data-reply-id="@(repliedCommentId?.ToString() ?? "")">
<i class="fa fa-times mr-1"></i> @L["Cancel"]
</abp-button>
}
<abp-button type="submit" button-type="Primary" size="Block">
<i class="fa fa-comment-alt mr-1"></i> @L["Send"]
</abp-button>
</div>
</div>
</div>
</div>
</form>
</div>;
</form>
</div>;
}
@{
Func<dynamic, IHtmlContent> GetCommentContentArea(Guid id, string text) =>
@<div>
<div class="cms-comment-content-area" data-id="@id.ToString()">
<p class="m-0">
@text
</p>
</div>
</div>;
@<div>
<div class="cms-comment-content-area" data-id="@id.ToString()">
<p class="m-0">
@text
</p>
</div>
</div>;
}
@{
Func<dynamic, IHtmlContent> GetCommentActionArea(Guid id, Guid authorId, bool isReply, string text) =>
@<div>
<div class="text-right">
Func<dynamic, IHtmlContent> GetCommentActionArea(Guid id, Guid authorId, bool isReply) =>
@<div class="text-right">
@if (!isReply)
{
@if (CurrentUser.IsAuthenticated)
@ -78,7 +81,6 @@
}
@if (authorId == CurrentUser.Id)
{
<a href="#" class="comment-delete-link btn btn-sm shadow-sm bg-white" data-id="@id.ToString()">
<i class="fa fa-trash mr-1 text-danger"></i> @L["Delete"]
</a>
@ -88,30 +90,48 @@
<i class="fa fa-pencil mr-1 text-info"></i> @L["Edit"]
</a>
}
</div>
<div class="cms-comment-edit-area" id="@($"cms-comment_{Model.EntityType}_{Model.EntityId}_{id}")" data-id="@id.ToString()" style="display:none">
</div>;
}
@{
Func<dynamic, IHtmlContent> GetEditArea(Guid id, string text) =>
@<div>
<div class="cms-comment-edit-area" id="@($"cms-comment_{Model.EntityType}_{Model.EntityId}_{id}")" data-id="@id.ToString()" style="display:none">
<div class="card bg-light p-4 mx-0 mt-3">
<form class="cms-comment-update-form">
<input hidden value="@id.ToString()" name="id" />
<div class="form-group">
<textarea class="form-control" name="commentText" rows="3">@text</textarea>
</div>
<div class="text-right">
<abp-button type="button" button-type="Secondary" class="comment-edit-cancel-button" data-id="@id.ToString()"> @L["Cancel"] </abp-button>
<abp-button type="submit" button-type="Primary"> @L["Update"] </abp-button>
</div>
</form>
<div class="card bg-light p-4 mx-0 mt-3">
<form class="cms-comment-update-form">
<input hidden value="@id.ToString()" name="id"/>
<div class="form-group">
<textarea class="form-control" name="commentText" rows="3">@text</textarea>
</div>
<div class="text-right">
<abp-button type="button" button-type="Secondary" class="comment-edit-cancel-button" data-id="@id.ToString()"> @L["Cancel"] </abp-button>
<abp-button type="submit" button-type="Primary"> @L["Update"] </abp-button>
</div>
</form>
</div>
</div>
</div>
</div>;
</div>;
}
<div class="cms-comment-area" data-entity-type="@Model.EntityType" data-entity-id="@Model.EntityId">
<div>
<h5 class="mb-0 mt-4">
<i class="fas fa-comment-alt"></i> @L["Comments"]
<i class="fas fa-comment-alt"></i> @L["Comments"]
</h5>
<div>
@if (CurrentUser.IsAuthenticated)
{
<div id="@($"cms-comment_{Model.EntityType}_{Model.EntityId}")">
@GetCommentArea(null).Invoke(null)
</div>
}
else if (!string.IsNullOrWhiteSpace(Model.LoginUrl))
{
<div class="text-right">
<a href="@Model.LoginUrl" class="btn btn-primary"> @L["LoginToAddComment"]</a>
</div>
}
</div>
@foreach (var comment in Model.Comments)
{
<div class="comment">
@ -124,16 +144,11 @@
@if (cmsKitUiOptions.Value.CommentsOptions.IsReactionsEnabled && GlobalFeatureManager.Instance.IsEnabled<ReactionsFeature>())
{
@await Component.InvokeAsync(typeof(ReactionSelectionViewComponent),
new { entityType = "comment", entityId = comment.Id.ToString() })
@await Component.InvokeAsync(typeof(ReactionSelectionViewComponent), new {entityType = "comment", entityId = comment.Id.ToString()})
}
@GetCommentActionArea(comment.Id, comment.Author.Id, false, comment.Text).Invoke(null)
@if (CurrentUser.IsAuthenticated)
{
@GetCommentArea(comment.Id, true).Invoke(null)
}
@GetCommentActionArea(comment.Id, comment.Author.Id, false).Invoke(null)
@GetEditArea(comment.Id, comment.Text).Invoke(null)
@if (comment.Replies.Any())
{
@ -151,31 +166,38 @@
@if (cmsKitUiOptions.Value.CommentsOptions.IsReactionsEnabled && GlobalFeatureManager.Instance.IsEnabled<ReactionsFeature>())
{
@await Component.InvokeAsync(typeof(ReactionSelectionViewComponent),
new { entityType = "comment", entityId = reply.Id.ToString() })
@await Component.InvokeAsync(typeof(ReactionSelectionViewComponent), new {entityType = "comment", entityId = reply.Id.ToString()})
}
@GetCommentActionArea(reply.Id, reply.Author.Id, true, reply.Text).Invoke(null)
@GetCommentActionArea(reply.Id, reply.Author.Id, true).Invoke(null)
@GetEditArea(reply.Id, reply.Text).Invoke(null)
</div>
</div>
</div>
}
@if (comment.Replies.Count >= 5)
{
<div class="text-right mt-4">
@if (CurrentUser.IsAuthenticated)
{
<a href="#" class="comment-reply-link btn btn-sm btn-primary" data-reply-id="@comment.Id.ToString()">
<i class="fa fa-reply mr-1"></i> @L["Reply"]
</a>
}
else
{
<a href="@(Model.LoginUrl + "_" + comment.Id)" class="btn btn-sm btn-primary shadow-sm"> @L["LoginToReply"]</a>
}
</div>
}
}
@if (CurrentUser.IsAuthenticated)
{
@GetCommentArea(comment.Id, true).Invoke(null)
}
</div>
</div>
}
<div>
@if (CurrentUser.IsAuthenticated)
{
<div id="@($"cms-comment_{Model.EntityType}_{Model.EntityId}")">
@GetCommentArea(null).Invoke(null)
</div>
}
else if (!string.IsNullOrWhiteSpace(Model.LoginUrl))
{
<div class="text-right">
<a href="@Model.LoginUrl" class="btn btn-primary"> @L["LoginToAddComment"]</a>
</div>
}
</div>
</div>
</div>

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

@ -58,10 +58,11 @@
var replyCommentId = $link.data('reply-id');
var $relatedCommentArea = $container.find('.cms-comment-form-area[data-reply-id=' + replyCommentId + ']');
var $links = $container.find('.comment-reply-link[data-reply-id=' + replyCommentId + ']');
$relatedCommentArea.show();
$relatedCommentArea.find('textarea').focus();
$link.removeAttr('href');
$links.addClass('disabled');
});
});
$container.find('.reply-cancel-button').each(function () {
@ -72,10 +73,10 @@
var replyCommentId = $button.data('reply-id');
var $relatedCommentArea = $container.find('.cms-comment-form-area[data-reply-id=' + replyCommentId + ']');
var $replyLink = $container.find('.comment-reply-link[data-reply-id=' + replyCommentId + ']');
var $links = $container.find('.comment-reply-link[data-reply-id=' + replyCommentId + ']');
$relatedCommentArea.hide();
$replyLink.attr('href', '#');
$links.removeClass('disabled');
});
});
}

49
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs

@ -43,15 +43,15 @@ namespace Volo.Abp.Identity
IOrganizationUnitRepository organizationUnitRepository,
ISettingProvider settingProvider)
: base(
store,
optionsAccessor,
passwordHasher,
userValidators,
passwordValidators,
keyNormalizer,
errors,
services,
logger)
store,
optionsAccessor,
passwordHasher,
userValidators,
passwordValidators,
keyNormalizer,
errors,
services,
logger)
{
OrganizationUnitRepository = organizationUnitRepository;
SettingProvider = settingProvider;
@ -71,7 +71,8 @@ namespace Volo.Abp.Identity
return user;
}
public virtual async Task<IdentityResult> SetRolesAsync([NotNull] IdentityUser user, [NotNull] IEnumerable<string> roleNames)
public virtual async Task<IdentityResult> SetRolesAsync([NotNull] IdentityUser user,
[NotNull] IEnumerable<string> roleNames)
{
Check.NotNull(user, nameof(user));
Check.NotNull(roleNames, nameof(roleNames));
@ -101,7 +102,8 @@ namespace Volo.Abp.Identity
public virtual async Task<bool> IsInOrganizationUnitAsync(IdentityUser user, OrganizationUnit ou)
{
await UserRepository.EnsureCollectionLoadedAsync(user, u => u.OrganizationUnits, CancellationTokenProvider.Token);
await UserRepository.EnsureCollectionLoadedAsync(user, u => u.OrganizationUnits,
CancellationTokenProvider.Token);
return user.IsInOrganizationUnit(ou.Id);
}
@ -110,12 +112,13 @@ namespace Volo.Abp.Identity
await AddToOrganizationUnitAsync(
await UserRepository.GetAsync(userId, cancellationToken: CancellationToken),
await OrganizationUnitRepository.GetAsync(ouId, cancellationToken: CancellationToken)
);
);
}
public virtual async Task AddToOrganizationUnitAsync(IdentityUser user, OrganizationUnit ou)
{
await UserRepository.EnsureCollectionLoadedAsync(user, u => u.OrganizationUnits, CancellationTokenProvider.Token);
await UserRepository.EnsureCollectionLoadedAsync(user, u => u.OrganizationUnits,
CancellationTokenProvider.Token);
if (user.OrganizationUnits.Any(cou => cou.OrganizationUnitId == ou.Id))
{
@ -125,19 +128,23 @@ namespace Volo.Abp.Identity
await CheckMaxUserOrganizationUnitMembershipCountAsync(user.OrganizationUnits.Count + 1);
user.AddOrganizationUnit(ou.Id);
await UserRepository.UpdateAsync(user, cancellationToken: CancellationToken);
}
public virtual async Task RemoveFromOrganizationUnitAsync(Guid userId, Guid ouId)
{
var user = await UserRepository.GetAsync(userId, cancellationToken: CancellationToken);
user.RemoveOrganizationUnit(ouId);
await UserRepository.UpdateAsync(user, cancellationToken: CancellationToken);
}
public virtual async Task RemoveFromOrganizationUnitAsync(IdentityUser user, OrganizationUnit ou)
{
await UserRepository.EnsureCollectionLoadedAsync(user, u => u.OrganizationUnits, CancellationTokenProvider.Token);
await UserRepository.EnsureCollectionLoadedAsync(user, u => u.OrganizationUnits,
CancellationTokenProvider.Token);
user.RemoveOrganizationUnit(ou.Id);
await UserRepository.UpdateAsync(user, cancellationToken: CancellationToken);
}
public virtual async Task SetOrganizationUnitsAsync(Guid userId, params Guid[] organizationUnitIds)
@ -155,7 +162,8 @@ namespace Volo.Abp.Identity
await CheckMaxUserOrganizationUnitMembershipCountAsync(organizationUnitIds.Length);
await UserRepository.EnsureCollectionLoadedAsync(user, u => u.OrganizationUnits, CancellationTokenProvider.Token);
await UserRepository.EnsureCollectionLoadedAsync(user, u => u.OrganizationUnits,
CancellationTokenProvider.Token);
//Remove from removed OUs
foreach (var ouId in user.OrganizationUnits.Select(uou => uou.OrganizationUnitId).ToArray())
@ -174,11 +182,14 @@ namespace Volo.Abp.Identity
user.AddOrganizationUnit(organizationUnitId);
}
}
await UserRepository.UpdateAsync(user, cancellationToken: CancellationToken);
}
private async Task CheckMaxUserOrganizationUnitMembershipCountAsync(int requestedCount)
{
var maxCount = await SettingProvider.GetAsync<int>(IdentitySettingNames.OrganizationUnit.MaxUserMembershipCount);
var maxCount =
await SettingProvider.GetAsync<int>(IdentitySettingNames.OrganizationUnit.MaxUserMembershipCount);
if (requestedCount > maxCount)
{
throw new BusinessException(IdentityErrorCodes.MaxAllowedOuMembership)
@ -187,9 +198,11 @@ namespace Volo.Abp.Identity
}
[UnitOfWork]
public virtual async Task<List<OrganizationUnit>> GetOrganizationUnitsAsync(IdentityUser user, bool includeDetails = false)
public virtual async Task<List<OrganizationUnit>> GetOrganizationUnitsAsync(IdentityUser user,
bool includeDetails = false)
{
await UserRepository.EnsureCollectionLoadedAsync(user, u => u.OrganizationUnits, CancellationTokenProvider.Token);
await UserRepository.EnsureCollectionLoadedAsync(user, u => u.OrganizationUnits,
CancellationTokenProvider.Token);
return await OrganizationUnitRepository.GetListAsync(
user.OrganizationUnits.Select(t => t.OrganizationUnitId),

8
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/OrganizationUnitManager.cs

@ -54,8 +54,8 @@ namespace Volo.Abp.Identity
return OrganizationUnit.CalculateNextCode(lastChild.Code);
}
var parentCode = parentId != null
? await GetCodeOrDefaultAsync(parentId.Value)
var parentCode = parentId != null
? await GetCodeOrDefaultAsync(parentId.Value)
: null;
return OrganizationUnit.AppendCode(
@ -174,7 +174,7 @@ namespace Volo.Abp.Identity
return Task.FromResult(0);
}
ou.AddRole(role.Id);
return Task.FromResult(0);
return OrganizationUnitRepository.UpdateAsync(ou);
}
public virtual async Task RemoveRoleFromOrganizationUnitAsync(Guid roleId, Guid ouId)
@ -188,7 +188,7 @@ namespace Volo.Abp.Identity
public virtual Task RemoveRoleFromOrganizationUnitAsync(IdentityRole role, OrganizationUnit organizationUnit)
{
organizationUnit.RemoveRole(role.Id);
return Task.FromResult(0);
return OrganizationUnitRepository.UpdateAsync(organizationUnit);
}
}
}

104
modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs

@ -16,22 +16,24 @@ namespace Volo.Abp.Identity
where TStartupModule : IAbpModule
{
private readonly IOrganizationUnitRepository _organizationUnitRepository;
private readonly IIdentityRoleRepository _identityRoleRepository;
private readonly ILookupNormalizer _lookupNormalizer;
private readonly IdentityTestData _testData;
private readonly IGuidGenerator _guidGenerator;
private readonly OrganizationUnitManager _organizationUnitManager;
private readonly IIdentityRoleRepository _identityRoleRepository;
private readonly IdentityUserManager _identityUserManager;
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IIdentityUserRepository _identityUserRepository;
public OrganizationUnitRepository_Tests()
{
_organizationUnitRepository = ServiceProvider.GetRequiredService<IOrganizationUnitRepository>();
_identityRoleRepository = ServiceProvider.GetRequiredService<IIdentityRoleRepository>();
_lookupNormalizer = ServiceProvider.GetRequiredService<ILookupNormalizer>();
_testData = GetRequiredService<IdentityTestData>();
_guidGenerator = GetRequiredService<IGuidGenerator>();
_organizationUnitManager = GetRequiredService<OrganizationUnitManager>();
_identityRoleRepository = GetRequiredService<IIdentityRoleRepository>();
_identityUserManager = GetRequiredService<IdentityUserManager>();
_unitOfWorkManager = GetRequiredService<IUnitOfWorkManager>();
_identityUserRepository = GetRequiredService<IIdentityUserRepository>();
}
@ -45,19 +47,102 @@ namespace Volo.Abp.Identity
[Fact]
public async Task GetAllChildrenWithParentCodeAsync()
{
(await _organizationUnitRepository.GetAllChildrenWithParentCodeAsync(OrganizationUnit.CreateCode(0), _guidGenerator.Create())).ShouldNotBeNull();
(await _organizationUnitRepository.GetAllChildrenWithParentCodeAsync(OrganizationUnit.CreateCode(0),
_guidGenerator.Create())).ShouldNotBeNull();
}
[Fact]
public async Task GetListAsync()
{
var ouIds = (await _organizationUnitRepository.GetListAsync(includeDetails: true))
.Select(ou => ou.Id).Take(2);
.Select(ou => ou.Id).Take(2);
var ous = await _organizationUnitRepository.GetListAsync(ouIds);
ous.Count.ShouldBe(2);
ous.ShouldContain(ou => ou.Id == ouIds.First());
}
[Fact]
public async Task AddMemberToOrganizationUnit()
{
using (var uow = _unitOfWorkManager.Begin())
{
var ou111 = await _organizationUnitRepository.GetAsync(
_lookupNormalizer.NormalizeName("OU111"));
var user = await _identityUserRepository.FindByNormalizedUserNameAsync(
_lookupNormalizer.NormalizeName("david"));
user.ShouldNotBeNull();
user.OrganizationUnits.Count.ShouldBe(1);
await _identityUserManager.AddToOrganizationUnitAsync(user.Id, ou111.Id);
await uow.CompleteAsync();
}
var updatedUser = await _identityUserRepository.FindByNormalizedUserNameAsync(
_lookupNormalizer.NormalizeName("david"));
updatedUser.OrganizationUnits.Count.ShouldBe(2);
}
[Fact]
public async Task AddRoleToOrganizationUnit()
{
using (var uow = _unitOfWorkManager.Begin())
{
var ou111 = await _organizationUnitRepository.GetAsync(
_lookupNormalizer.NormalizeName("OU111"));
ou111.Roles.Count.ShouldBe(2);
var roleSupporter = await _identityRoleRepository.FindByNormalizedNameAsync(
_lookupNormalizer.NormalizeName("supporter"));
await _organizationUnitManager.AddRoleToOrganizationUnitAsync(roleSupporter.Id, ou111.Id);
await uow.CompleteAsync();
}
var ou111Updated = await _organizationUnitRepository.GetAsync(
_lookupNormalizer.NormalizeName("OU111"));
ou111Updated.Roles.Count.ShouldBeGreaterThan(2);
}
[Fact]
public async Task RemoveRoleFromOrganizationUnit()
{
using (var uow = _unitOfWorkManager.Begin())
{
var ou111 = await _organizationUnitRepository.GetAsync(
_lookupNormalizer.NormalizeName("OU111"));
ou111.Roles.ShouldContain(q => q.RoleId == _testData.RoleModeratorId);
await _organizationUnitManager.RemoveRoleFromOrganizationUnitAsync(_testData.RoleModeratorId, ou111.Id);
await uow.CompleteAsync();
}
var ou111Updated = await _organizationUnitRepository.GetAsync(
_lookupNormalizer.NormalizeName("OU111"));
ou111Updated.Roles.ShouldNotContain(q => q.RoleId == _testData.RoleModeratorId);
}
[Fact]
public async Task RemoveOrganizationUnitFromUser()
{
using (var uow = _unitOfWorkManager.Begin())
{
var ou112 = await _organizationUnitRepository.GetAsync(
_lookupNormalizer.NormalizeName("OU112"));
var user = await _identityUserRepository.FindByNormalizedUserNameAsync(
_lookupNormalizer.NormalizeName("david"));
user.OrganizationUnits.Count.ShouldBe(1);
user.OrganizationUnits.ShouldContain(q => q.OrganizationUnitId == ou112.Id);
await _identityUserManager.RemoveFromOrganizationUnitAsync(user.Id, ou112.Id);
await uow.CompleteAsync();
}
var updatedUser = await _identityUserRepository.FindByNormalizedUserNameAsync(
_lookupNormalizer.NormalizeName("david"));
updatedUser.OrganizationUnits.Count.ShouldBe(0);
}
[Fact]
public async Task GetOrganizationUnitAsync()
{
@ -79,6 +164,7 @@ namespace Volo.Abp.Identity
ou.Roles.ShouldNotBeNull();
ou.Roles.Any().ShouldBeTrue();
}
[Fact]
public async Task GetOrganizationUnitRolesAsync()
{
@ -95,7 +181,8 @@ namespace Volo.Abp.Identity
{
OrganizationUnit ou = await _organizationUnitRepository.GetAsync("OU111", includeDetails: true);
var ou111Roles = await _organizationUnitRepository.GetRolesAsync(ou, sorting: "name desc", maxResultCount: 1, includeDetails: true);
var ou111Roles = await _organizationUnitRepository.GetRolesAsync(ou, sorting: "name desc",
maxResultCount: 1, includeDetails: true);
ou111Roles.Count.ShouldBe(1);
ou111Roles.ShouldContain(n => n.Name == "moderator");
}
@ -105,9 +192,10 @@ namespace Volo.Abp.Identity
{
OrganizationUnit ou1 = await _organizationUnitRepository.GetAsync("OU111", true);
OrganizationUnit ou2 = await _organizationUnitRepository.GetAsync("OU112", true);
var users = await _identityUserRepository.GetUsersInOrganizationsListAsync(new List<Guid> { ou1.Id, ou2.Id });
var users = await _identityUserRepository.GetUsersInOrganizationsListAsync(new List<Guid> {ou1.Id, ou2.Id});
users.Count.ShouldBeGreaterThan(0);
}
[Fact]
public async Task GetMembersInOrganizationUnitWithParamsAsync()
{
@ -147,12 +235,12 @@ namespace Volo.Abp.Identity
public async Task GetMembersCountOfOrganizationUnitWithParamsAsync()
{
OrganizationUnit ou = await _organizationUnitRepository.GetAsync("OU111", true);
var usersCount = await _organizationUnitRepository.GetMembersCountAsync(ou,"n");
var usersCount = await _organizationUnitRepository.GetMembersCountAsync(ou, "n");
usersCount.ShouldBeGreaterThan(1);
usersCount.ShouldBeLessThanOrEqualTo(5);
usersCount = await _organizationUnitRepository.GetMembersCountAsync(ou,"undefined-username");
usersCount = await _organizationUnitRepository.GetMembersCountAsync(ou, "undefined-username");
usersCount.ShouldBe(0);
}

13
npm/ng-packs/packages/core/src/lib/strategies/auth-flow.strategy.ts

@ -25,7 +25,7 @@ export abstract class AuthFlowStrategy {
constructor(protected injector: Injector) {
this.store = injector.get(Store);
this.oAuthService = injector.get(OAuthService);
this.oAuthConfig = injector.get(Store).selectSnapshot(ConfigState.getDeep('environment.oAuthConfig'));
this.oAuthConfig = this.store.selectSnapshot(ConfigState.getDeep('environment.oAuthConfig'));
}
async init(): Promise<any> {
@ -41,7 +41,14 @@ export class AuthCodeFlowStrategy extends AuthFlowStrategy {
return super
.init()
.then(() => this.oAuthService.tryLogin())
.then(() => this.oAuthService.setupAutomaticSilentRefresh());
.then(() => {
if (this.oAuthService.hasValidAccessToken() || !this.oAuthService.getRefreshToken()) {
return Promise.resolve();
}
return this.oAuthService.refreshToken() as Promise<any>;
})
.then(() => this.oAuthService.setupAutomaticSilentRefresh({}, 'access_token'));
}
login() {
@ -88,7 +95,7 @@ export class AuthPasswordFlowStrategy extends AuthFlowStrategy {
)
.pipe(
tap(() => this.oAuthService.logOut()),
switchMap(() => this.store.dispatch(new GetAppConfiguration())),
switchMap(() => this.store.dispatch(new GetAppConfiguration())),
);
}

18
npm/ng-packs/packages/schematics/src/collection.json

@ -1,9 +1,19 @@
{
"schematics": {
"proxy": {
"description": "ABP Proxy Generator Schematics",
"factory": "./commands/proxy",
"schema": "./commands/proxy/schema.json"
"proxy-add": {
"description": "ABP Proxy Generator Add Schematics",
"factory": "./commands/proxy-add",
"schema": "./commands/proxy-add/schema.json"
},
"proxy-refresh": {
"description": "ABP Proxy Generator Refresh Schematics",
"factory": "./commands/proxy-refresh",
"schema": "./commands/proxy-refresh/schema.json"
},
"proxy-remove": {
"description": "ABP Proxy Generator Remove Schematics",
"factory": "./commands/proxy-remove",
"schema": "./commands/proxy-remove/schema.json"
},
"api": {
"description": "ABP API Generator Schematics",

0
npm/ng-packs/packages/schematics/src/commands/api/files-enum/shared/enums/__namespace@dir__/__name@kebab__.ts.template → npm/ng-packs/packages/schematics/src/commands/api/files-enum/proxy/__namespace@dir__/__name@kebab__.enum.ts.template

0
npm/ng-packs/packages/schematics/src/commands/api/files-model/shared/models/__namespace@dir__/index.ts.template → npm/ng-packs/packages/schematics/src/commands/api/files-model/proxy/__namespace@dir__/models.ts.template

0
npm/ng-packs/packages/schematics/src/commands/api/files-service/shared/services/__namespace@dir__/__name@kebab__.service.ts.template → npm/ng-packs/packages/schematics/src/commands/api/files-service/proxy/__namespace@dir__/__name@kebab__.service.ts.template

22
npm/ng-packs/packages/schematics/src/commands/api/index.ts

@ -10,15 +10,17 @@ import {
url,
} from '@angular-devkit/schematics';
import { Exception } from '../../enums';
import { ServiceGeneratorParams } from '../../models';
import { GenerateProxySchema, ServiceGeneratorParams } from '../../models';
import {
applyWithOverwrite,
buildDefaultPath,
createApiDefinitionReader,
createControllerToServiceMapper,
createImportRefsToModelReducer,
createImportRefToEnumMapper,
createProxyConfigReader,
createProxyConfigWriterCreator,
EnumGeneratorParams,
generateProxyConfigJson,
getEnumNamesFromImports,
getRootNamespace,
interpolate,
@ -28,7 +30,6 @@ import {
serializeParameters,
} from '../../utils';
import * as cases from '../../utils/text';
import { Schema as GenerateProxySchema } from './schema';
export default function(schema: GenerateProxySchema) {
const params = removeDefaultPlaceholders(schema);
@ -40,9 +41,9 @@ export default function(schema: GenerateProxySchema) {
const target = await resolveProject(tree, params.target!);
const solution = getRootNamespace(tree, source, moduleName);
const targetPath = buildDefaultPath(target.definition);
const definitionPath = `${targetPath}/shared/api-definition.json`;
const readApiDefinition = createApiDefinitionReader(definitionPath);
const data = readApiDefinition(tree);
const readProxyConfig = createProxyConfigReader(targetPath);
const createProxyConfigWriter = createProxyConfigWriterCreator(targetPath);
const data = readProxyConfig(tree);
const types = data.types;
const modules = data.modules;
if (!types || !modules) throw new SchematicsException(Exception.InvalidApiDefinition);
@ -80,7 +81,14 @@ export default function(schema: GenerateProxySchema) {
modelImports,
});
return branchAndMerge(chain([generateServices, generateModels, generateEnums]));
if (!data.generated.includes(moduleName)) data.generated.push(moduleName);
data.generated.sort();
const json = generateProxyConfigJson(data);
const overwriteProxyConfig = createProxyConfigWriter('overwrite', json);
return branchAndMerge(
chain([generateServices, generateModels, generateEnums, overwriteProxyConfig]),
);
},
]);
}

16
npm/ng-packs/packages/schematics/src/commands/api/schema.ts

@ -1,16 +0,0 @@
export interface Schema {
/**
* Backend module to generate code for
*/
module?: string;
/**
* Angular project to resolve root namespace & API definition URL from
*/
source?: string;
/**
* Angular project to generate code in
*/
target?: string;
}

60
npm/ng-packs/packages/schematics/src/commands/proxy-add/index.ts

@ -0,0 +1,60 @@
import { strings } from '@angular-devkit/core';
import { chain, SchematicContext, Tree } from '@angular-devkit/schematics';
import { GenerateProxySchema } from '../../models';
import {
buildDefaultPath,
createApiDefinitionGetter,
createApisGenerator,
createProxyClearer,
createProxyConfigReader,
createProxyConfigSaver,
createProxyIndexGenerator,
createProxyWarningSaver,
mergeAndAllowDelete,
removeDefaultPlaceholders,
resolveProject,
} from '../../utils';
export default function(schema: GenerateProxySchema) {
const params = removeDefaultPlaceholders(schema);
const moduleName = strings.camelize(params.module || 'app');
return chain([
async (host: Tree, _context: SchematicContext) => {
const target = await resolveProject(host, params.target!);
const targetPath = buildDefaultPath(target.definition);
const readProxyConfig = createProxyConfigReader(targetPath);
let generated: string[] = [];
try {
generated = readProxyConfig(host).generated;
const index = generated.findIndex(m => m === moduleName);
if (index < 0) generated.push(moduleName);
} catch (_) {
generated.push(moduleName);
}
const getApiDefinition = createApiDefinitionGetter(params);
const data = { generated, ...(await getApiDefinition(host)) };
data.generated = [];
const clearProxy = createProxyClearer(targetPath);
const saveProxyConfig = createProxyConfigSaver(data, targetPath);
const saveProxyWarning = createProxyWarningSaver(targetPath);
const generateApis = createApisGenerator(schema, generated);
const generateIndex = createProxyIndexGenerator(targetPath);
return chain([
mergeAndAllowDelete(host, clearProxy),
saveProxyConfig,
saveProxyWarning,
generateApis,
generateIndex,
]);
},
]);
}

12
npm/ng-packs/packages/schematics/src/commands/proxy/schema.json → npm/ng-packs/packages/schematics/src/commands/proxy-add/schema.json

@ -6,33 +6,33 @@
"properties": {
"module": {
"alias": "m",
"description": "Backend module to generate code for",
"description": "Backend module name",
"type": "string",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "Please enter name of the backend module you wish to generate proxies for. (default: \"app\")"
"x-prompt": "Please enter backend module name. (default: \"app\")"
},
"source": {
"alias": "s",
"description": "Angular project to resolve root namespace & API definition URL from",
"description": "Source Angular project for API definition URL & root namespace resolution",
"type": "string",
"$default": {
"$source": "argv",
"index": 1
},
"x-prompt": "Plese enter Angular project name to resolve root namespace & API definition URL from. (default: workspace \"defaultProject\")"
"x-prompt": "Plese enter source Angular project for API definition URL & root namespace resolution. (default: workspace \"defaultProject\")"
},
"target": {
"alias": "t",
"description": "Angular project to generate code in",
"description": "Target Angular project to place the generated code",
"type": "string",
"$default": {
"$source": "argv",
"index": 2
},
"x-prompt": "Plese enter Angular project name to place generated code in. (default: workspace \"defaultProject\")"
"x-prompt": "Plese enter target Angular project to place the generated code. (default: workspace \"defaultProject\")"
}
},
"required": []

45
npm/ng-packs/packages/schematics/src/commands/proxy-refresh/index.ts

@ -0,0 +1,45 @@
import { chain, SchematicContext, Tree } from '@angular-devkit/schematics';
import { GenerateProxySchema } from '../../models';
import {
buildDefaultPath,
createApiDefinitionGetter,
createApisGenerator,
createProxyClearer,
createProxyConfigReader,
createProxyConfigSaver,
createProxyIndexGenerator,
mergeAndAllowDelete,
removeDefaultPlaceholders,
resolveProject,
} from '../../utils';
export default function(schema: GenerateProxySchema) {
const params = removeDefaultPlaceholders(schema);
return async (host: Tree, _context: SchematicContext) => {
const target = await resolveProject(host, params.target!);
const targetPath = buildDefaultPath(target.definition);
const readProxyConfig = createProxyConfigReader(targetPath);
const { generated } = readProxyConfig(host);
const getApiDefinition = createApiDefinitionGetter(params);
const data = { generated, ...(await getApiDefinition(host)) };
data.generated = [];
const clearProxy = createProxyClearer(targetPath);
const saveProxyConfig = createProxyConfigSaver(data, targetPath);
const generateApis = createApisGenerator(schema, generated);
const generateIndex = createProxyIndexGenerator(targetPath);
return chain([
mergeAndAllowDelete(host, clearProxy),
saveProxyConfig,
generateApis,
generateIndex,
]);
};
}

39
npm/ng-packs/packages/schematics/src/commands/proxy-refresh/schema.json

@ -0,0 +1,39 @@
{
"$schema": "http://json-schema.org/schema",
"id": "SchematicsAbpGenerateProxy",
"title": "ABP Generate Proxy Schema",
"type": "object",
"properties": {
"module": {
"alias": "m",
"description": "Backend module name",
"type": "string",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "Please enter backend module name. (default: \"app\")"
},
"source": {
"alias": "s",
"description": "Source Angular project for API definition URL & root namespace resolution",
"type": "string",
"$default": {
"$source": "argv",
"index": 1
},
"x-prompt": "Plese enter source Angular project for API definition URL & root namespace resolution. (default: workspace \"defaultProject\")"
},
"target": {
"alias": "t",
"description": "Target Angular project to place the generated code",
"type": "string",
"$default": {
"$source": "argv",
"index": 2
},
"x-prompt": "Plese enter target Angular project to place the generated code. (default: workspace \"defaultProject\")"
}
},
"required": []
}

51
npm/ng-packs/packages/schematics/src/commands/proxy-remove/index.ts

@ -0,0 +1,51 @@
import { strings } from '@angular-devkit/core';
import { chain, SchematicContext, Tree } from '@angular-devkit/schematics';
import { GenerateProxySchema } from '../../models';
import {
buildDefaultPath,
createApiDefinitionGetter,
createApisGenerator,
createProxyClearer,
createProxyConfigReader,
createProxyConfigSaver,
createProxyIndexGenerator,
mergeAndAllowDelete,
removeDefaultPlaceholders,
resolveProject,
} from '../../utils';
export default function(schema: GenerateProxySchema) {
const params = removeDefaultPlaceholders(schema);
const moduleName = strings.camelize(params.module || 'app');
return async (host: Tree, _context: SchematicContext) => {
const target = await resolveProject(host, params.target!);
const targetPath = buildDefaultPath(target.definition);
const readProxyConfig = createProxyConfigReader(targetPath);
const { generated } = readProxyConfig(host);
const index = generated.findIndex(m => m === moduleName);
if (index < 0) return host;
generated.splice(index, 1);
const getApiDefinition = createApiDefinitionGetter(params);
const data = { generated, ...(await getApiDefinition(host)) };
data.generated = [];
const clearProxy = createProxyClearer(targetPath);
const saveProxyConfig = createProxyConfigSaver(data, targetPath);
const generateApis = createApisGenerator(schema, generated);
const generateIndex = createProxyIndexGenerator(targetPath);
return chain([
mergeAndAllowDelete(host, clearProxy),
saveProxyConfig,
generateApis,
generateIndex,
]);
};
}

39
npm/ng-packs/packages/schematics/src/commands/proxy-remove/schema.json

@ -0,0 +1,39 @@
{
"$schema": "http://json-schema.org/schema",
"id": "SchematicsAbpGenerateProxy",
"title": "ABP Generate Proxy Schema",
"type": "object",
"properties": {
"module": {
"alias": "m",
"description": "Backend module name",
"type": "string",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "Please enter backend module name. (default: \"app\")"
},
"source": {
"alias": "s",
"description": "Source Angular project for API definition URL & root namespace resolution",
"type": "string",
"$default": {
"$source": "argv",
"index": 1
},
"x-prompt": "Plese enter source Angular project for API definition URL & root namespace resolution. (default: workspace \"defaultProject\")"
},
"target": {
"alias": "t",
"description": "Target Angular project to place the generated code",
"type": "string",
"$default": {
"$source": "argv",
"index": 2
},
"x-prompt": "Plese enter target Angular project to place the generated code. (default: workspace \"defaultProject\")"
}
},
"required": []
}

42
npm/ng-packs/packages/schematics/src/commands/proxy/index.ts

@ -1,42 +0,0 @@
import { strings } from '@angular-devkit/core';
import {
branchAndMerge,
chain,
schematic,
SchematicContext,
Tree,
} from '@angular-devkit/schematics';
import { API_DEFINITION_ENDPOINT } from '../../constants';
import { ApiDefinition } from '../../models';
import {
buildDefaultPath,
createApiDefinitionSaver,
getApiDefinition,
getSourceUrl,
removeDefaultPlaceholders,
resolveProject,
} from '../../utils';
import { Schema as GenerateProxySchema } from './schema';
export default function(schema: GenerateProxySchema) {
const params = removeDefaultPlaceholders(schema);
const moduleName = strings.camelize(params.module || 'app');
return chain([
async (tree: Tree, _context: SchematicContext) => {
const source = await resolveProject(tree, params.source!);
const target = await resolveProject(tree, params.target!);
const sourceUrl = getSourceUrl(tree, source, moduleName);
const targetPath = buildDefaultPath(target.definition);
const data: ApiDefinition = await getApiDefinition(sourceUrl + API_DEFINITION_ENDPOINT);
const saveApiDefinition = createApiDefinitionSaver(
data,
`${targetPath}/shared/api-definition.json`,
);
const createApi = schematic('api', schema);
return branchAndMerge(chain([saveApiDefinition, createApi]));
},
]);
}

16
npm/ng-packs/packages/schematics/src/commands/proxy/schema.ts

@ -1,16 +0,0 @@
export interface Schema {
/**
* Backend module to generate code for
*/
module?: string;
/**
* Angular project to resolve root namespace & API definition URL from
*/
source?: string;
/**
* Angular project to generate code in
*/
target?: string;
}

1
npm/ng-packs/packages/schematics/src/constants/index.ts

@ -1,3 +1,4 @@
export * from './api';
export * from './proxy';
export * from './system-types';
export * from './volo';

18
npm/ng-packs/packages/schematics/src/constants/proxy.ts

@ -0,0 +1,18 @@
export const PROXY_PATH = '/proxy';
export const PROXY_CONFIG_PATH = `${PROXY_PATH}/generate-proxy.json`;
export const PROXY_WARNING_PATH = `${PROXY_PATH}/README.md`;
export const PROXY_WARNING = `# Proxy Generation Output
This directory includes the output of the latest proxy generation.
The files and folders in it will be overwritten when proxy generation is run again.
Therefore, please do not place your own content in this folder.
In addition, \`generate-proxy.json\` works like a lock file.
It includes information used by the proxy generator, so please do not delete or modify it.
Finally, the name of the files and folders should not be changed for two reasons:
- Proxy generator will keep creating them at those paths and you will have multiple copies of the same content.
- ABP Suite generates files which include imports from this folder.
`;

4
npm/ng-packs/packages/schematics/src/enums/exception.ts

@ -1,11 +1,13 @@
export const enum Exception {
DirRemoveFailed = '[Directory Remove Failed] Cannot remove "{0}".',
FileNotFound = '[File Not Found] There is no file at "{0}" path.',
FileWriteFailed = '[File Write Failed] Cannot write file at "{0}".',
InvalidModule = '[Invalid Module] Backend module "{0}" does not exist in API definition.',
InvalidApiDefinition = '[Invalid API Definition] The provided API definition is invalid.',
InvalidWorkspace = '[Invalid Workspace] The angular.json should be a valid JSON file.',
NoApi = '[API Not Available] Please double-check the URL in the source project environment and make sure your application is up and running.',
NoApiDefinition = '[API Definition Not Found] There is no API definition file at "{0}".',
NoProject = '[Project Not Found] Either define a default project in your workspace or specify the project name in schematics options.',
NoProxyConfig = '[Proxy Config Not Found] There is no JSON file at "{0}".',
NoTypeDefinition = '[Type Definition Not Found] There is no type definition for "{0}".',
NoWorkspace = '[Workspace Not Found] Make sure you are running schematics at the root directory of your workspace and it has an angular.json file.',
NoEnvironment = '[Environment Not Found] An environment file cannot be located in "{0}" project.',

16
npm/ng-packs/packages/schematics/src/models/generate-proxy-schema.ts

@ -0,0 +1,16 @@
export interface GenerateProxySchema {
/**
* Backend module name
*/
module?: string;
/**
* Source Angular project for API definition URL & root namespace resolution
*/
source?: string;
/**
* Target Angular project to place the generated code
*/
target?: string;
}

3
npm/ng-packs/packages/schematics/src/models/index.ts

@ -1,7 +1,10 @@
export * from './api-definition';
export * from './generate-proxy-schema';
export * from './import';
export * from './method';
export * from './model';
export * from './project';
export * from './proxy-config';
export * from './service';
export * from './tree';
export * from './util';

5
npm/ng-packs/packages/schematics/src/models/proxy-config.ts

@ -0,0 +1,5 @@
import { ApiDefinition } from './api-definition';
export interface ProxyConfig extends ApiDefinition {
generated: string[];
}

1
npm/ng-packs/packages/schematics/src/models/tree.ts

@ -0,0 +1 @@
export type WriteOp = 'create' | 'overwrite';

6
npm/ng-packs/packages/schematics/src/utils/api.ts

@ -0,0 +1,6 @@
import { chain, schematic } from '@angular-devkit/schematics';
import { GenerateProxySchema } from '../models';
export function createApisGenerator(schema: GenerateProxySchema, generated: string[]) {
return chain(generated.map(m => schematic('api', { ...schema, module: m })));
}

48
npm/ng-packs/packages/schematics/src/utils/barrel.ts

@ -0,0 +1,48 @@
import { strings } from '@angular-devkit/core';
import { Tree } from '@angular-devkit/schematics';
import { PROXY_PATH } from '../constants';
import { createFileSaver } from './file';
export function createProxyIndexGenerator(targetPath: string) {
return createBarrelsGenerator(targetPath + PROXY_PATH);
}
export function createBarrelsGenerator(rootPath: string) {
return (tree: Tree) => {
generateBarrelFromPath(tree, rootPath);
return tree;
};
}
export function generateBarrelFromPath(tree: Tree, indexPath: string) {
const saveFile = createFileSaver(tree);
const dir = tree.getDir(indexPath);
const _exports: string[] = [];
dir.subfiles.forEach(fragment => {
if (!fragment.endsWith('.ts')) return;
_exports.push(`export * from './${fragment.replace(/\.ts$/, '')}';`);
});
dir.subdirs.forEach(fragment => {
const subDirPath = indexPath + '/' + fragment;
const subDir = tree.getDir(subDirPath);
let hasFiles = false;
subDir.visit(() => (hasFiles = true));
if (!hasFiles) return;
_exports.push(`export * as ${strings.classify(fragment)} from './${fragment}';`);
generateBarrelFromPath(tree, subDirPath);
});
_exports.sort();
if (_exports.length)
saveFile(
indexPath + '/index.ts',
_exports.join(`
`),
);
}

2
npm/ng-packs/packages/schematics/src/utils/enum.ts

@ -13,7 +13,7 @@ export interface EnumGeneratorParams {
}
export function isEnumImport(path: string) {
return path.includes('/enums/');
return path.endsWith('.enum');
}
export function getEnumNamesFromImports(serviceImports: Record<string, string[]>) {

8
npm/ng-packs/packages/schematics/src/utils/file.ts

@ -0,0 +1,8 @@
import { Tree } from '@angular-devkit/schematics';
export function createFileSaver(tree: Tree) {
return (filePath: string, fileContent: string) =>
tree.exists(filePath)
? tree.overwrite(filePath, fileContent)
: tree.create(filePath, fileContent);
}

3
npm/ng-packs/packages/schematics/src/utils/index.ts

@ -1,7 +1,10 @@
export * from './angular';
export * from './api';
export * from './ast';
export * from './barrel';
export * from './common';
export * from './enum';
export * from './file';
export * from './import';
export * from './model';
export * from './namespace';

32
npm/ng-packs/packages/schematics/src/utils/path.ts

@ -1,15 +1,33 @@
import { dir, kebab } from './text';
import { strings } from '@angular-devkit/core';
import { kebab } from './text';
export function relativePathToEnum(namespace: string, enumNamespace: string, enumName: string) {
const repeats = namespace ? namespace.split('.').length : 0;
const path = '..' + '/..'.repeat(repeats) + '/enums/' + dir(enumNamespace);
return removeDoubleSlash(path + '/' + kebab(enumName));
const path = calculateRelativePath(namespace, enumNamespace);
return path + `/${kebab(enumName)}.enum`;
}
export function relativePathToModel(namespace: string, modelNamespace: string) {
const repeats = namespace ? namespace.split('.').length : 0;
const path = '..' + '/..'.repeat(repeats) + '/models/' + dir(modelNamespace);
return removeTrailingSlash(path);
const path = calculateRelativePath(namespace, modelNamespace);
return path + '/models';
}
function calculateRelativePath(ns1: string, ns2: string) {
if (ns1 === ns2) return '.';
const parts1 = ns1 ? ns1.split('.') : [];
const parts2 = ns2 ? ns2.split('.') : [];
while (parts1.length && parts2.length) {
if (parts1[0] !== parts2[0]) break;
parts1.shift();
parts2.shift();
}
const up = '../'.repeat(parts1.length) || '.';
const down = parts2.reduce((acc, p) => acc + '/' + strings.dasherize(p), '');
return removeTrailingSlash(removeDoubleSlash(up + down));
}
function removeDoubleSlash(path: string) {

9
npm/ng-packs/packages/schematics/src/utils/rule.ts

@ -1,6 +1,8 @@
import {
apply,
callRule,
forEach,
MergeStrategy,
mergeWith,
Rule,
SchematicContext,
@ -16,6 +18,13 @@ export function applyWithOverwrite(source: Source, rules: Rule[]): Rule {
};
}
export function mergeAndAllowDelete(host: Tree, rule: Rule) {
return async (tree: Tree, context: SchematicContext) => {
const nextTree = await callRule(rule, tree, context).toPromise();
host.merge(nextTree, MergeStrategy.AllowDeleteConflict);
};
}
export function overwriteFileIfExists(tree: Tree): Rule {
return forEach(fileEntry => {
if (!tree.exists(fileEntry.path)) return fileEntry;

127
npm/ng-packs/packages/schematics/src/utils/source.ts

@ -1,13 +1,32 @@
import { strings } from '@angular-devkit/core';
import { SchematicsException, Tree } from '@angular-devkit/schematics';
import got from 'got';
import {
API_DEFINITION_ENDPOINT,
PROXY_CONFIG_PATH,
PROXY_PATH,
PROXY_WARNING,
PROXY_WARNING_PATH,
} from '../constants';
import { Exception } from '../enums';
import { ApiDefinition, Project } from '../models';
import { ApiDefinition, GenerateProxySchema, Project, ProxyConfig, WriteOp } from '../models';
import { getAssignedPropertyFromObjectliteral } from './ast';
import { interpolate } from './common';
import { readEnvironment } from './workspace';
import { readEnvironment, resolveProject } from './workspace';
export async function getApiDefinition(url: string) {
let body: any;
export function createApiDefinitionGetter(params: GenerateProxySchema) {
const moduleName = strings.camelize(params.module || 'app');
return async (host: Tree) => {
const source = await resolveProject(host, params.source!);
const sourceUrl = getSourceUrl(host, source, moduleName);
return await getApiDefinition(sourceUrl);
};
}
async function getApiDefinition(sourceUrl: string) {
const url = sourceUrl + API_DEFINITION_ENDPOINT;
let body: ApiDefinition;
try {
({ body } = await got(url, {
@ -17,9 +36,10 @@ export async function getApiDefinition(url: string) {
}));
} catch ({ response }) {
// handle redirects
if (response?.body && response.statusCode < 400) return response.body;
if (response.statusCode >= 400 || !response?.body)
throw new SchematicsException(Exception.NoApi);
throw new SchematicsException(Exception.NoApi);
body = response.body;
}
return body;
@ -71,23 +91,100 @@ export function getSourceUrl(tree: Tree, project: Project, moduleName: string) {
return assignment.replace(/[`'"]/g, '');
}
export function createApiDefinitionReader(targetPath: string) {
export function createProxyConfigReader(targetPath: string) {
targetPath += PROXY_CONFIG_PATH;
return (tree: Tree) => {
try {
const buffer = tree.read(targetPath);
const apiDefinition: ApiDefinition = JSON.parse(buffer!.toString());
return apiDefinition;
return JSON.parse(buffer!.toString()) as ProxyConfig;
} catch (_) {}
throw new SchematicsException(interpolate(Exception.NoApiDefinition, targetPath));
throw new SchematicsException(interpolate(Exception.NoProxyConfig, targetPath));
};
}
export function createProxyClearer(targetPath: string) {
targetPath += PROXY_PATH;
const proxyIndexPath = `${targetPath}/index.ts`;
return (tree: Tree) => {
try {
tree.getDir(targetPath).subdirs.forEach(dirName => {
const dirPath = `${targetPath}/${dirName}`;
tree.getDir(dirPath).visit(filePath => tree.delete(filePath));
tree.delete(dirPath);
});
if (tree.exists(proxyIndexPath)) tree.delete(proxyIndexPath);
return tree;
} catch (_) {
throw new SchematicsException(interpolate(Exception.DirRemoveFailed, targetPath));
}
};
}
export function createApiDefinitionSaver(apiDefinition: ApiDefinition, targetPath: string) {
export function createProxyWarningSaver(targetPath: string) {
targetPath += PROXY_WARNING_PATH;
const createFileWriter = createFileWriterCreator(targetPath);
return (tree: Tree) => {
tree[tree.exists(targetPath) ? 'overwrite' : 'create'](
targetPath,
JSON.stringify(apiDefinition, null, 2),
);
const op = tree.exists(targetPath) ? 'overwrite' : 'create';
const writeWarningMD = createFileWriter(op, PROXY_WARNING);
writeWarningMD(tree);
return tree;
};
}
export function createProxyConfigSaver(apiDefinition: ApiDefinition, targetPath: string) {
const createProxyConfigJson = createProxyConfigJsonCreator(apiDefinition);
const readPreviousConfig = createProxyConfigReader(targetPath);
const createProxyConfigWriter = createProxyConfigWriterCreator(targetPath);
targetPath += PROXY_CONFIG_PATH;
return (tree: Tree) => {
const generated: string[] = [];
let op: WriteOp = 'create';
if (tree.exists(targetPath)) {
op = 'overwrite';
try {
readPreviousConfig(tree).generated.forEach(m => generated.push(m));
} catch (_) {}
}
const json = createProxyConfigJson(generated);
const writeProxyConfig = createProxyConfigWriter(op, json);
writeProxyConfig(tree);
return tree;
};
}
export function createProxyConfigWriterCreator(targetPath: string) {
targetPath += PROXY_CONFIG_PATH;
return createFileWriterCreator(targetPath);
}
export function createFileWriterCreator(targetPath: string) {
return (op: WriteOp, data: string) => (tree: Tree) => {
try {
tree[op](targetPath, data);
return tree;
} catch (_) {}
throw new SchematicsException(interpolate(Exception.FileWriteFailed, targetPath));
};
}
export function createProxyConfigJsonCreator(apiDefinition: ApiDefinition) {
return (generated: string[]) => generateProxyConfigJson({ generated, ...apiDefinition });
}
export function generateProxyConfigJson(proxyConfig: ProxyConfig) {
return JSON.stringify(proxyConfig, null, 2);
}

4
npm/ng-packs/scripts/build-schematics.ts

@ -20,7 +20,9 @@ class FileCopy {
const PACKAGE_TO_BUILD = 'schematics';
const FILES_TO_COPY_AFTER_BUILD: (FileCopy | string)[] = [
{ src: 'src/commands/proxy/schema.json', dest: 'commands/proxy/schema.json' },
{ src: 'src/commands/proxy-add/schema.json', dest: 'commands/proxy-add/schema.json' },
{ src: 'src/commands/proxy-refresh/schema.json', dest: 'commands/proxy-refresh/schema.json' },
{ src: 'src/commands/proxy-remove/schema.json', dest: 'commands/proxy-remove/schema.json' },
{ src: 'src/commands/api/files-enum', dest: 'commands/api/files-enum' },
{ src: 'src/commands/api/files-model', dest: 'commands/api/files-model' },
{ src: 'src/commands/api/files-service', dest: 'commands/api/files-service' },

4
npm/ng-packs/tsconfig.base.json

@ -29,7 +29,9 @@
"@abp/ng.setting-management": ["packages/setting-management/src/public-api.ts"],
"@abp/ng.setting-management/config": ["packages/setting-management/config/src/public-api.ts"],
"@abp/ng.permission-management": ["packages/permission-management/src/public-api.ts"],
"@abp/ng.feature-management": ["packages/feature-management/src/public-api.ts"]
"@abp/ng.feature-management": ["packages/feature-management/src/public-api.ts"],
"@proxy": ["apps/dev-app/src/app/proxy/index.ts"],
"@proxy/*": ["apps/dev-app/src/app/proxy/*"]
}
},
"angularCompilerOptions": {

6
templates/app/angular/tsconfig.base.json

@ -12,7 +12,11 @@
"importHelpers": true,
"target": "es2015",
"typeRoots": ["node_modules/@types"],
"lib": ["es2018", "dom"]
"lib": ["es2018", "dom"],
"paths": {
"@proxy": ["src/app/proxy/index.ts"],
"@proxy/*": ["src/app/proxy/*"]
}
},
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,

4
templates/module/angular/tsconfig.base.json

@ -7,7 +7,9 @@
],
"@my-company-name/my-project-name/config": [
"projects/my-project-name/config/src/public-api.ts"
]
],
"@proxy": ["projects/my-project-name/src/lib/proxy/index.ts"],
"@proxy/*": ["projects/my-project-name/src/lib/proxy/*"]
}
}
}

Loading…
Cancel
Save