Merge branch 'master' into dev

pull/3873/head
Halil İbrahim Kalkan 5 years ago
commit ef334ebd56

@ -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<IdentityUser>(options =>
{
options.AddOrUpdateProperty<string>("SocialSecurityNumber");
options.AddOrUpdateProperty<bool>("IsSuperUser");
}
);
````
It is easy to define validation rules for the properties:
````csharp
ObjectExtensionManager.Instance
.AddOrUpdateProperty<IdentityUserCreateDto, string>(
"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<IdentityUser, string>(
"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
<h3>{{L "PasswordReset"}}</h3>
<p>{{L "PasswordResetInfoInEmail"}}</p>
<div>
<a href="{{model.link}}">{{L "ResetMyPassword"}}</a>
</div>
````
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
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
</head>
<body>
{{content}}
</body>
</html>
````
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<string>( //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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

@ -0,0 +1,3 @@
# Text-Templating
TODO

@ -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
<router-outlet></router-outlet>
```
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<BookDto>;
}
@State<BooksStateModel>({
name: 'BooksState',
defaults: { book: {} } as BooksStateModel,
@State<BookStateModel>({
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<BooksStateModel>) {
get(ctx: StateContext<BookStateModel>) {
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<BookDto[]>;
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
<div class="card">

@ -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<BookDto>;
}
@State<BooksStateModel>({
name: 'BooksState',
defaults: { book: {} } as BooksStateModel,
@State<BookStateModel>({
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<BooksStateModel>) {
get(ctx: StateContext<BookStateModel>) {
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<BooksStateModel>, action: CreateUpdateBook) {
save(ctx: StateContext<BookStateModel>, 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<BookDto[]>;
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<BookDto[]>;
booksType = BookType;
@ -774,42 +774,40 @@ Open `book-list.component.html` in `app\books\book-list` folder and replace `<ng
#### Datepicker requirements
Open `books.module.ts` file in `app\books` folder and replace the content as below:
Open `book.module.ts` file in `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';
import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; //<== added this line ==>
@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<BookDto[]>;
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<BookDto[]>;
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
<ng-template #abpFooter>
@ -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<BooksStateModel>, action: CreateUpdateBook) {
save(ctx: StateContext<BookStateModel>, action: CreateUpdateBook) {
if (action.id) {
return this.bookService.updateByIdAndInput(action.payload, action.id);
} else {
@ -1033,19 +1030,19 @@ save(ctx: StateContext<BooksStateModel>, 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<BookDto[]>;
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 `<div class="card-body">` tag as below:
Open the `book-list.component.html` in `app\book\book-list` folder and replace the `<div class="card-body">` tag as below:
```html
<div class="card-body">
@ -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 `<ng-template #abpHeader>` tag and replace the content as below.
Open `book-list.component.html` in `app\book\book-list` folder and find the `<ng-template #abpHeader>` tag and replace the content as below.
```html
<ng-template #abpHeader>
@ -1213,45 +1210,45 @@ Open `book-list.component.html` in `app\books\book-list` folder and find the `<n
#### DeleteBook action
Open `books.actions.ts` in `books\state `folder and add an action named `DeleteBook`.
Open `book.actions.ts` in `app\book\state` folder and add an action named `DeleteBook`.
```js
export class DeleteBook {
static readonly type = '[Books] Delete';
static readonly type = '[Book] Delete';
constructor(public id: string) {}
}
```
Open the `books.state.ts` in `books\state` folder and replace the content as below:
Open the `book.state.ts` 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, DeleteBook } from './books.actions'; // <== added DeleteBook==>
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<BookDto>;
}
@State<BooksStateModel>({
name: 'BooksState',
defaults: { book: {} } as BooksStateModel,
@State<BookStateModel>({
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<BooksStateModel>) {
get(ctx: StateContext<BookStateModel>) {
return this.bookService.getListByInput().pipe(
tap((booksResponse) => {
ctx.patchState({
@ -1262,7 +1259,7 @@ export class BooksState {
}
@Action(CreateUpdateBook)
save(ctx: StateContext<BooksStateModel>, action: CreateUpdateBook) {
save(ctx: StateContext<BookStateModel>, 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<BooksStateModel>, action: DeleteBook) {
delete(ctx: StateContext<BookStateModel>, 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
<div ngbDropdownMenu>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Loading…
Cancel
Save