diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json index f8d5de9688..03d1ad0cba 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json @@ -536,7 +536,7 @@ "Pricing_Page_Testimonial_3": "We love ABP. We don't have to write everything from scratch. We start from out-of-the-box features and just focus on what we really need to write. Also, ABP is well-architected and the code is high quality with fewer bugs. If we would have to write everything we needed on our own, we might have to spend years. Once more things we like is that the new version, or issue fixing, or improvement come out very soon every other week. We don't wait too long.", "Pricing_Page_Testimonial_4": "ABP Commercial is a fantastic product would recommend. Commercial products to market for our customers in a single configurable platform. The jump start that the framework and tooling provide any team is worth every cent. ABP Commercial was the best fit for our needs.", "AbpBookDownloadArea_ClaimYourEBook": "Claim your Mastering ABP Framework E-Book", - "AddMemberModal_Warning_1": "If the username you are trying to add doesn't exist in the system, please ask your team member to register on {0} and share the username of his/her account with you.", + "AddMemberModal_Warning_1": "If the username you are trying to add doesn't exist in the system, please ask your team member to register on {0} and share the username of his/her account with you.", "MyOrganizations_Detail_WelcomeMessage": "Welcome to your organization, {0}", "MyOrganizations_Detail_OrganizationManagement": "Organization Management", "OrganizationDisplayName": "Organization Display Name", @@ -562,7 +562,7 @@ "MyOrganizations_Detail_PaymentProviderInfo": "If you have purchased your license through {0} gateway, it sends the PDF invoice to your email address, see {0} invoicing.", "MyOrganizations_Detail_PayUInfo": "If you have purchased through the PayU gateway, click the \"Request Invoice\" button and fill in the billing information.", "MyOrganizations_Detail_ConclusionInfo": "Your invoice request will be concluded within {0} business days.", - "ExtendYourLicense": "Extend Your {0} License", + "ExtendYourLicense": "Extend your {0} license", "Continue": "Continue", "PurchaseLicense": "Purchase {0} license", "DownloadInvoiceModal_DownloadInvoice": "Download Invoice", diff --git a/docs/en/Startup-Templates/Application-Single-Layer.md b/docs/en/Startup-Templates/Application-Single-Layer.md new file mode 100644 index 0000000000..ce194d7585 --- /dev/null +++ b/docs/en/Startup-Templates/Application-Single-Layer.md @@ -0,0 +1,106 @@ +# Application (Single Layer) Startup Template + +## Introduction + +This template provides a single-layered application for a quick start with ABP Framework. + +This document explains the **solution structure** and project in detail. + +### What is the Difference Between the Application Startup Templates? + +ABP's [Application Startup Template](Application.md) provides a well-organized and layered solution to create maintainable business applications based on the [Domain Driven Design](../Domain-Driven-Design.md) (DDD) practices. However, some developers find this template a little bit complex (or unnecessary) for simple and short-time applications. + +At this point, a single-layer application template has been created for such applications. This template has the same functionality, features and modules on runtime with the [Application Startup Template](Application.md) but the development model is minimal and everything is in a single project (`.csproj`). + +## How to Start with It? + +You can use the [ABP CLI](../CLI.md) to create a new project using this startup template. Alternatively, you can directly create & download this startup template from the [Get Started](https://abp.io/get-started) page. How to download via CLI is explained in this section. + +Firstly, install the ABP CLI if you haven't installed it before: + +```bash +dotnet tool install -g Volo.Abp.Cli +``` + +Then, use the `abp new` command in an empty folder to create a new solution: + +```bash +abp new Acme.BookStore -t app-nolayers +``` + +* `Acme.BookStore` is the solution name, like *YourCompany.YourProduct*. You can use single-level, two-level or three-level naming. +* In this example, the `-t` option (or `--template` option) specifies the template name. + +### Specify the UI Framework + +This template provides multiple UI frameworks: + +* `mvc`: ASP.NET Core MVC UI with Razor Pages (default) +* `blazor-server`: Blazor Server UI +* `angular`: Angular UI +* `none`: Without UI + +> Currently, this template doesn't have Blazor WASM UI, because it requires 3 projects at least (server-side, UI and shared library between these two projects). + +Use the `-u` or `--ui` option to specify the UI framework while creating the solution: + +```bash +abp new Acme.BookStore -t app-nolayers -u angular +``` + +* This example specifies the UI type (`-u` option) as `angular`, you can also specify `mvc` or `blazor-server` for UI type. + +### Specify the Database Provider + +This template supports the following database providers: + +- `ef`: Entity Framework Core (default) +- `mongodb`: MongoDB + +Use the `-d` (or `--database-provider`) option to specify the database provider while creating the solution: + +```bash +abp new Acme.BookStore -t app-nolayers -d mongodb +``` + +## Solution Structure + +If you don't specify any additional options while creating an `app-nolayers` template, you will have a solution as shown below: + +![](../images/bookstore-single-layer-solution-structure.png) + +It's a single-layer template rather than a layered-architecture solution (like the `Application Startup Template`). It's helpful to create a running application quickly without considering the layered architecture. + +### Folder Structure + +Since this template provides a single-layer solution, we've separated concerns into folders instead of layers and you can see the pre-defined folders as shown below: + +![](../images/single-layer-folder-structure.png) + +* You can define your `entities`, `application services`, `DTOs`, etc. in this single project (in the related folders). +* For example, you can define your `application services` and `DTOs` under the **Services** folder. + +### How to Run? + +Before running the application, you need to create the database and seed the initial data. To do that, you can run the following command in the directory of your project: + +```bash +dotnet run --migrate-database +``` + +* This command will create the database and seed the initial data for you. Then you can run the application with any IDE that supports .NET or by running the `dotnet run` CLI command in the directory of your project. The default username is `admin` and the password is `1q2w3E*`. + +> While creating a database & applying migrations seem only necessary for relational databases, this project comes even if you choose a NoSQL database provider (like MongoDB). In that case, it still seeds the initial data which is necessary for the application. + +### Angular UI + +If you choose `Angular` as the UI framework, the solution will be separated into two folders: + +* An `angular` folder that contains the Angular UI application, the client-side code. +* An `aspnet-core` folder that contains the ASP.NET Core solution (a single project), the server-side code. + +The server-side is similar to the solution described in the [Solution Structure](#solution-structure) section above. This project serves the API, so the `Angular` application can consume it. + +The client-side application consumes the HTTP APIs as mentioned. You can see the folder structure of the Angular project shown below: + +![](../images/single-layer-angular-folder-structure.png) diff --git a/docs/en/Tutorials/Todo/Single-Layer/Index.md b/docs/en/Tutorials/Todo/Single-Layer/Index.md new file mode 100644 index 0000000000..9f4927f5e4 --- /dev/null +++ b/docs/en/Tutorials/Todo/Single-Layer/Index.md @@ -0,0 +1,760 @@ +# Quick Start + +````json +//[doc-params] +{ + "UI": ["MVC", "BlazorServer", "NG"], + "DB": ["EF", "Mongo"] +} +```` + +This is a single-part quick-start tutorial to build a simple todo application with the ABP Framework. Here's a screenshot from the final application: + +![todo-list](../todo-list.png) + +You can find the source code of the completed application [here](https://github.com/abpframework/abp-samples/tree/master/TodoApp-SingleLayer). + +## Pre-Requirements + +* An IDE (e.g. [Visual Studio](https://visualstudio.microsoft.com/vs/)) that supports [.NET 6.0+](https://dotnet.microsoft.com/download/dotnet) development. + +{{if DB=="Mongo"}} + +* [MongoDB Server 4.0+](https://docs.mongodb.com/manual/administration/install-community/) + +{{end}} + +{{if UI=="NG"}} + +* [Node v14.x](https://nodejs.org/) + +{{end}} + +## Creating a New Solution + +In this tutorial, we will use the [ABP CLI](../../CLI.md) to create the sample application with the ABP Framework. You can run the following command in a command-line terminal to install the **ABP CLI**, if you haven't installed it yet: + +````bash +dotnet tool install -g Volo.Abp.Cli +```` + +Then create an empty folder, open a command-line terminal and execute the following command in the terminal: + +````bash +abp new TodoApp -t app-nolayers{{if UI=="BlazorServer"}} -u blazor-server{{else if UI=="NG"}} -u angular{{end}}{{if DB=="Mongo"}} -d mongodb{{end}} +```` + +{{if UI=="NG"}} + +This will create a new solution with a single project, named *TodoApp* with `angular` and `aspnet-core` folders. Once the solution is ready, open the solution in your favorite IDE. + +{{else}} + +This will create a new solution with a single project, named *TodoApp*. Once the solution is ready, open it in your favorite IDE. + +{{end}} + +### Create the Database + +You can run the following command in the directory of your project to create the database and seed the initial data: + +```bash +dotnet run --migrate-database +``` + +This command will create the database and seed the initial data for you. Then you can run the application. + +### Run the Application + +{{if UI=="MVC" || UI=="BlazorServer"}} + +It is good to run the application before starting the development. Running the application is pretty straight-forward, you can run the application with any IDE that supports .NET or by running the `dotnet run` CLI command in the directory of your project: + +{{else if UI=="NG"}} + +It is good to run the application before starting the development. The solution has two main applications: + +* `TodoApp` (in the .NET solution) hosts the server-side HTTP API, so the Angular application can consume it. (server-side application) +* `angular` folder contains the Angular application. (client-side application) + +Firstly, run the `TodoApp` project in your favorite IDE (or run the `dotnet run` CLI command on your project directory) to see the server-side HTTP API on [Swagger UI](https://swagger.io/tools/swagger-ui/): + +![todo-swagger-ui-initial](../todo-swagger-ui-initial.png) + +You can explore and test your HTTP API with this UI. If it works, then we can run the Angular client application. + +You can run the application using the following (or `yarn start`) command: + +````bash +npm start +```` + +This command takes time, but eventually runs and opens the application in your default browser: + +{{end}} + +![todo-ui-initial](../todo-ui-initial.png) + +You can click on the *Login* button and use `admin` as the username and `1q2w3E*` as the password to login to the application. + +All right. We can start coding! + +## Defining Entities + +This application will have a single [entity](../../../Entities.md) and we can start by creating it. So, create a new `TodoItem` class under the **Entities** folder of the project: + +````csharp +using Volo.Abp.Domain.Entities; + +namespace TodoApp.Entities; + +public class TodoItem : BasicAggregateRoot +{ + public string Text { get; set; } +} +```` + +`BasicAggregateRoot` is the simplest base class to create root entities, and `Guid` is the primary key (`Id`) of the entity here. + +## Database Integration + +{{if DB=="EF"}} + +Next step is to setup the [Entity Framework Core](../../../Entity-Framework-Core.md) configuration. + +### Mapping Configuration + +Open the `TodoAppDbContext` class (under the **Data** folder) and add a new `DbSet` property to this class: + +````csharp +public DbSet TodoItems { get; set; } +```` + +Then navigate to the `OnModelCreating` method in the same class and add the following mapping code for the `TodoItem ` entity: + +````csharp +protected override void OnModelCreating(ModelBuilder builder) +{ + base.OnModelCreating(builder); + + /* Include modules to your migration db context */ + + builder.ConfigurePermissionManagement(); + ... + + /* Configure your own tables/entities inside here */ + builder.Entity(b => + { + b.ToTable("TodoItems"); + }); +} +```` + +We've mapped the `TodoItem` entity to the `TodoItems` table in the database. The next step is to create a migration and apply the changes to the database. + +### Code First Migrations + +The startup solution is configured to use Entity Framework Core [Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations). Since we've changed the database mapping configuration, we should create a new migration and apply changes to the database. + +Open a command-line terminal in the directory of your project and type the following command: + +````bash +dotnet ef migrations add Added_TodoItem +```` + +This will add a new migration class to the project. You should see the new migration in the **Migrations** folder. + +![todo-efcore-migration](todo-efcore-migration-single-layer.png) + +Then, you can apply changes to the database using the following command, in the same command-line terminal: + +````bash +dotnet ef database update +```` + +{{else if DB=="Mongo"}} + +The next step is to setup the [MongoDB](../../../MongoDB.md) configuration. Open the `TodoAppDbContext` class (under the **Data** folder) in your project and make the following changes: + +1. Add a new property to the class: + +````csharp +public IMongoCollection TodoItems => Collection(); +```` + +2. Add the following code inside the `CreateModel` method: + +````csharp +modelBuilder.Entity(b => +{ + b.CollectionName = "TodoItems"; +}); +```` + +{{end}} + +After the database integrations, now we can start to create application service methods and implement our use-cases. + +## Creating the Application Service + +An [Application Service](../../Application-Services.md) is used to perform the use cases of the application. We need to perform the following use cases in this application: + +* Get the list of the todo items +* Create a new todo item +* Delete an existing todo item + +Before starting to implement these use cases, first we need to create DTOs. + +### Creating the Data Transfer Object (DTO) + +`ApplicationService` typically gets and returns DTOs ([Data Transfer Objects](../../../Data-Transfer-Objects.md)) instead of entities. So, create a new `TodoItemDto` class under the **Dtos** folder (under the **Services** folder): + +```csharp +namespace TodoApp.Services.Dtos; + +public class TodoItemDto +{ + public Guid Id { get; set; } + public string Text { get; set; } +} +``` + +* This is a very simple DTO class that has the same properties as the `TodoItem` entity. Now, we are ready to implement our use-cases. + +### Application Service Implementation + +Create a `TodoAppService` class under the **Services** folder of your project, as shown below: + +```csharp +using TodoApp.Entities; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; + +namespace TodoApp.Services; + +public class TodoAppService : ApplicationService +{ + private readonly IRepository _todoItemRepository; + + public TodoAppService(IRepository todoItemRepository) + { + _todoItemRepository = todoItemRepository; + } + + // TODO: Implement the methods here... +} +``` + +This class inherits from the `ApplicationService` class of the ABP Framework and implements our use-cases. ABP provides default generic [repositories](../../../Repositories.md) for the entities. We can use them to perform the fundamental database operations. This class [injects](../../../Dependency-Injection.md) `IRepository`, which is the default repository for the `TodoItem` entity. We will use it to implement the use cases (`GetListAsync`, `CreateAsync` and `DeleteAsync`) described before. + +#### Getting the Todo Items + +Let's start by implementing the `GetListAsync` method: + +````csharp +public async Task> GetListAsync() +{ + var items = await _todoItemRepository.GetListAsync(); + return items + .Select(item => new TodoItemDto + { + Id = item.Id, + Text = item.Text + }).ToList(); +} +```` + +We are simply getting the `TodoItem` list from the database, mapping them to the `TodoItemDto` objects and returning as the result. + +#### Creating a New Todo Item + +The next method is `CreateAsync` and we can implement it as shown below: + +````csharp +public async Task CreateAsync(string text) +{ + var todoItem = await _todoItemRepository.InsertAsync( + new TodoItem {Text = text} + ); + + return new TodoItemDto + { + Id = todoItem.Id, + Text = todoItem.Text + }; +} +```` + +The repository's `InsertAsync` method inserts the given `TodoItem` to the database and returns the same `TodoItem` object. It also sets the `Id`, so we can use it on the returning object. We are simply returning a `TodoItemDto` by creating from the new `TodoItem` entity. + +#### Deleting a Todo Item + +Finally, we can implement the `DeleteAsync` as the following code block: + +````csharp +public async Task DeleteAsync(Guid id) +{ + await _todoItemRepository.DeleteAsync(id); +} +```` + +The application service is ready to be used from the UI layer. So, let's implement it. + +## User Interface + +It is time to show the todo items on the UI! Before starting to write the code, it would be good to remember what we are trying to build. Here's a sample screenshot from the final UI: + +![todo-list](../todo-list.png) + +{{if UI=="MVC"}} + +### Index.cshtml.cs + +Open the `Index.cshtml.cs` file in the `Pages` folder and replace the content with the following code block: + +```csharp +using TodoApp.Services; +using TodoApp.Services.Dtos; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace TodoApp.Pages; + +public class IndexModel : AbpPageModel +{ + public List TodoItems { get; set; } + + private readonly TodoAppService _todoAppService; + + public IndexModel(TodoAppService todoAppService) + { + _todoAppService = todoAppService; + } + + public async Task OnGetAsync() + { + TodoItems = await _todoAppService.GetListAsync(); + } +} +``` + +This class uses `TodoAppService` to get the list of todo items and assign the `TodoItems` property. We will use it to render the todo items on the razor page. + +### Index.cshtml + +Open the `Index.cshtml` file in the `Pages` folder and replace it with the following content: + +```xml +@page +@model TodoApp.Pages.IndexModel + +@section styles { + +} +@section scripts { + +} + +
+ + + + TODO LIST + + + + +
+
+
+ +
+
+
+ +
+
+ +
    + @foreach (var todoItem in Model.TodoItems) + { +
  • + @todoItem.Text +
  • + } +
+
+
+
+``` + +We are using ABP's [card tag helper](../../UI/AspNetCore/Tag-Helpers/Cards.md) to create a simple card view. You could directly use the standard bootstrap HTML structure, however the ABP [tag helpers](../../UI/AspNetCore/Tag-Helpers/Index.md) make it much easier and type safe. + +This page imports a CSS and a JavaScript file, so we should also create them. + +### Index.cshtml.js + +Open the `Index.cshtml.js` file in the `Pages` folder and replace with the following content: + +````js +$(function () { + + // DELETING ITEMS ///////////////////////////////////////// + $('#TodoList').on('click', 'li i', function(){ + var $li = $(this).parent(); + var id = $li.attr('data-id'); + + todoApp.services.todo.delete(id).then(function(){ + $li.remove(); + abp.notify.info('Deleted the todo item.'); + }); + }); + + // CREATING NEW ITEMS ///////////////////////////////////// + $('#NewItemForm').submit(function(e){ + e.preventDefault(); + + var todoText = $('#NewItemText').val(); + todoApp.services.todo.create(todoText).then(function(result){ + $('
  • ') + .html(' ' + result.text) + .appendTo($('#TodoList')); + $('#NewItemText').val(''); + }); + }); +}); +```` + +In the first part, we subscribed to the click events of the trash icons near the todo items, deleted the related item on the server and showed a notification on the UI. Also, we removed the deleted item from the DOM, so we wouldn't need to refresh the page. + +In the second part, we created a new todo item on the server. If it succeeded, we would then manipulate the DOM to insert a new `
  • ` element to the todo list. This way, we wouldn't need to refresh the whole page after creating a new todo item. + +The interesting part here is how we communicate with the server. See the [*Dynamic JavaScript Proxies & Auto API Controllers*](#dynamic-javascript-proxies--auto-api-controllers) section to understand how it works. But now, let's continue and complete the application. + +### Index.cshtml.css + +As for the final touch, open the `Index.cshtml.css` file in the `Pages` folder and replace with the following content: + +````css +#TodoList{ + list-style: none; + margin: 0; + padding: 0; +} + +#TodoList li { + padding: 5px; + margin: 5px 0px; + border: 1px solid #cccccc; + background-color: #f5f5f5; +} + +#TodoList li i +{ + opacity: 0.5; +} + +#TodoList li i:hover +{ + opacity: 1; + color: #ff0000; + cursor: pointer; +} +```` + +This is a simple styling for the todo page. We believe that you can do much better :) + +Now, you can run the application again and see the result. + +### Dynamic JavaScript Proxies & Auto API Controllers + +In the `Index.cshtml.js` file, we've used the `todoApp.services.todo.delete(...)` and `todoApp.services.todo.create(...)` functions to communicate with the server. These functions are dynamically created by the ABP Framework, thanks to the [Dynamic JavaScript Client Proxy](../../../UI/AspNetCore/Dynamic-JavaScript-Proxies.md) system. They perform HTTP API calls to the server and return a promise, so you can register a callback to the `then` function as we've done above. + +> **services** keyword comes from the namespace (`namespace TodoApp.Services;`). It's a naming convention. + +However, you may notice that we haven't created any API Controllers, so how does the server handle these requests? This question brings us to the [Auto API Controller](../../../API/Auto-API-Controllers.md) feature of the ABP Framework. It automatically converts the application services to **API Controllers** by convention. + +If you open [Swagger UI](https://swagger.io/tools/swagger-ui/) by entering the `/swagger` URL in your application, you can see the Todo API: + +![todo-api](../todo-api.png) + +{{else if UI=="BlazorServer"}} + +### Index.razor.cs + +Open the `Index.razor.cs` file in the `Pages` folder and replace the content with the following code block: + +```csharp +using Microsoft.AspNetCore.Components; +using TodoApp.Services; +using TodoApp.Services.Dtos; + +namespace TodoApp.Pages; + +public partial class Index +{ + [Inject] + private TodoAppService TodoAppService { get; set; } + + private List TodoItems { get; set; } = new List(); + private string NewTodoText { get; set; } + + protected override async Task OnInitializedAsync() + { + TodoItems = await TodoAppService.GetListAsync(); + } + + private async Task Create() + { + var result = await TodoAppService.CreateAsync(NewTodoText); + TodoItems.Add(result); + NewTodoText = null; + } + + private async Task Delete(TodoItemDto todoItem) + { + await TodoAppService.DeleteAsync(todoItem.Id); + await Notify.Info("Deleted the todo item."); + TodoItems.Remove(todoItem); + } +} +``` + +This class uses the `TodoAppService` to get the list of todo items. It manipulates the `TodoItems` list after create and delete operations. This way, we don't need to refresh the whole todo list from the server. + +### Index.razor + +Open the `Index.razor` file in the `Pages` folder and replace the content with the following code block: + +```xml +@page "/" +@inherits TodoAppComponentBase + +
    + + + + TODO LIST + + + + +
    +
    +
    + +
    +
    +
    + +
    +
    + +
      + @foreach (var todoItem in TodoItems) + { +
    • + + @todoItem.Text +
    • + } +
    +
    +
    +
    +``` + +### Index.razor.css + +As the final touch, open the `Index.razor.css` file in the `Pages` folder and replace it with the following content: + +````css +#TodoList{ + list-style: none; + margin: 0; + padding: 0; +} + +#TodoList li { + padding: 5px; + margin: 5px 0px; + border: 1px solid #cccccc; + background-color: #f5f5f5; +} + +#TodoList li i +{ + opacity: 0.5; +} + +#TodoList li i:hover +{ + opacity: 1; + color: #ff0000; + cursor: pointer; +} +```` + +This is a simple styling for the todo page. We believe that you can do much better :) + +Now, you can run the application again to see the result. + +{{else if UI=="NG"}} + +### Service Proxy Generation + +ABP provides a handy feature to automatically create client-side services to easily consume HTTP APIs provided by the server. + +You first need to run the `TodoApp` project since the proxy generator reads API definitions from the server application. + +Once you run the `TodoApp` project (**Swagger API Definition** will be shown), open a command-line terminal in the directory of `angular` folder and run the following command: + +```bash +abp generate-proxy -t ng +``` + +If everything goes well, it should generate an output as shown below: + +```bash +CREATE src/app/proxy/generate-proxy.json (182755 bytes) +CREATE src/app/proxy/README.md (1000 bytes) +CREATE src/app/proxy/services/todo.service.ts (833 bytes) +CREATE src/app/proxy/services/dtos/models.ts (71 bytes) +CREATE src/app/proxy/services/dtos/index.ts (26 bytes) +CREATE src/app/proxy/services/index.ts (81 bytes) +CREATE src/app/proxy/index.ts (61 bytes) +``` + +Then, we can use the `TodoService` to use the server-side HTTP APIs, as we'll do in the next section. + +### home.component.ts + +Open the `/angular/src/app/home/home.component.ts` file and replace its content with the following code block: + +```ts +import { ToasterService } from "@abp/ng.theme.shared"; +import { Component, OnInit } from '@angular/core'; +import { TodoItemDto } from "@proxy/services/dtos"; +import { TodoService } from "@proxy/services"; + +@Component({ + selector: 'app-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.scss'], +}) + +export class HomeComponent implements OnInit { + + todoItems: TodoItemDto[]; + newTodoText: string; + + constructor( + private todoService: TodoService, + private toasterService: ToasterService) + { } + + ngOnInit(): void { + this.todoService.getList().subscribe(response => { + this.todoItems = response; + }); + } + + create(): void{ + this.todoService.create(this.newTodoText).subscribe((result) => { + this.todoItems = this.todoItems.concat(result); + this.newTodoText = null; + }); + } + + delete(id: string): void { + this.todoService.delete(id).subscribe(() => { + this.todoItems = this.todoItems.filter(item => item.id !== id); + this.toasterService.info('Deleted the todo item.'); + }); + } +} +``` + +We've used `TodoService` to get the list of todo items and assigned the returning value to the `todoItems` array. We've also added `create` and `delete` methods. These methods will be used on the view side. + +### home.component.html + +Open the `/angular/src/app/home/home.component.html` file and replace its content with the following code block: + +````html +
    +
    +
    +
    TODO LIST
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    + +
      +
    • + {%{{{ todoItem.text }}}%} +
    • +
    +
    +
    +
    +```` + +### home.component.scss + +As the final touch, open the `/angular/src/app/home/home.component.scss` file and replace its content with the following code block: + +````css +#TodoList{ + list-style: none; + margin: 0; + padding: 0; +} + +#TodoList li { + padding: 5px; + margin: 5px 0px; + border: 1px solid #cccccc; + background-color: #f5f5f5; +} + +#TodoList li i +{ + opacity: 0.5; +} + +#TodoList li i:hover +{ + opacity: 1; + color: #ff0000; + cursor: pointer; +} +```` + +This is a simple styling for the todo page. We believe that you can do much better :) + +Now, you can run the application again to see the result. + +{{end}} + +## Conclusion + +In this tutorial, we've built a very simple application to warm up with the ABP Framework. + +## Source Code + +You can find the source code of the completed application [here](https://github.com/abpframework/abp-samples/tree/master/TodoApp-SingleLayer). + +## See Also + +* Check the [Web Application Development Tutorial](../Part-1.md) to see a real-life web application development in a layered architecture using the [Application Startup Template](../../../Startup-Templates/Application.md). diff --git a/docs/en/Tutorials/Todo/Single-Layer/todo-efcore-migration-single-layer.png b/docs/en/Tutorials/Todo/Single-Layer/todo-efcore-migration-single-layer.png new file mode 100644 index 0000000000..686d8a5f87 Binary files /dev/null and b/docs/en/Tutorials/Todo/Single-Layer/todo-efcore-migration-single-layer.png differ diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 186eef1e07..610f0f60a4 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -118,6 +118,10 @@ "text": "Application", "path": "Startup-Templates/Application.md" }, + { + "text": "Application (Single Layer)", + "path": "Startup-Templates/Application-Single-Layer.md" + }, { "text": "Module", "path": "Startup-Templates/Module.md" diff --git a/docs/en/images/bookstore-single-layer-solution-structure.png b/docs/en/images/bookstore-single-layer-solution-structure.png new file mode 100644 index 0000000000..edc495d909 Binary files /dev/null and b/docs/en/images/bookstore-single-layer-solution-structure.png differ diff --git a/docs/en/images/single-layer-angular-folder-structure.png b/docs/en/images/single-layer-angular-folder-structure.png new file mode 100644 index 0000000000..91fe09d61e Binary files /dev/null and b/docs/en/images/single-layer-angular-folder-structure.png differ diff --git a/docs/en/images/single-layer-folder-structure.png b/docs/en/images/single-layer-folder-structure.png new file mode 100644 index 0000000000..06aa77049c Binary files /dev/null and b/docs/en/images/single-layer-folder-structure.png differ diff --git a/framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacRegistration.cs b/framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacRegistration.cs index 9b6c261392..241c3dd7a8 100644 --- a/framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacRegistration.cs +++ b/framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacRegistration.cs @@ -94,13 +94,22 @@ public static class AutofacRegistration throw new ArgumentNullException(nameof(services)); } - builder.RegisterType().As().ExternallyOwned(); + builder.RegisterType() + .As() + .As() + .ExternallyOwned(); + var autofacServiceScopeFactory = typeof(AutofacServiceProvider).Assembly.GetType("Autofac.Extensions.DependencyInjection.AutofacServiceScopeFactory"); if (autofacServiceScopeFactory == null) { throw new AbpException("Unable get type of Autofac.Extensions.DependencyInjection.AutofacServiceScopeFactory!"); } - builder.RegisterType(autofacServiceScopeFactory).As(); + + // Issue #83: IServiceScopeFactory must be a singleton and scopes must be flat, not hierarchical. + builder + .RegisterType(autofacServiceScopeFactory) + .As() + .SingleInstance(); Register(builder, services, lifetimeScopeTagForSingletons); } diff --git a/framework/src/Volo.Abp.Autofac/Volo.Abp.Autofac.csproj b/framework/src/Volo.Abp.Autofac/Volo.Abp.Autofac.csproj index 7ce82c1c9e..d8d2b83e5a 100644 --- a/framework/src/Volo.Abp.Autofac/Volo.Abp.Autofac.csproj +++ b/framework/src/Volo.Abp.Autofac/Volo.Abp.Autofac.csproj @@ -15,8 +15,8 @@ - - + + diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SuiteCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SuiteCommand.cs index aabd5c6dfd..6ee6a74a47 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SuiteCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SuiteCommand.cs @@ -132,6 +132,11 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency if (!response.IsNullOrWhiteSpace()) { Logger.LogError(response); + + if (response.Contains("Commercial.SuiteTemplates.dll")) + { + Logger.LogInformation("The solution should be built before generating an entity! Use `dotnet build` to build your solution."); + } } else { diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/InstallLibsService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/InstallLibsService.cs index 56fa776904..71cf789c9c 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/InstallLibsService.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/LIbs/InstallLibsService.cs @@ -236,13 +236,13 @@ public class InstallLibsService : IInstallLibsService, ITransientDependency private void RunNpmInstall(string directory) { Logger.LogInformation($"Running npm install on {directory}"); - CmdHelper.RunCmd($"cd {directory} && npm install"); + CmdHelper.RunCmd($"npm install", directory); } private void RunYarn(string directory) { Logger.LogInformation($"Running Yarn on {directory}"); - CmdHelper.RunCmd($"cd {directory} && yarn"); + CmdHelper.RunCmd($"yarn", directory); } private bool IsNpmInstalled() diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs index 64517b1900..135c04feaf 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs @@ -314,13 +314,13 @@ public class NpmPackagesUpdater : ITransientDependency protected virtual void RunYarn(string fileDirectory) { Logger.LogInformation($"Running Yarn on {fileDirectory}"); - CmdHelper.RunCmd($"cd {fileDirectory} && yarn"); + CmdHelper.RunCmd($"yarn", fileDirectory); } protected virtual void RunNpmInstall(string fileDirectory) { Logger.LogInformation($"Running npm install on {fileDirectory}"); - CmdHelper.RunCmd($"cd {fileDirectory} && npm install"); + CmdHelper.RunCmd($"npm install", fileDirectory); } protected virtual List GetPackageVersionList(JProperty package) diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Blogs/BlogPostGetListInput.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Blogs/BlogPostGetListInput.cs index f136fe6562..ddb8ebcbcb 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Blogs/BlogPostGetListInput.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Application.Contracts/Volo/CmsKit/Admin/Blogs/BlogPostGetListInput.cs @@ -11,6 +11,8 @@ public class BlogPostGetListInput : PagedAndSortedResultRequestDto public Guid? BlogId { get; set; } public Guid? AuthorId { get; set; } + + public Guid? TagId { get; set; } public BlogPostStatus? Status { get; set; } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Blogs/BlogPostAdminAppService.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Blogs/BlogPostAdminAppService.cs index f09a2e4565..9d8fcfd917 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Blogs/BlogPostAdminAppService.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Blogs/BlogPostAdminAppService.cs @@ -101,11 +101,11 @@ public class BlogPostAdminAppService : CmsKitAppServiceBase, IBlogPostAdminAppSe { var blogs = (await BlogRepository.GetListAsync()).ToDictionary(x => x.Id); - var blogPosts = await BlogPostRepository.GetListAsync(input.Filter, input.BlogId, input.AuthorId, + var blogPosts = await BlogPostRepository.GetListAsync(input.Filter, input.BlogId, input.AuthorId, input.TagId, statusFilter: input.Status, input.MaxResultCount, input.SkipCount, input.Sorting); - var count = await BlogPostRepository.GetCountAsync(input.Filter, input.BlogId, input.AuthorId); + var count = await BlogPostRepository.GetCountAsync(input.Filter, input.BlogId, input.AuthorId, tagId: input.TagId); var dtoList = blogPosts.Select(x => { diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/IBlogPostRepository.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/IBlogPostRepository.cs index 2af1036162..404d21dc83 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/IBlogPostRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/IBlogPostRepository.cs @@ -13,6 +13,7 @@ public interface IBlogPostRepository : IBasicRepository string filter = null, Guid? blogId = null, Guid? authorId = null, + Guid? tagId = null, BlogPostStatus? statusFilter = null, CancellationToken cancellationToken = default); @@ -20,6 +21,7 @@ public interface IBlogPostRepository : IBasicRepository string filter = null, Guid? blogId = null, Guid? authorId = null, + Guid? tagId = null, BlogPostStatus? statusFilter = null, int maxResultCount = int.MaxValue, int skipCount = 0, diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Tags/EntityTagManager.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Tags/EntityTagManager.cs index 4000cc8aec..b33d337a06 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Tags/EntityTagManager.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Tags/EntityTagManager.cs @@ -71,4 +71,12 @@ public class EntityTagManager : DomainService await AddTagToEntityAsync(tag.Id, entityType, entityId, CurrentTenant?.Id); } } -} + + public async Task> GetEntityIdsFilteredByTagAsync( + [NotNull] Guid tagId, + [CanBeNull] Guid? tenantId, + CancellationToken cancellationToken = default) + { + return await EntityTagRepository.GetEntityIdsFilteredByTagAsync(tagId, tenantId, cancellationToken); + } +} \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Tags/IEntityTagRepository.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Tags/IEntityTagRepository.cs index 15efdcd433..a96402267d 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Tags/IEntityTagRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Tags/IEntityTagRepository.cs @@ -1,5 +1,6 @@ using JetBrains.Annotations; using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Volo.Abp.Domain.Repositories; @@ -15,4 +16,9 @@ public interface IEntityTagRepository : IBasicRepository CancellationToken cancellationToken = default); Task DeleteManyAsync(Guid[] tagIds, CancellationToken cancellationToken = default); + + Task> GetEntityIdsFilteredByTagAsync( + [NotNull] Guid tagId, + [CanBeNull] Guid? tenantId, + CancellationToken cancellationToken = default); } diff --git a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Blogs/EfCoreBlogPostRepository.cs b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Blogs/EfCoreBlogPostRepository.cs index 0c359debcc..dfe2c6fa19 100644 --- a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Blogs/EfCoreBlogPostRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Blogs/EfCoreBlogPostRepository.cs @@ -11,14 +11,20 @@ using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; using Volo.CmsKit.EntityFrameworkCore; +using Volo.CmsKit.Tags; using Volo.CmsKit.Users; namespace Volo.CmsKit.Blogs; public class EfCoreBlogPostRepository : EfCoreRepository, IBlogPostRepository { - public EfCoreBlogPostRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) + private EntityTagManager _entityTagManager; + + public EfCoreBlogPostRepository( + IDbContextProvider dbContextProvider, + EntityTagManager entityTagManager) : base(dbContextProvider) { + _entityTagManager = entityTagManager; } public async Task GetBySlugAsync( @@ -43,10 +49,18 @@ public class EfCoreBlogPostRepository : EfCoreRepository entityIdFilters = null; + if (tagId.HasValue) + { + entityIdFilters = await _entityTagManager.GetEntityIdsFilteredByTagAsync(tagId.Value, CurrentTenant.Id, cancellationToken); + } + var queryable = (await GetDbSetAsync()) + .WhereIf(entityIdFilters != null, x => entityIdFilters.Contains(x.Id.ToString())) .WhereIf(blogId.HasValue, x => x.BlogId == blogId) .WhereIf(authorId.HasValue, x => x.AuthorId == authorId) .WhereIf(statusFilter.HasValue, x => x.Status == statusFilter) @@ -60,6 +74,7 @@ public class EfCoreBlogPostRepository : EfCoreRepository(); var usersDbSet = dbContext.Set(); - var queryable = blogPostsDbSet + List entityIdFilters = null; + if (tagId.HasValue) + { + entityIdFilters = await _entityTagManager.GetEntityIdsFilteredByTagAsync(tagId.Value, CurrentTenant.Id, cancellationToken); + } + + var queryable = (await GetDbSetAsync()) + .WhereIf(entityIdFilters != null, x => entityIdFilters.Contains(x.Id.ToString())) .WhereIf(blogId.HasValue, x => x.BlogId == blogId) .WhereIf(!string.IsNullOrWhiteSpace(filter), x => x.Title.Contains(filter) || x.Slug.Contains(filter)) .WhereIf(authorId.HasValue, x => x.AuthorId == authorId) @@ -138,4 +160,4 @@ public class EfCoreBlogPostRepository : EfCoreRepository x.Status == BlogPostStatus.WaitingForReview, GetCancellationToken(cancellationToken)); } -} +} \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Tags/EfCoreEntityTagRepository.cs b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Tags/EfCoreEntityTagRepository.cs index 16d648ca61..fe7e622841 100644 --- a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Tags/EfCoreEntityTagRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Tags/EfCoreEntityTagRepository.cs @@ -1,8 +1,10 @@ using JetBrains.Annotations; using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; using Volo.Abp; using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; @@ -39,4 +41,15 @@ public class EfCoreEntityTagRepository : EfCoreRepository> GetEntityIdsFilteredByTagAsync( + [NotNull] Guid tagId, + [CanBeNull] Guid? tenantId, + CancellationToken cancellationToken = default) + { + return await (await GetDbContextAsync()).Set() + .Where(q => q.TagId == tagId && q.TenantId == tenantId) + .Select(q => q.EntityId) + .ToListAsync(cancellationToken: GetCancellationToken(cancellationToken)); + } +} \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Blogs/MongoBlogPostRepository.cs b/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Blogs/MongoBlogPostRepository.cs index 129c5e10b7..9358635f5c 100644 --- a/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Blogs/MongoBlogPostRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Blogs/MongoBlogPostRepository.cs @@ -12,15 +12,18 @@ using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Repositories.MongoDB; using Volo.Abp.MongoDB; using Volo.CmsKit.Blogs; +using Volo.CmsKit.Tags; using Volo.CmsKit.Users; namespace Volo.CmsKit.MongoDB.Blogs; public class MongoBlogPostRepository : MongoDbRepository, IBlogPostRepository { - public MongoBlogPostRepository(IMongoDbContextProvider dbContextProvider) : base( + private EntityTagManager _entityTagManager; + public MongoBlogPostRepository(IMongoDbContextProvider dbContextProvider, EntityTagManager entityTagManager) : base( dbContextProvider) { + _entityTagManager = entityTagManager; } public virtual async Task GetBySlugAsync(Guid blogId, [NotNull] string slug, @@ -44,12 +47,20 @@ public class MongoBlogPostRepository : MongoDbRepository entityIdFilters = null; + if (tagId.HasValue) + { + entityIdFilters = await _entityTagManager.GetEntityIdsFilteredByTagAsync(tagId.Value, CurrentTenant.Id, cancellationToken); + } + return await (await GetMongoQueryableAsync(cancellationToken)) + .WhereIf>(entityIdFilters != null, x => entityIdFilters.Contains(x.Id.ToString())) .WhereIf>(!string.IsNullOrWhiteSpace(filter), x => x.Title.Contains(filter) || x.Slug.Contains(filter)) .WhereIf>(blogId.HasValue, x => x.BlogId == blogId) .WhereIf>(authorId.HasValue, x => x.AuthorId == authorId) @@ -61,6 +72,7 @@ public class MongoBlogPostRepository : MongoDbRepository entityIdFilters = null; + if (tagId.HasValue) + { + entityIdFilters = await _entityTagManager.GetEntityIdsFilteredByTagAsync(tagId.Value, CurrentTenant.Id, cancellationToken); + } + var usersQueryable = dbContext.Collection().AsQueryable(); var queryable = blogPostQueryable + .WhereIf(entityIdFilters != null, x => entityIdFilters.Contains(x.Id.ToString())) .WhereIf(blogId.HasValue, x => x.BlogId == blogId) .WhereIf(!string.IsNullOrWhiteSpace(filter), x => x.Title.Contains(filter) || x.Slug.Contains(filter)) .WhereIf(authorId.HasValue, x => x.AuthorId == authorId) diff --git a/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Tags/MongoEntityTagRepository.cs b/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Tags/MongoEntityTagRepository.cs index e4d6143996..82952c9bf5 100644 --- a/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Tags/MongoEntityTagRepository.cs +++ b/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo/CmsKit/MongoDB/Tags/MongoEntityTagRepository.cs @@ -2,6 +2,7 @@ using MongoDB.Driver; using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Volo.Abp; @@ -39,4 +40,16 @@ public class MongoEntityTagRepository : MongoDbRepository> GetEntityIdsFilteredByTagAsync( + [NotNull] Guid tagId, + [CanBeNull] Guid? tenantId, + CancellationToken cancellationToken = default) + { + var blogPostQueryable = (await GetQueryableAsync()) + .Where(q => q.TagId == tagId && q.TenantId == tenantId) + .Select(q => q.EntityId); + + return await AsyncExecuter.ToListAsync(blogPostQueryable, GetCancellationToken(cancellationToken)); + } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Blogs/BlogPostGetListInput.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Blogs/BlogPostGetListInput.cs index b471171f3b..5e3ffe9311 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Blogs/BlogPostGetListInput.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Blogs/BlogPostGetListInput.cs @@ -5,5 +5,7 @@ namespace Volo.CmsKit.Public.Blogs; public class BlogPostGetListInput : PagedAndSortedResultRequestDto { - public Guid? AuthorId { get; set; } + public Guid? AuthorId { get; set; } + + public Guid? TagId { get; set; } } \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Blogs/BlogPostPublicAppService.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Blogs/BlogPostPublicAppService.cs index 94847fba11..65e6c6fa20 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Blogs/BlogPostPublicAppService.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Blogs/BlogPostPublicAppService.cs @@ -38,11 +38,13 @@ public class BlogPostPublicAppService : CmsKitPublicAppServiceBase, IBlogPostPub { var blog = await BlogRepository.GetBySlugAsync(blogSlug); - var blogPosts = await BlogPostRepository.GetListAsync(null, blog.Id, input.AuthorId, BlogPostStatus.Published, input.MaxResultCount, + var blogPosts = await BlogPostRepository.GetListAsync(null, blog.Id, input.AuthorId, input.TagId, + BlogPostStatus.Published, input.MaxResultCount, input.SkipCount, input.Sorting); return new PagedResultDto( - await BlogPostRepository.GetCountAsync(blogId: blog.Id, statusFilter: BlogPostStatus.Published, authorId: input.AuthorId), + await BlogPostRepository.GetCountAsync(blogId: blog.Id, tagId: input.TagId, + statusFilter: BlogPostStatus.Published, authorId: input.AuthorId), ObjectMapper.Map, List>(blogPosts)); } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Tags/Default.cshtml b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Tags/Default.cshtml index f86ba0998e..85113c8688 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Tags/Default.cshtml +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Tags/Default.cshtml @@ -2,16 +2,29 @@ @model TagViewComponent.TagViewModel -
    -
    - @if (Model.Tags != null) +
    +
    + @if (Model.Tags != null) + { + foreach (var tag in Model.Tags) { - foreach (var tag in Model.Tags) + if (Model.UrlFormat.IsNullOrWhiteSpace()) { + @tag.Name } + else + { + var formattedUrl = Model.UrlFormat.Replace("{TagId}", tag.Id.ToString()); + + + @tag.Name + + + } } -
    -
    \ No newline at end of file + } +
    +
    \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Tags/TagViewComponent.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Tags/TagViewComponent.cs index 43fb6ea8cc..a3dc23c61a 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Tags/TagViewComponent.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Tags/TagViewComponent.cs @@ -26,7 +26,8 @@ public class TagViewComponent : AbpViewComponent public virtual async Task InvokeAsync( string entityType, - string entityId) + string entityId, + string urlFormat) { var tagDtos = await TagAppService.GetAllRelatedTagsAsync(entityType, entityId); @@ -34,7 +35,8 @@ public class TagViewComponent : AbpViewComponent { EntityId = entityId, EntityType = entityType, - Tags = tagDtos + Tags = tagDtos, + UrlFormat = urlFormat }; return View("~/Pages/CmsKit/Shared/Components/Tags/Default.cshtml", viewModel); @@ -45,5 +47,6 @@ public class TagViewComponent : AbpViewComponent public List Tags { get; set; } public string EntityId { get; set; } public string EntityType { get; set; } + public string UrlFormat { get; set; } } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/BlogPost.cshtml b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/BlogPost.cshtml index 62670a742d..9d8680e96c 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/BlogPost.cshtml +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/BlogPost.cshtml @@ -85,7 +85,8 @@ @await Component.InvokeAsync(typeof(TagViewComponent), new { entityType = Volo.CmsKit.Blogs.BlogPostConsts.EntityType, - entityId = Model.BlogPost.Id.ToString() + entityId = Model.BlogPost.Id.ToString(), + urlFormat = $"/blogs/{Model.BlogSlug}?tagId={{TagId}}" }) } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/Index.cshtml.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/Index.cshtml.cs index c7f667382b..f5f2cc2f7a 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/Index.cshtml.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/Index.cshtml.cs @@ -24,6 +24,9 @@ public class IndexModel : CmsKitPublicPageModelBase [BindProperty(SupportsGet = true)] public Guid? AuthorId { get; set; } + [BindProperty(SupportsGet = true)] + public Guid? TagId { get; set; } + public PagedResultDto Blogs { get; private set; } public PagerModel PagerModel => new PagerModel(Blogs.TotalCount, Blogs.Items.Count, CurrentPage, PageSize, Request.Path.ToString()); @@ -45,7 +48,8 @@ public class IndexModel : CmsKitPublicPageModelBase { SkipCount = PageSize * (CurrentPage - 1), MaxResultCount = PageSize, - AuthorId = AuthorId + AuthorId = AuthorId, + TagId = TagId }); if (AuthorId != null) diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor.cs index e81eed859d..b477de008d 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor.cs @@ -133,7 +133,7 @@ public partial class PermissionManagementModal if (!updateDto.Permissions.Any(x => x.IsGranted)) { - if (!await Message.Confirm(L["RemoveAllPermissionsWarningMessage"].Value)) + if (!await Message.Confirm(L["SaveWithoutAnyPermissionsWarningMessage"].Value)) { return; } diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/en.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/en.json index 98f3a60216..8fbe3e9eab 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/en.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/en.json @@ -6,6 +6,6 @@ "All": "All", "SelectAllInAllTabs": "Grant all permissions", "SelectAllInThisTab": "Select all", - "RemoveAllPermissionsWarningMessage": "Are you sure you want to remove all permissions?" + "SaveWithoutAnyPermissionsWarningMessage": "Are you sure you want to save without any permissions?" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/tr.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/tr.json index e462d91a51..dececfcce5 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/tr.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/tr.json @@ -5,6 +5,7 @@ "OnlyProviderPermissons": "Sadece bu sağlayıcı", "All": "Hepsi", "SelectAllInAllTabs": "Tüm izinleri ver", - "SelectAllInThisTab": "Hepsini seç" + "SelectAllInThisTab": "Hepsini seç", + "SaveWithoutAnyPermissionsWarningMessage": "Hiçbir izin olmadan kaydetmek istediğinize emin misiniz?" } -} \ No newline at end of file +} diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/permission-management-modal.js b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/permission-management-modal.js index 208047314c..9577d38696 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/permission-management-modal.js +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/permission-management-modal.js @@ -265,7 +265,7 @@ var abp = abp || {}; e.preventDefault(); if(!$form.find("input:checked").length > 0) { - abp.message.confirm(l("RemoveAllPermissionsWarningMessage")) + abp.message.confirm(l("SaveWithoutAnyPermissionsWarningMessage")) .then(function (confirmed) { if(confirmed) { $form.submit();