diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/Post.md b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/Post.md new file mode 100644 index 0000000000..b6c1d3f952 --- /dev/null +++ b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/Post.md @@ -0,0 +1,247 @@ +# ABP Framework v2.7.0 Has Been Released! + +The **ABP Framework** & and the **ABP Commercial** v2.7 have been released. We hadn't created blog post for the 2.4, 2.4 and 2.6 releases, so this post will also cover **what's new** with these releases and **what we've done** in the last 2 months. + +## About the Release Cycle & Development + +Reminding that we had started to release a new minor feature version **in every two weeks**, generally on Thursdays. Our goal is to deliver new features as soon as possible. + +We've completed & merged hundreds of issues and pull requests with **1,300+ commits** in the last 7-8 weeks, only for the ABP Framework repository. Daily commit counts are constantly increasing: + +![github-contribution-graph](github-contribution-graph.png) + +ABP.IO Platform is rapidly growing and we are getting more and more contributions from the community. + +## What's New in the ABP Framework? + +### Object Extending System + +In the last few releases, we've mostly focused on providing ways to extend existing modules when you use them as NuGet/NPM Packages. + +The Object Extending System allows module developers to create extensible modules and allows application developers to customize and extend a module easily. + +For example, you can add two extension properties to the user entity of the identity module: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdate(options => + { + options.AddOrUpdateProperty("SocialSecurityNumber"); + options.AddOrUpdateProperty("IsSuperUser"); + } + ); +```` + +It is easy to define validation rules for the properties: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "SocialSecurityNumber", + options => + { + options.Attributes.Add(new RequiredAttribute()); + options.Attributes.Add( + new StringLengthAttribute(32) { + MinimumLength = 6 + } + ); + }); +```` + +You can even write custom code to validate the property. It automatically works for the objects those are parameters of an application service, controller or a page. + +While extension properties of an entity are normally stored in a single JSON formatted field in the database table, you can easily configure to store a property as a table field using the EF Core mapping: + +````csharp +ObjectExtensionManager.Instance + .AddOrUpdateProperty( + "SocialSecurityNumber", + options => + { + options.MapEfCore(b => b.HasMaxLength(32)); + } + ); +```` + +See the [Object Extensions document](https://docs.abp.io/en/abp/latest/Object-Extensions) for details about this system. + +See also the [Customizing the Existing Modules](https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Guide) guide to learn all the possible customization options. + +### Text Templating Package + +[Volo.Abp.TextTemplating](https://www.nuget.org/packages/Volo.Abp.TextTemplating) is a new package introduced with the v2.7.0. Previously, [Volo.Abp.Emailing](https://www.nuget.org/packages/Volo.Abp.Emailing) package had a similar functionality but it was limited, experimental and tightly coupled to the emailing. + +The new text templating package allows you to define text based templates those can be easily localized and reused. You can define layout templates and share the layout from other templates. + +We are currently using it for email sending. A module needs to send an email typically defines a template. Example: + +````xml +

{{L "PasswordReset"}}

+ +

{{L "PasswordResetInfoInEmail"}}

+ + +```` + +This is a typical password reset email template. + +* The template system is based on the open source [Scriban library](https://github.com/lunet-io/scriban). So it supports if conditions, loops and much more. +* `model` is used to pass data to the template (just like the ASP.NET Core MVC). +* `L` is a special function that localizes the given string. + +It is typical to use the same layout for all emails. So, you can define a layout template. This is the standard layout template comes with the framework: + +````xml + + + + + + + {{content}} + + +```` + +A layout should have a `{{content}}` area to render the child content (just like the `RenderBody()` in the MVC). + +It is very easy to override a template content by the final application to customize it. + +Whenever you need to render a template, use the `ITemplateRenderer` service by providing the template name and a model. See the [text templating documentation](https://docs.abp.io/en/abp/latest/Text-Templating) for details. We've even created a UI for the ABP Commercial (see the related section below). + +### Subscribing to the Exceptions + +ABP Framework's [exception handling system](https://docs.abp.io/en/abp/latest/Exception-Handling) automatically handles exceptions and returns an appropriate result to the client. In some cases, you may want to have a callback that is notified whenever an exception occurs. In this way, for example, you can send an email or take any action based on the exception. + +Just create a class derived from the `ExceptionSubscriber` class in your application: + +````csharp +public class MyExceptionSubscriber : ExceptionSubscriber +{ + public override async Task HandleAsync(ExceptionNotificationContext context) + { + //TODO... + } +} +```` + +See the [exception handling](https://docs.abp.io/en/abp/latest/Exception-Handling) document for more. + +### Others + +There are many minor features and enhancements made to the framework in the past releases. Here, a few ones: + +* Added `AbpLocalizationOptions.DefaultResourceType` to set the default resource type for the application. In this way, the localization system uses the default resource whenever the resource was not specified. The latest application startup template already configures it, but you may want to set it for your existing applications. +* Added `IsEnabled` to permission definition. In this way, you can completely disable a permission and hide the related functionality from the application. This can be a way of feature switch for some applications. See [#3486](https://github.com/abpframework/abp/issues/3486) for usage. +* Added Dutch and German localizations to all the localization resources defined by the framework. Thanks to the contributors. + +## What's New in the ABP Commercial + +The goal of the [ABP Commercial](https://commercial.abp.io/) is to provide pre-build application functionalities, code generation tools, professional themes, advanced samples and premium support for ABP Framework based projects. + +We are working on the ABP Commercial in the parallel to align with the ABP Framework features and provide more modules, theme options and tooling. + +This section explains what's going on the ABP Commercial side. + +### Module Entity Extension System + +Module entity extension system is a higher level API that uses the object extension system (introduced above) and provides an easy way to add extension properties to existing entities. A new extension property easily automatically becomes a part of the HTTP API and the User Interface. + +Example: Add a `SocialSecurityNumber` to the user entity of the identity module + +````csharp +ObjectExtensionManager.Instance.Modules() + .ConfigureIdentity(identity => + { + identity.ConfigureUser(user => + { + user.AddOrUpdateProperty( //property type: string + "SocialSecurityNumber", //property name + property => + { + //validation rules + property.Attributes.Add(new RequiredAttribute()); + property.Attributes.Add( + new StringLengthAttribute(64) { + MinimumLength = 4 + } + ); + + //...other configurations for this property + } + ); + }); + }); +```` + +With just such a configuration, the user interface will have the new property (on the table and on the create/edit forms): + +![module-entity-extended-ui](module-entity-extended-ui.png) + +The new property can be easily localized and validated. Currently, it supports primitive types like string, number and boolean, but we planned to add more advanced scenarios by the time (like navigation/lookup properties). + +See the [Module Entity Extensions](https://docs.abp.io/en/commercial/latest/guides/module-entity-extensions) guide to learn how to use it and configure details. + +#### Other Extension Points + +There are also some other pre-defined points to customize and extend the user interface of a depended module: + +* You can add a new action for an entity on the data table (left side on the picture below). +* You can add new buttons (or other controls) to the page toolbar (right side on the picture below). +* You can add custom columns to a data table. + +![abp-commercial-ui-extensions](abp-commercial-ui-extensions.png) + +See the [Customizing the Modules](https://docs.abp.io/en/commercial/latest/guides/customizing-modules) guide to learn all the possible ways to customize a depended module. + +### Text Template Management Module + +We are introducing a new module with the v2.7 release: [Text Template Management](https://docs.abp.io/en/commercial/latest/modules/text-template-management). It is basically used to edit text/email templates (introduced with the ABP Framework 2.7) on the user interface and save changed in the database. + +A screenshot from the content editing for the password reset email template: + +![text-template-content-ui](text-template-content-ui.png) + +This module comes pre-installed when you create a new project. + +### Entity History Views + +Audit logging UI module now shows all the entity changes in the application with property change details. + +![audit-log-entity-changes](audit-log-entity-changes.png) + +You can also check history for an entity when you click to the actions menu for the entity: + +![tenant-entity-changes](tenant-entity-changes.png) + +### More Samples + +We are creating more advanced sample applications built with the ABP Commercial. Easy CRM is one of them which will be available in a few days to the commercial customers. + +Here, a screenshot from the Easy CRM dashboard: + +![easy-crm](easy-crm.png) + +It has accounts, contacts, product groups, products, orders and so on. + +### New Modules + +We continue to improve existing modules and creating new modules. In addition to the new [text template management](https://docs.abp.io/en/commercial/latest/modules/text-template-management) module introduced above; + +* We've recently released a [payment module](https://commercial.abp.io/modules/Volo.Payment) that currently works with PayU and 2Checkout payment gateways. More gateways will be added by the time. +* We've created a simple [Twilio SMS integration](https://docs.abp.io/en/commercial/latest/modules/twilio-sms) module to send SMS over the Twilio. +* We are working on a **chat module** that is currently being developed and will be available in the next weeks. +* We are working on the **organization unit management** system for the identity module to create hierarchical organization units (domain layer will be open source & free). + +More modules, theme and tooling options are being developed for the ABP Commercial and the ABP Framework. + +## ABP Framework vs ABP Commercial + +We ([Volosoft](https://volosoft.com/) - the core team behind the ABP.IO platform), are spending almost equal time on the ABP Framework and the ABP Commercial and we consider the ABP.IO platform as a whole. + +[ABP Framework](https://abp.io/) provides all the infrastructure and application independent framework features to make you more productive, focus on your own business code and implement software development best practices. It provides you a well defined and comfortable development experience without repeating yourself. + +[ABP Commercial](https://commercial.abp.io/) provides pre-built functionalities, themes and tooling to save your time if your requirements involve these functionalities in addition to the premium support for the framework and the pre-built modules. \ No newline at end of file diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/abp-commercial-ui-extensions.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/abp-commercial-ui-extensions.png new file mode 100644 index 0000000000..52fe37a712 Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/abp-commercial-ui-extensions.png differ diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/audit-log-entity-changes.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/audit-log-entity-changes.png new file mode 100644 index 0000000000..e06b3300ee Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/audit-log-entity-changes.png differ diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/easy-crm.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/easy-crm.png new file mode 100644 index 0000000000..e40f399525 Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/easy-crm.png differ diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/github-contribution-graph.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/github-contribution-graph.png new file mode 100644 index 0000000000..ff3db2ad22 Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/github-contribution-graph.png differ diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/module-entity-extended-ui.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/module-entity-extended-ui.png new file mode 100644 index 0000000000..25621aabb7 Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/module-entity-extended-ui.png differ diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/tenant-entity-changes.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/tenant-entity-changes.png new file mode 100644 index 0000000000..4cd573c588 Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/tenant-entity-changes.png differ diff --git a/docs/en/Blog-Posts/2020-05-08 v2_7_Release/text-template-content-ui.png b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/text-template-content-ui.png new file mode 100644 index 0000000000..037e3e4d8b Binary files /dev/null and b/docs/en/Blog-Posts/2020-05-08 v2_7_Release/text-template-content-ui.png differ diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md new file mode 100644 index 0000000000..efde511323 --- /dev/null +++ b/docs/en/Text-Templating.md @@ -0,0 +1,3 @@ +# Text-Templating + +TODO \ No newline at end of file diff --git a/docs/en/Tutorials/Part-1.md b/docs/en/Tutorials/Part-1.md index 51ab468d07..f7a98e485d 100644 --- a/docs/en/Tutorials/Part-1.md +++ b/docs/en/Tutorials/Part-1.md @@ -756,28 +756,28 @@ Open a new command line interface (terminal window) and go to your `angular` fol yarn ``` -#### BooksModule +#### BookModule -Run the following command line to create a new module, named `BooksModule`: +Run the following command line to create a new module, named `BookModule`: ```bash -yarn ng generate module books --route books --module app.module +yarn ng generate module book --routing true ``` -![Generating books module](./images/bookstore-creating-books-module-terminal.png) +![Generating books module](./images/bookstore-creating-book-module-terminal.png) #### Routing -Open the `app-routing.module.ts` file in `src\app` folder. Add the new `import` and replace `books` path as shown below +Open the `app-routing.module.ts` file in `src\app` folder. Add the new `import` and add a route as shown below ```js import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added this line to imports <== -//...replaced original books path with the below +//...added books path with the below to the routes array { path: 'books', component: ApplicationLayoutComponent, - loadChildren: () => import('./books/books.module').then(m => m.BooksModule), + loadChildren: () => import('./book/book.module').then(m => m.BookModule), data: { routes: { name: '::Menu:Books', @@ -789,71 +789,50 @@ import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added th * The `ApplicationLayoutComponent` configuration sets the application layout to the new page. We added the `data` object. The `name` is the menu item name and the `iconClass` is the icon of the menu item. -Run `yarn start` and wait for Angular to serve the application: - -```bash -yarn start -``` - -Open the browser and navigate to http://localhost:4200/books. You'll see a blank page saying "*books works!*". - -![initial-books-page](./images/bookstore-initial-books-page-with-layout.png) - #### Book list component -Replace the `books.component.html` in the `app\books` folder with the following content: - -```html - -``` - -Then run the command below on the terminal in the root folder to generate a new component, named book-list: +Run the command below on the terminal in the root folder to generate a new component, named book-list: ```bash -yarn ng generate component books/book-list +yarn ng generate component book/book-list ``` ![Creating books list](./images/bookstore-creating-book-list-terminal.png) -Open `books.module.ts` file in the `app\books` folder and replace the content as below: +Open `book.module.ts` file in the `app\book` folder and replace the content as below: ```js import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; - -import { BooksRoutingModule } from './books-routing.module'; -import { BooksComponent } from './books.component'; +import { BookRoutingModule } from './book-routing.module'; import { BookListComponent } from './book-list/book-list.component'; import { SharedModule } from '../shared/shared.module'; //<== added this line ==> @NgModule({ - declarations: [BooksComponent, BookListComponent], + declarations: [BookListComponent], imports: [ CommonModule, - BooksRoutingModule, + BookRoutingModule, SharedModule, //<== added this line ==> - ] + ], }) -export class BooksModule { } +export class BookModule {} ``` * We imported `SharedModule` and added to `imports` array. -Open `books-routing.module.ts` file in the `app\books` folder and replace the content as below: +Open `book-routing.module.ts` file in the `app\book` folder and replace the content as below: ```js import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { BookListComponent } from './book-list/book-list.component'; // <== added this line ==> -import { BooksComponent } from './books.component'; -import { BookListComponent } from './book-list/book-list.component'; //<== added this line ==> - -//<== replaced routes ==> +// <== replaced routes ==> const routes: Routes = [ { path: '', - component: BooksComponent, - children: [{ path: '', component: BookListComponent }], + component: BookListComponent, }, ]; @@ -861,36 +840,42 @@ const routes: Routes = [ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) -export class BooksRoutingModule { } +export class BookRoutingModule { } ``` * We imported `BookListComponent` and replaced `routes` const. -We'll see **book-list works!** text on the books page: +Run `yarn start` and wait for Angular to serve the application: + +```bash +yarn start +``` + +Open the browser and navigate to http://localhost:4200/books. We'll see **book-list works!** text on the books page: ![Initial book list page](./images/bookstore-initial-book-list-page.png) -#### Create BooksState +#### Create BookState Run the following command in the terminal to create a new state, named `BooksState`: ```bash -npx @ngxs/cli --name books --directory src/app/books +npx @ngxs/cli --name book --directory src/app/book ``` -* This command creates `books.state.ts` and `books.actions.ts` files in the `src/app/books/state` folder. See the [NGXS CLI documentation](https://www.ngxs.io/plugins/cli). +* This command creates `book.state.ts` and `book.actions.ts` files in the `src/app/book/state` folder. See the [NGXS CLI documentation](https://www.ngxs.io/plugins/cli). -Import the `BooksState` to the `app.module.ts` in the `src/app` folder and then add the `BooksState` to `forRoot` static method of `NgxsModule` as an array element of the first parameter of the method. +Import the `BookState` to the `app.module.ts` in the `src/app` folder and then add the `BookState` to `forRoot` static method of `NgxsModule` as an array element of the first parameter of the method. ```js // ... -import { BooksState } from './books/state/books.state'; //<== imported BooksState ==> +import { BookState } from './books/state/books.state'; //<== imported BookState ==> @NgModule({ imports: [ // other imports - NgxsModule.forRoot([BooksState]), //<== added BooksState ==> + NgxsModule.forRoot([BookState]), //<== added BookState ==> //other imports ], @@ -919,46 +904,46 @@ The generated files looks like below: Actions can either be thought of as a command which should trigger something to happen, or as the resulting event of something that has already happened. [See NGXS Actions documentation](https://www.ngxs.io/concepts/actions). -Open the `books.actions.ts` file in `app/books/state` folder and replace the content below: +Open the `book.actions.ts` file in `app/book/state` folder and replace the content below: ```js export class GetBooks { - static readonly type = '[Books] Get'; + static readonly type = '[Book] Get'; } ``` -#### Implement BooksState +#### Implement BookState -Open the `books.state.ts` file in `app/books/state` folder and replace the content below: +Open the `book.state.ts` file in `app/book/state` folder and replace the content below: ```js import { PagedResultDto } from '@abp/ng.core'; import { State, Action, StateContext, Selector } from '@ngxs/store'; -import { GetBooks } from './books.actions'; -import { BookService } from '../../app/shared/services'; +import { GetBooks } from './book.actions'; +import { BookService } from '../services'; import { tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; -import { BookDto } from '../../app/shared/models'; +import { BookDto } from '../models'; -export class BooksStateModel { +export class BookStateModel { public book: PagedResultDto; } -@State({ - name: 'BooksState', - defaults: { book: {} } as BooksStateModel, +@State({ + name: 'BookState', + defaults: { book: {} } as BookStateModel, }) @Injectable() -export class BooksState { +export class BookState { @Selector() - static getBooks(state: BooksStateModel) { + static getBooks(state: BookStateModel) { return state.book.items || []; } constructor(private bookService: BookService) {} @Action(GetBooks) - get(ctx: StateContext) { + get(ctx: StateContext) { return this.bookService.getListByInput().pipe( tap((booksResponse) => { ctx.patchState({ @@ -969,23 +954,23 @@ export class BooksState { } } ``` -* We added the book property to BooksStateModel model. -* We added `@Injectable()` decorator to BookState class (Regquired for Ivy to work properly). -* We added the `GetBooks` action that retrieves the books data via `BooksService` that generated via ABP CLI and patches the state. + +* We added the book property to BookStateModel model. +* We added the `GetBooks` action that retrieves the book data via `BookService` that generated via ABP CLI and patches the state. * `NGXS` requires to return the observable without subscribing it in the get function. #### BookListComponent -Open the `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below: +Open the `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below: ```js import { Component, OnInit } from '@angular/core'; import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; import { finalize } from 'rxjs/operators'; -import { BookDto, BookType } from '../../app/shared/models'; -import { GetBooks } from '../state/books.actions'; -import { BooksState } from '../state/books.state'; +import { BookDto, BookType } from '../models'; +import { GetBooks } from '../state/book.actions'; +import { BookState } from '../state/book.state'; @Component({ selector: 'app-book-list', @@ -993,7 +978,7 @@ import { BooksState } from '../state/books.state'; styleUrls: ['./book-list.component.scss'], }) export class BookListComponent implements OnInit { - @Select(BooksState.getBooks) + @Select(BookState.getBooks) books$: Observable; booksType = BookType; @@ -1019,7 +1004,7 @@ export class BookListComponent implements OnInit { * We added the `get` function that updates store to get the books. * See the [Dispatching actions](https://ngxs.gitbook.io/ngxs/concepts/store#dispatching-actions) and [Select](https://ngxs.gitbook.io/ngxs/concepts/select) on the `NGXS` documentation for more information on these `NGXS` features. -Open the `book-list.component.html` file in `app\books\book-list` folder and replace the content as below: +Open the `book-list.component.html` file in `app\book\book-list` folder and replace the content as below: ```html
diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md index cfe9936812..69f3e5436b 100644 --- a/docs/en/Tutorials/Part-2.md +++ b/docs/en/Tutorials/Part-2.md @@ -458,54 +458,54 @@ In this section, you will learn how to create a new modal dialog form to create #### State definitions -Open `books.action.ts` in `books\state` folder and replace the content as below: +Open `book.action.ts` in `app\book\state` folder and replace the content as below: ```js -import { CreateUpdateBookDto } from '../../app/shared/models'; //<== added this line ==> +import { CreateUpdateBookDto } from '../models'; //<== added this line ==> export class GetBooks { - static readonly type = '[Books] Get'; + static readonly type = '[Book] Get'; } // added CreateUpdateBook class export class CreateUpdateBook { - static readonly type = '[Books] Create Update Book'; + static readonly type = '[Book] Create Update Book'; constructor(public payload: CreateUpdateBookDto) { } } ``` * We imported the `CreateUpdateBookDto` model and created the `CreateUpdateBook` action. -Open `books.state.ts` file in `books\state` folder and replace the content as below: +Open `book.state.ts` file in `app\book\state` folder and replace the content as below: ```js import { PagedResultDto } from '@abp/ng.core'; import { State, Action, StateContext, Selector } from '@ngxs/store'; -import { GetBooks, CreateUpdateBook } from './books.actions'; // <== added CreateUpdateBook==> -import { BookService } from '../../app/shared/services'; +import { GetBooks, CreateUpdateBook } from './book.actions'; // <== added CreateUpdateBook==> +import { BookService } from '../services'; import { tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; -import { BookDto } from '../../app/shared/models'; +import { BookDto } from '../models'; -export class BooksStateModel { +export class BookStateModel { public book: PagedResultDto; } -@State({ - name: 'BooksState', - defaults: { book: {} } as BooksStateModel, +@State({ + name: 'BookState', + defaults: { book: {} } as BookStateModel, }) @Injectable() -export class BooksState { +export class BookState { @Selector() - static getBooks(state: BooksStateModel) { + static getBooks(state: BookStateModel) { return state.book.items || []; } constructor(private bookService: BookService) {} @Action(GetBooks) - get(ctx: StateContext) { + get(ctx: StateContext) { return this.bookService.getListByInput().pipe( tap((bookResponse) => { ctx.patchState({ @@ -517,7 +517,7 @@ export class BooksState { // added CreateUpdateBook action listener @Action(CreateUpdateBook) - save(ctx: StateContext, action: CreateUpdateBook) { + save(ctx: StateContext, action: CreateUpdateBook) { return this.bookService.createByInput(action.payload); } } @@ -605,16 +605,16 @@ Open `book-list.component.html` file in `books\book-list` folder and replace the * `abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits. * We added `New book` button to the `AbpContentToolbar`. -Open `book-list.component.ts` file in `books\book-list` folder and replace the content as below: +Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below: ```js import { Component, OnInit } from '@angular/core'; import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; import { finalize } from 'rxjs/operators'; -import { BookDto, BookType } from '../../app/shared/models'; -import { GetBooks } from '../state/books.actions'; -import { BooksState } from '../state/books.state'; +import { BookDto, BookType } from '../models'; +import { GetBooks } from '../state/book.actions'; +import { BookState } from '../state/book.state'; @Component({ selector: 'app-book-list', @@ -622,7 +622,7 @@ import { BooksState } from '../state/books.state'; styleUrls: ['./book-list.component.scss'], }) export class BookListComponent implements OnInit { - @Select(BooksState.getBooks) + @Select(BookState.getBooks) books$: Observable; booksType = BookType; @@ -662,16 +662,16 @@ You can open your browser and click **New book** button to see the new modal. [Reactive forms](https://angular.io/guide/reactive-forms) provide a model-driven approach to handling form inputs whose values change over time. -Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below: +Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below: ```js import { Component, OnInit } from '@angular/core'; import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; import { finalize } from 'rxjs/operators'; -import { BookDto, BookType } from '../../app/shared/models'; -import { GetBooks } from '../state/books.actions'; -import { BooksState } from '../state/books.state'; +import { BookDto, BookType } from '../models'; +import { GetBooks } from '../state/book.actions'; +import { BookState } from '../state/book.state'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== added this line ==> @Component({ @@ -680,7 +680,7 @@ import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== adde styleUrls: ['./book-list.component.scss'], }) export class BookListComponent implements OnInit { - @Select(BooksState.getBooks) + @Select(BookState.getBooks) books$: Observable; booksType = BookType; @@ -774,42 +774,40 @@ Open `book-list.component.html` in `app\books\book-list` folder and replace ` @NgModule({ - declarations: [BooksComponent, BookListComponent], + declarations: [BookListComponent], imports: [ CommonModule, - BooksRoutingModule, + BookRoutingModule, SharedModule, - NgbDatepickerModule //<== added this line ==> - ] + NgbDatepickerModule, //<== added this line ==> + ], }) -export class BooksModule { } +export class BookModule {} ``` * We imported `NgbDatepickerModule` to be able to use the date picker. -Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below: +Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below: ```js import { Component, OnInit } from '@angular/core'; import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; import { finalize } from 'rxjs/operators'; -import { BookDto, BookType } from '../../app/shared/models'; -import { GetBooks } from '../state/books.actions'; -import { BooksState } from '../state/books.state'; +import { BookDto, BookType } from '../models'; +import { GetBooks } from '../state/book.actions'; +import { BookState } from '../state/book.state'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; // <== added this line ==> @@ -820,7 +818,7 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added this line ==> }) export class BookListComponent implements OnInit { - @Select(BooksState.getBooks) + @Select(BookState.getBooks) books$: Observable; booksType = BookType; @@ -885,16 +883,16 @@ Now, you can open your browser to see the changes: #### Saving the book -Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below: +Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below: ```js import { Component, OnInit } from '@angular/core'; import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; import { finalize } from 'rxjs/operators'; -import { BookDto, BookType } from '../../app/shared/models'; -import { GetBooks, CreateUpdateBook } from '../state/books.actions'; // <== added CreateUpdateBook ==> -import { BooksState } from '../state/books.state'; +import { BookDto, BookType } from '../models'; +import { GetBooks, CreateUpdateBook } from '../state/book.actions'; // <== added CreateUpdateBook ==> +import { BookState } from '../state/book.state'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; @@ -905,12 +903,11 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], }) export class BookListComponent implements OnInit { - @Select(BooksState.getBooks) + @Select(BookState.getBooks) books$: Observable; booksType = BookType; - //added bookTypeArr array bookTypeArr = Object.keys(BookType).filter( (bookType) => typeof this.booksType[bookType] === 'number' ); @@ -949,7 +946,7 @@ export class BookListComponent implements OnInit { }); } - //<== added save ==> + // <== added save ==> save() { if (this.form.invalid) { return; @@ -967,7 +964,7 @@ export class BookListComponent implements OnInit { * We imported `CreateUpdateBook`. * We added `save` method -Open `book-list.component.html` in `app\books\book-list` folder and add the following `abp-button` to save the new book. +Open `book-list.component.html` in `app\book\book-list` folder and add the following `abp-button` to save the new book. ```html @@ -1001,28 +998,28 @@ The final modal UI looks like below: #### CreateUpdateBook action -Open the `books.actions.ts` in `books\state` folder and replace the content as below: +Open the `book.actions.ts` in `app\book\state` folder and replace the content as below: ```js -import { CreateUpdateBookDto } from '../../app/shared/models'; +import { CreateUpdateBookDto } from '../models'; export class GetBooks { - static readonly type = '[Books] Get'; + static readonly type = '[Book] Get'; } export class CreateUpdateBook { - static readonly type = '[Books] Create Update Book'; - constructor(public payload: CreateUpdateBookDto, public id?: string) { } // <== added id parameter ==> + static readonly type = '[Book] Create Update Book'; + constructor(public payload: CreateUpdateBookDto, public id?: string) {} // <== added id parameter ==> } ``` * We added `id` parameter to the `CreateUpdateBook` action's constructor. -Open the `books.state.ts` in `books\state` folder and replace the `save` method as below: +Open the `book.state.ts` in `app\book\state` folder and replace the `save` method as below: ```js @Action(CreateUpdateBook) -save(ctx: StateContext, action: CreateUpdateBook) { +save(ctx: StateContext, action: CreateUpdateBook) { if (action.id) { return this.bookService.updateByIdAndInput(action.payload, action.id); } else { @@ -1033,19 +1030,19 @@ save(ctx: StateContext, action: CreateUpdateBook) { #### BookListComponent -Open `book-list.component.ts` in `app\books\book-list` folder and inject `BookService` dependency by adding it to the constructor and add a variable named `selectedBook`. +Open `book-list.component.ts` in `app\book\book-list` folder and inject `BookService` dependency by adding it to the constructor and add a variable named `selectedBook`. ```js import { Component, OnInit } from '@angular/core'; import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; import { finalize } from 'rxjs/operators'; -import { BookDto, BookType } from '../../app/shared/models'; -import { GetBooks, CreateUpdateBook } from '../state/books.actions'; -import { BooksState } from '../state/books.state'; +import { BookDto, BookType } from '../models'; +import { GetBooks, CreateUpdateBook } from '../state/book.actions'; +import { BookState } from '../state/book.state'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; -import { BookService } from '../../app/shared/services'; // <== imported BookService ==> +import { BookService } from '../services'; // <== imported BookService ==> @Component({ selector: 'app-book-list', @@ -1054,7 +1051,7 @@ import { BookService } from '../../app/shared/services'; // <== imported BookSer providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], }) export class BookListComponent implements OnInit { - @Select(BooksState.getBooks) + @Select(BookState.getBooks) books$: Observable; booksType = BookType; @@ -1141,7 +1138,7 @@ export class BookListComponent implements OnInit { #### Add "Actions" dropdown to the table -Open the `book-list.component.html` in `app\books\book-list` folder and replace the `
` tag as below: +Open the `book-list.component.html` in `app\book\book-list` folder and replace the `
` tag as below: ```html
@@ -1199,7 +1196,7 @@ The final UI looks like as below: ![Action buttons](./images/bookstore-actions-buttons.png) -Open `book-list.component.html` in `app\books\book-list` folder and find the `` tag and replace the content as below. +Open `book-list.component.html` in `app\book\book-list` folder and find the `` tag and replace the content as below. ```html @@ -1213,45 +1210,45 @@ Open `book-list.component.html` in `app\books\book-list` folder and find the ` -import { BookService } from '../../app/shared/services'; +import { GetBooks, CreateUpdateBook, DeleteBook } from './book.actions'; // <== added DeleteBook==> +import { BookService } from '../services'; import { tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; -import { BookDto } from '../../app/shared/models'; +import { BookDto } from '../models'; -export class BooksStateModel { +export class BookStateModel { public book: PagedResultDto; } -@State({ - name: 'BooksState', - defaults: { book: {} } as BooksStateModel, +@State({ + name: 'BookState', + defaults: { book: {} } as BookStateModel, }) @Injectable() -export class BooksState { +export class BookState { @Selector() - static getBooks(state: BooksStateModel) { + static getBooks(state: BookStateModel) { return state.book.items || []; } constructor(private bookService: BookService) {} @Action(GetBooks) - get(ctx: StateContext) { + get(ctx: StateContext) { return this.bookService.getListByInput().pipe( tap((booksResponse) => { ctx.patchState({ @@ -1262,7 +1259,7 @@ export class BooksState { } @Action(CreateUpdateBook) - save(ctx: StateContext, action: CreateUpdateBook) { + save(ctx: StateContext, action: CreateUpdateBook) { if (action.id) { return this.bookService.updateByIdAndInput(action.payload, action.id); } else { @@ -1272,7 +1269,7 @@ export class BooksState { // <== added DeleteBook action listener ==> @Action(DeleteBook) - delete(ctx: StateContext, action: DeleteBook) { + delete(ctx: StateContext, action: DeleteBook) { return this.bookService.deleteById(action.id); } } @@ -1285,7 +1282,7 @@ export class BooksState { #### Delete confirmation popup -Open `book-list.component.ts` in`app\books\book-list` folder and inject the `ConfirmationService`. +Open `book-list.component.ts` in`app\book\book-list` folder and inject the `ConfirmationService`. Replace the constructor as below: @@ -1309,7 +1306,7 @@ See the [Confirmation Popup documentation](https://docs.abp.io/en/abp/latest/UI/ In the `book-list.component.ts` add a delete method : ```js -import { GetBooks, CreateUpdateBook, DeleteBook } from '../state/books.actions' ;// <== imported DeleteBook ==> +import { GetBooks, CreateUpdateBook, DeleteBook } from '../state/book.actions' ;// <== imported DeleteBook ==> import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation ==> @@ -1335,7 +1332,7 @@ The `delete` method shows a confirmation popup and subscribes for the user respo #### Add a delete button -Open `book-list.component.html` in `app\books\book-list` folder and modify the `ngbDropdownMenu` to add the delete button as shown below: +Open `book-list.component.html` in `app\book\book-list` folder and modify the `ngbDropdownMenu` to add the delete button as shown below: ```html
diff --git a/docs/en/Tutorials/images/bookstore-angular-file-tree.png b/docs/en/Tutorials/images/bookstore-angular-file-tree.png index a3197b6457..ffa8dcd7e2 100644 Binary files a/docs/en/Tutorials/images/bookstore-angular-file-tree.png and b/docs/en/Tutorials/images/bookstore-angular-file-tree.png differ diff --git a/docs/en/Tutorials/images/bookstore-book-list.png b/docs/en/Tutorials/images/bookstore-book-list.png index 9e6cc9e010..d402895c9b 100644 Binary files a/docs/en/Tutorials/images/bookstore-book-list.png and b/docs/en/Tutorials/images/bookstore-book-list.png differ diff --git a/docs/en/Tutorials/images/bookstore-creating-book-list-terminal.png b/docs/en/Tutorials/images/bookstore-creating-book-list-terminal.png index 6f19dcc7bf..13829db60f 100644 Binary files a/docs/en/Tutorials/images/bookstore-creating-book-list-terminal.png and b/docs/en/Tutorials/images/bookstore-creating-book-list-terminal.png differ diff --git a/docs/en/Tutorials/images/bookstore-creating-book-module-terminal.png b/docs/en/Tutorials/images/bookstore-creating-book-module-terminal.png new file mode 100644 index 0000000000..c935a6f130 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-creating-book-module-terminal.png differ diff --git a/docs/en/Tutorials/images/bookstore-creating-books-module-terminal.png b/docs/en/Tutorials/images/bookstore-creating-books-module-terminal.png deleted file mode 100644 index ec9ef4c42f..0000000000 Binary files a/docs/en/Tutorials/images/bookstore-creating-books-module-terminal.png and /dev/null differ diff --git a/docs/en/Tutorials/images/generated-proxies.png b/docs/en/Tutorials/images/generated-proxies.png index 9e466e7d55..1d322c0765 100644 Binary files a/docs/en/Tutorials/images/generated-proxies.png and b/docs/en/Tutorials/images/generated-proxies.png differ