diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
index a5089e06f7..d0d829bbf3 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
+++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
@@ -13,8 +13,11 @@
"Permission:Edit": "Edit",
"Permission:Delete": "Delete",
"Permission:Create": "Create",
+ "Permission:Accounting": "Accounting",
+ "Permission:Accounting:Quotation": "Quotation",
+ "Permission:Accounting:Invoice": "Invoice",
"Menu:Organizations": "Organizations",
- "Menu:Accounting": "Accounting",
+ "Menu:Accounting": "Accounting",
"Menu:Packages": "Packages",
"NpmPackageDeletionWarningMessage": "This NPM Package will be deleted. Do you confirm that?",
"NugetPackageDeletionWarningMessage": "This Nuget Package will be deleted. Do you confirm that?",
@@ -94,13 +97,28 @@
"UsernameOrEmail": "Username or email",
"UsernameOrEmailPlaceholder": "Username or email...",
"Member": "Member",
- "QuotationPurchasedOrderNo": "Quotation Purchased Order No",
- "QuotationTime": "Quotation Time",
- "CompanyName": "Company Name",
- "CompanyAddress": "Company Address",
+ "PurchaseOrderNo": "Purchase order no",
+ "QuotationDate": "Quotation date",
+ "CompanyName": "Company name",
+ "CompanyAddress": "Company address",
"Price": "Price",
+ "DiscountText": "Discount text",
+ "DiscountQuantity": "Discount quantity",
+ "DiscountPrice": "Discount price",
+ "Quotation": "Quotation",
"ExtraText": "Extra Text",
"ExtraAmount": "Extra Amount",
- "DownloadQuotation": "Download Quotation"
+ "DownloadQuotation": "Download Quotation",
+ "Invoice": "Invoice",
+ "TaxNumber": "Tax Number",
+ "InvoiceNumber": "Invoice Number",
+ "InvoiceDate": "Invoice Date",
+ "Quantity": "Quantity",
+ "AddProduct": "Add Product",
+ "AddProductWarning": "You need to add product!",
+ "TotalPrice": "Total Price",
+ "Generate": "Generate",
+ "MissingQuantityField": "The quantity field is required!",
+ "MissingPriceField": "The Price field is required!"
}
}
\ No newline at end of file
diff --git a/common.DotSettings b/common.DotSettings
index 0eb4875d49..6f40d029a7 100644
--- a/common.DotSettings
+++ b/common.DotSettings
@@ -20,5 +20,11 @@
FalseFalseSQL
+ False
+ False
+ False
+ False
+ False
+ FalseTrue
\ No newline at end of file
diff --git a/common.props b/common.props
index 19b6f81fe6..ab7b86e3b2 100644
--- a/common.props
+++ b/common.props
@@ -1,7 +1,7 @@
latest
- 2.7.0
+ 2.8.0$(NoWarn);CS1591https://abp.io/assets/abp_nupkg.pnghttps://abp.io
diff --git a/docs/en/CLI.md b/docs/en/CLI.md
index b687a5244a..75e3e027b4 100644
--- a/docs/en/CLI.md
+++ b/docs/en/CLI.md
@@ -135,6 +135,8 @@ abp update [options]
* `--include-previews` or `-p`: Includes preview, beta and rc packages while checking the latest versions.
* `--npm`: Only updates NPM packages.
* `--nuget`: Only updates NuGet packages.
+* `--solution-path` or `-sp`: Specify the solution path. Use the current directory by default
+* `--solution-name` or `-sn`: Specify the solution name. Search `*.sln` files in the directory by default.
### switch-to-preview
@@ -170,7 +172,11 @@ Some features of the CLI requires to be logged in to abp.io platform. To login w
abp login
```
-Notice that, a new login with an already active session, will kill the previous session and creates a new one.
+```bash
+abp login -p
+```
+
+Notice that, a new login with an already active session, overwrites the previous session.
### logout
diff --git a/docs/en/Entity-Framework-Core-Migrations.md b/docs/en/Entity-Framework-Core-Migrations.md
index e0772579ec..00e791a94e 100644
--- a/docs/en/Entity-Framework-Core-Migrations.md
+++ b/docs/en/Entity-Framework-Core-Migrations.md
@@ -546,6 +546,8 @@ Entity extension system solves the main problem of the extra properties: It can
All you need to do is to use the `ObjectExtensionManager` to define the extra property as explained above, in the `AppRole` example. Then you can continue to use the same `GetProperty` and `SetProperty` methods defined above to get/set the related property on the entity, but this time stored as a separate field in the database.
+See the [entity extension system](Customizing-Application-Modules-Extending-Entities.md) for details.
+
###### Creating a New Table
Instead of creating a new entity and mapping to the same table, you can also create **your own table** to store your properties. You typically duplicate some values of the original entity. For example, you can add `Name` field to your own table which is a duplication of the `Name` field in the original table.
diff --git a/docs/en/Getting-Started.md b/docs/en/Getting-Started.md
index 07e55f553e..fa7f47f276 100644
--- a/docs/en/Getting-Started.md
+++ b/docs/en/Getting-Started.md
@@ -25,6 +25,12 @@ The following tools should be installed on your development machine:
* [Node v12+](https://nodejs.org)
* [Yarn v1.19+](https://classic.yarnpkg.com/)
+{{ if Tiered == "Yes" }}
+
+* [Redis](https://redis.io/): The applications use Redis as as [distributed cache](../Caching.md). So, you need to have Redis installed & running.
+
+{{ end }}
+
> You can use another editor instead of Visual Studio as long as it supports .NET Core and ASP.NET Core.
@@ -400,8 +406,8 @@ Enter **admin** as the username and **1q2w3E*** as the password to login to the
The application is up and running. You can continue to develop your application based on this startup template.
-> The [application startup template](startup-templates/application/index.md) includes the TenantManagement and Identity modules.
+> The [application startup template](Startup-Templates/Application.md) includes the TenantManagement and Identity modules.
## What's next?
-[Application development tutorial](tutorials/book-store/part-1.md)
+[Application development tutorial](Tutorials/Part-1.md)
diff --git a/docs/en/Localization.md b/docs/en/Localization.md
index 11b838cf7f..954fe8620f 100644
--- a/docs/en/Localization.md
+++ b/docs/en/Localization.md
@@ -87,6 +87,21 @@ A JSON localization file content is shown below:
* Every localization file should define the `culture` code for the file (like "en" or "en-US").
* `texts` section just contains key-value collection of the localization strings (keys may have spaces too).
+### Default Resource
+
+`AbpLocalizationOptions.DefaultResourceType` can be set to a resource type, so it is used when the localization resource was not specified:
+
+````csharp
+Configure(options =>
+{
+ options.DefaultResourceType = typeof(TestResource);
+});
+````
+
+> The [application startup template](Startup-Templates/Application.md) sets `DefaultResourceType` to the localization resource of the application.
+
+See the *Client Side* section below for a use case.
+
### Short Localization Resource Name
Localization resources are also available in the client (JavaScript) side. So, setting a short name for the localization resource makes it easy to use localization texts. Example:
@@ -166,6 +181,10 @@ public class MyService
}
````
+##### Format Arguments
+
+Format arguments can be passed after the localization key. If your message is `Hello {0}, welcome!`, then you can pass the `{0}` argument to the localizer like `_localizer["HelloMessage", "John"]`
+
#### Simplest Usage In A Razor View/Page
````c#
@@ -180,18 +199,47 @@ Refer to the [Microsoft's localization documentation](https://docs.microsoft.com
ABP provides JavaScript services to use the same localized texts in the client side.
-Get a localization resource:
+#### getResource
+
+`abp.localization.getResource` function is used to get a localization resource:
````js
var testResource = abp.localization.getResource('Test');
````
-Localize a string:
+Then you can localize a string based on this resource:
````js
var str = testResource('HelloWorld');
````
+#### localize
+
+`abp.localization.localize` function is a shortcut where you can both specify the text name and the resource name:
+
+````js
+var str = abp.localization.localize('HelloWorld', 'Test');
+````
+
+`HelloWorld` is the text to localize, where `Test` is the localization resource name here.
+
+If you don't specify the localization resource name, it uses the default localization resource defined on the `AbpLocalizationOptions` (see the *Default Resource* section above). Example:
+
+````js
+var str = abp.localization.localize('HelloWorld'); //uses the default resource
+````
+
+##### Format Arguments
+
+If your localized string contains arguments, like `Hello {0}, welcome!`, you can pass arguments to the localization methods. Examples:
+
+````js
+var str1 = abp.localization.getResource('Test')('HelloWelcomeMessage', 'John');
+var str2 = abp.localization.localize('HelloWorld', 'Test', 'John');
+````
+
+Both of the samples above produce the output `Hello John, welcome!`.
+
## See Also
* [Localization in Angular UI](UI/Angular/Localization.md)
\ No newline at end of file
diff --git a/docs/en/Object-Extensions.md b/docs/en/Object-Extensions.md
index 6eff328c85..bee229dc27 100644
--- a/docs/en/Object-Extensions.md
+++ b/docs/en/Object-Extensions.md
@@ -197,8 +197,8 @@ ObjectExtensionManager.Instance
"SocialSecurityNumber",
options =>
{
- options.ValidationAttributes.Add(new RequiredAttribute());
- options.ValidationAttributes.Add(
+ options.Attributes.Add(new RequiredAttribute());
+ options.Attributes.Add(
new StringLengthAttribute(32) {
MinimumLength = 6
}
@@ -248,12 +248,12 @@ ObjectExtensionManager.Instance
objConfig.AddOrUpdateProperty("Password", propertyConfig =>
{
- propertyConfig.ValidationAttributes.Add(new RequiredAttribute());
+ propertyConfig.Attributes.Add(new RequiredAttribute());
});
objConfig.AddOrUpdateProperty("PasswordRepeat", propertyConfig =>
{
- propertyConfig.ValidationAttributes.Add(new RequiredAttribute());
+ propertyConfig.Attributes.Add(new RequiredAttribute());
});
//Write a common validation logic works on multiple properties
diff --git a/docs/en/Tutorials/Part-1.md b/docs/en/Tutorials/Part-1.md
index a0aa6612e9..51ab468d07 100644
--- a/docs/en/Tutorials/Part-1.md
+++ b/docs/en/Tutorials/Part-1.md
@@ -38,7 +38,7 @@ Create a new project named `Acme.BookStore` where `Acme` is the company name and
#### Create the project
-By running the below command, it creates a new ABP project with the database provider `{{DB_Text}}` and UI option `MVC`. To see the other CLI options, check out [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) document.
+By running the below command, it creates a new ABP project with the database provider `{{DB_Text}}` and UI option `{{UI_Value}}`. To see the other CLI options, check out [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) document.
```bash
abp new Acme.BookStore --template app --database-provider {{DB}} --ui {{UI_Text}} --mobile none
@@ -61,7 +61,7 @@ After creating the project, you need to apply the initial migrations and create
To run the project, right click to the {{if UI == "MVC"}} `Acme.BookStore.Web`{{end}} {{if UI == "NG"}} `Acme.BookStore.HttpApi.Host` {{end}} project and click **Set As StartUp Project**. And run the web project by pressing **CTRL+F5** (*without debugging and fast*) or press **F5** (*with debugging and slow*). {{if UI == "NG"}}You will see the Swagger UI for BookStore API.{{end}}
-Further information, see the [running the application section](../../Getting-Started-{{if UI == "NG"}}Angular{{else}}AspNetCore-MVC{{end}}-Template#running-the-application).Getting-Started-AspNetCore-MVC-Template#running-the-application
+Further information, see the [running the application section](../Getting-Started?UI={{UI}}#run-the-application).

@@ -335,7 +335,7 @@ INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES
### Create the application service
-The next step is to create an [application service](../../Application-Services.md) to manage the books which will allow us the four basic functions: creating, reading, updating and deleting. Application layer is separated into two projects:
+The next step is to create an [application service](../Application-Services.md) to manage the books which will allow us the four basic functions: creating, reading, updating and deleting. Application layer is separated into two projects:
* `Acme.BookStore.Application.Contracts` mainly contains your `DTO`s and application service interfaces.
* `Acme.BookStore.Application` contains the implementations of your application services.
@@ -874,97 +874,52 @@ We'll see **book-list works!** text on the books page:
Run the following command in the terminal to create a new state, named `BooksState`:
-
-
```bash
-yarn ng generate ngxs-schematic:state books
+npx @ngxs/cli --name books --directory src/app/books
```
-* This command creates several new files and updates `app.modules.ts` file to import the `NgxsModule` with the new state.
-
-#### Get books data from backend
-
-Create data types to map the data from the backend (you can check Swagger UI or your backend API to see the data format).
+* 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).
-
-
-Open the `books.ts` file in the `app\store\models` folder and replace the content as below:
+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.
```js
-export namespace Books {
- export interface State {
- books: Response;
- }
+// ...
+import { BooksState } from './books/state/books.state'; //<== imported BooksState ==>
- export interface Response {
- items: Book[];
- totalCount: number;
- }
+@NgModule({
+ imports: [
+ // other imports
- export interface Book {
- name: string;
- type: BookType;
- publishDate: string;
- price: number;
- lastModificationTime: string;
- lastModifierId: string;
- creationTime: string;
- creatorId: string;
- id: string;
- }
+ NgxsModule.forRoot([BooksState]), //<== added BooksState ==>
- export enum BookType {
- Undefined,
- Adventure,
- Biography,
- Dystopia,
- Fantastic,
- Horror,
- Science,
- ScienceFiction,
- Poetry,
- }
-}
+ //other imports
+ ],
+ // ...
+})
+export class AppModule {}
```
-* Added `Book` interface that represents a book object and `BookType` enum which represents a book category.
+#### Generate proxies
-#### BooksService
+ABP CLI provides `generate-proxy` command that generates client proxies for your HTTP APIs to make easy to consume your services from the client side. Before running generate-proxy command, your host must be up and running. See the [CLI documentation](../CLI.md)
-Create a new service, named `BooksService` to perform `HTTP` calls to the server:
+Run the following command in the `angular` folder:
```bash
-yarn ng generate service books/shared/books
+abp generate-proxy --module app
```
-
+
-Open the `books.service.ts` file in `app\books\shared` folder and replace the content as below:
+The generated files looks like below:
-```js
-import { Injectable } from '@angular/core';
-import { RestService } from '@abp/ng.core';
-import { Books } from '../../store/models';
-import { Observable } from 'rxjs';
+
-@Injectable({
- providedIn: 'root',
-})
-export class BooksService {
- constructor(private restService: RestService) {}
-
- get(): Observable {
- return this.restService.request({
- method: 'GET',
- url: '/api/app/book'
- });
- }
-}
-```
+#### GetBooks Action
-* We added the `get` method to get the list of books by performing an HTTP request to the related endpoint.
+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\store\actions` folder and replace the content below:
+Open the `books.actions.ts` file in `app/books/state` folder and replace the content below:
```js
export class GetBooks {
@@ -974,43 +929,49 @@ export class GetBooks {
#### Implement BooksState
-Open the `books.state.ts` file in `app\store\states` folder and replace the content below:
+Open the `books.state.ts` file in `app/books/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 '../actions/books.actions';
-import { Books } from '../models/books';
-import { BooksService } from '../../books/shared/books.service';
+import { GetBooks } from './books.actions';
+import { BookService } from '../../app/shared/services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
+import { BookDto } from '../../app/shared/models';
-@State({
+export class BooksStateModel {
+ public book: PagedResultDto;
+}
+
+@State({
name: 'BooksState',
- defaults: { books: {} } as Books.State,
+ defaults: { book: {} } as BooksStateModel,
})
@Injectable()
export class BooksState {
@Selector()
- static getBooks(state: Books.State) {
- return state.books.items || [];
+ static getBooks(state: BooksStateModel) {
+ return state.book.items || [];
}
- constructor(private booksService: BooksService) {}
+ constructor(private bookService: BookService) {}
@Action(GetBooks)
- get(ctx: StateContext) {
- return this.booksService.get().pipe(
- tap(booksResponse => {
+ get(ctx: StateContext) {
+ return this.bookService.getListByInput().pipe(
+ tap((booksResponse) => {
ctx.patchState({
- books: booksResponse,
+ book: booksResponse,
});
- }),
+ })
);
}
}
```
-
-* We added the `GetBooks` action that retrieves the books data via `BooksService` and patches the state.
+* 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.
* `NGXS` requires to return the observable without subscribing it in the get function.
#### BookListComponent
@@ -1019,11 +980,12 @@ Open the `book-list.component.ts` file in `app\books\book-list` folder and repla
```js
import { Component, OnInit } from '@angular/core';
-import { Store, Select } from '@ngxs/store';
-import { BooksState } from '../../store/states';
+import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
-import { Books } from '../../store/models';
-import { GetBooks } from '../../store/actions';
+import { finalize } from 'rxjs/operators';
+import { BookDto, BookType } from '../../app/shared/models';
+import { GetBooks } from '../state/books.actions';
+import { BooksState } from '../state/books.state';
@Component({
selector: 'app-book-list',
@@ -1032,13 +994,13 @@ import { GetBooks } from '../../store/actions';
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
- books$: Observable;
+ books$: Observable;
- booksType = Books.BookType;
+ booksType = BookType;
loading = false;
- constructor(private store: Store) { }
+ constructor(private store: Store) {}
ngOnInit() {
this.get();
@@ -1046,9 +1008,10 @@ export class BookListComponent implements OnInit {
get() {
this.loading = true;
- this.store.dispatch(new GetBooks()).subscribe(() => {
- this.loading = false;
- });
+ this.store
+ .dispatch(new GetBooks())
+ .pipe(finalize(() => (this.loading = false)))
+ .subscribe(() => {});
}
}
```
diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md
index 9565724cb8..cfe9936812 100644
--- a/docs/en/Tutorials/Part-2.md
+++ b/docs/en/Tutorials/Part-2.md
@@ -456,161 +456,76 @@ Run the application and try to delete a book.
In this section, you will learn how to create a new modal dialog form to create a new book.
-#### Type definition
-
-Open `books.ts` file in `app\store\models` folder and replace the content as below:
-
-```js
-export namespace Books {
- export interface State {
- books: Response;
- }
-
- export interface Response {
- items: Book[];
- totalCount: number;
- }
-
- export interface Book {
- name: string;
- type: BookType;
- publishDate: string;
- price: number;
- lastModificationTime: string;
- lastModifierId: string;
- creationTime: string;
- creatorId: string;
- id: string;
- }
-
- export enum BookType {
- Undefined,
- Adventure,
- Biography,
- Dystopia,
- Fantastic,
- Horror,
- Science,
- ScienceFiction,
- Poetry,
- }
-
- //<== added CreateUpdateBookInput interface ==>
- export interface CreateUpdateBookInput {
- name: string;
- type: BookType;
- publishDate: string;
- price: number;
- }
-}
-```
-
-* We added `CreateUpdateBookInput` interface.
-* You can see the properties of this interface from Swagger UI.
-* The `CreateUpdateBookInput` interface matches with the `CreateUpdateBookDto` in the backend.
-
-#### Service method
-
-Open the `books.service.ts` file in `app\books\shared` folder and replace the content as below:
-
-```js
-import { Injectable } from '@angular/core';
-import { RestService } from '@abp/ng.core';
-import { Books } from '../../store/models';
-import { Observable } from 'rxjs';
-
-@Injectable({
- providedIn: 'root',
-})
-export class BooksService {
- constructor(private restService: RestService) {}
-
- get(): Observable {
- return this.restService.request({
- method: 'GET',
- url: '/api/app/book'
- });
- }
-
- //<== added create method ==>
- create(createBookInput: Books.CreateUpdateBookInput): Observable {
- return this.restService.request({
- method: 'POST',
- url: '/api/app/book',
- body: createBookInput
- });
- }
-}
-```
-
-- We added the `create` method to perform an HTTP Post request to the server.
-- `restService.request` function gets generic parameters for the types sent to and received from the server. This example sends a `CreateUpdateBookInput` object and receives a `Book` object (you can set `void` for request or return type if not used).
-
#### State definitions
-Open `books.action.ts` in `app\store\actions` folder and replace the content as below:
+Open `books.action.ts` in `books\state` folder and replace the content as below:
```js
-import { Books } from '../models'; //<== added this line ==>
+import { CreateUpdateBookDto } from '../../app/shared/models'; //<== added this line ==>
export class GetBooks {
static readonly type = '[Books] Get';
}
-//added CreateUpdateBook class
+// added CreateUpdateBook class
export class CreateUpdateBook {
static readonly type = '[Books] Create Update Book';
- constructor(public payload: Books.CreateUpdateBookInput) { }
+ constructor(public payload: CreateUpdateBookDto) { }
}
```
-* We imported the Books namespace and created the `CreateUpdateBook` action.
+* We imported the `CreateUpdateBookDto` model and created the `CreateUpdateBook` action.
-Open `books.state.ts` file in `app\store\states` and replace the content as below:
+Open `books.state.ts` file in `books\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 '../actions/books.actions'; //<== added CreateUpdateBook==>
-import { Books } from '../models/books';
-import { BooksService } from '../../books/shared/books.service';
+import { GetBooks, CreateUpdateBook } from './books.actions'; // <== added CreateUpdateBook==>
+import { BookService } from '../../app/shared/services';
import { tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
+import { BookDto } from '../../app/shared/models';
+
+export class BooksStateModel {
+ public book: PagedResultDto;
+}
-@State({
+@State({
name: 'BooksState',
- defaults: { books: {} } as Books.State,
+ defaults: { book: {} } as BooksStateModel,
})
@Injectable()
export class BooksState {
@Selector()
- static getBooks(state: Books.State) {
- return state.books.items || [];
+ static getBooks(state: BooksStateModel) {
+ return state.book.items || [];
}
- constructor(private booksService: BooksService) { }
+ constructor(private bookService: BookService) {}
@Action(GetBooks)
- get(ctx: StateContext) {
- return this.booksService.get().pipe(
- tap(booksResponse => {
+ get(ctx: StateContext) {
+ return this.bookService.getListByInput().pipe(
+ tap((bookResponse) => {
ctx.patchState({
- books: booksResponse,
+ book: bookResponse,
});
- }),
+ })
);
}
- //added CreateUpdateBook action listener
+ // added CreateUpdateBook action listener
@Action(CreateUpdateBook)
- save(ctx: StateContext, action: CreateUpdateBook) {
- return this.booksService.create(action.payload);
+ save(ctx: StateContext, action: CreateUpdateBook) {
+ return this.bookService.createByInput(action.payload);
}
}
```
* We imported `CreateUpdateBook` action and defined the `save` method that will listen to a `CreateUpdateBook` action to create a book.
-When the `SaveBook` action dispatched, the save method is being executed. It calls `create` method of the `BooksService`.
+When the `SaveBook` action dispatched, the save method is being executed. It calls `createByInput` method of the `BookService`.
#### Add a modal to BookListComponent
@@ -694,11 +609,12 @@ Open `book-list.component.ts` file in `books\book-list` folder and replace the c
```js
import { Component, OnInit } from '@angular/core';
-import { Store, Select } from '@ngxs/store';
-import { BooksState } from '../../store/states';
+import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
-import { Books } from '../../store/models';
-import { GetBooks } from '../../store/actions';
+import { finalize } from 'rxjs/operators';
+import { BookDto, BookType } from '../../app/shared/models';
+import { GetBooks } from '../state/books.actions';
+import { BooksState } from '../state/books.state';
@Component({
selector: 'app-book-list',
@@ -707,15 +623,15 @@ import { GetBooks } from '../../store/actions';
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
- books$: Observable;
+ books$: Observable;
- booksType = Books.BookType;
+ booksType = BookType;
loading = false;
- isModalOpen = false; //<== added this line ==>
+ isModalOpen = false; // <== added this line ==>
- constructor(private store: Store) { }
+ constructor(private store: Store) {}
ngOnInit() {
this.get();
@@ -723,12 +639,13 @@ export class BookListComponent implements OnInit {
get() {
this.loading = true;
- this.store.dispatch(new GetBooks()).subscribe(() => {
- this.loading = false;
- });
+ this.store
+ .dispatch(new GetBooks())
+ .pipe(finalize(() => (this.loading = false)))
+ .subscribe(() => {});
}
- //added createBook method
+ // added createBook method
createBook() {
this.isModalOpen = true;
}
@@ -749,12 +666,13 @@ Open `book-list.component.ts` file in `app\books\book-list` folder and replace t
```js
import { Component, OnInit } from '@angular/core';
-import { Store, Select } from '@ngxs/store';
-import { BooksState } from '../../store/states';
+import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
-import { Books } from '../../store/models';
-import { GetBooks } from '../../store/actions';
-import { FormGroup, FormBuilder, Validators } from '@angular/forms'; //<== added this line ==>
+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 { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== added this line ==>
@Component({
selector: 'app-book-list',
@@ -763,17 +681,17 @@ import { FormGroup, FormBuilder, Validators } from '@angular/forms'; //<== added
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
- books$: Observable;
+ books$: Observable;
- booksType = Books.BookType;
+ booksType = BookType;
loading = false;
isModalOpen = false;
- form: FormGroup;
+ form: FormGroup; // <== added this line ==>
- constructor(private store: Store, private fb: FormBuilder) { } //<== added FormBuilder ==>
+ constructor(private store: Store, private fb: FormBuilder) {} // <== added FormBuilder ==>
ngOnInit() {
this.get();
@@ -781,9 +699,10 @@ export class BookListComponent implements OnInit {
get() {
this.loading = true;
- this.store.dispatch(new GetBooks()).subscribe(() => {
- this.loading = false;
- });
+ this.store
+ .dispatch(new GetBooks())
+ .pipe(finalize(() => (this.loading = false)))
+ .subscribe(() => {});
}
createBook() {
@@ -791,7 +710,7 @@ export class BookListComponent implements OnInit {
this.isModalOpen = true;
}
- //added buildForm method
+ // added buildForm method
buildForm() {
this.form = this.fb.group({
name: ['', Validators.required],
@@ -804,6 +723,7 @@ export class BookListComponent implements OnInit {
```
* We imported `FormGroup, FormBuilder and Validators`.
+* We added `form: FormGroup` variable.
* We injected `fb: FormBuilder` service to the constructor. The [FormBuilder](https://angular.io/api/forms/FormBuilder) service provides convenient methods for generating controls. It reduces the amount of boilerplate needed to build complex forms.
* We added `buildForm` method to the end of the file and executed `buildForm()` in the `createBook` method. This method creates a reactive form to be able to create a new book.
* The `group` method of `FormBuilder`, `fb` creates a `FormGroup`.
@@ -880,35 +800,34 @@ export class BooksModule { }
* 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:
```js
import { Component, OnInit } from '@angular/core';
-import { Store, Select } from '@ngxs/store';
-import { BooksState } from '../../store/states';
+import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
-import { Books } from '../../store/models';
-import { GetBooks } from '../../store/actions';
+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 { FormGroup, FormBuilder, Validators } from '@angular/forms';
-import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; //<== added this line ==>
+import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; // <== added this line ==>
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
- providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }] //<== added this line ==>
+ providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added this line ==>
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
- books$: Observable;
+ books$: Observable;
- booksType = Books.BookType;
+ booksType = BookType;
//added bookTypeArr array
- bookTypeArr = Object.keys(Books.BookType).filter(
- bookType => typeof this.booksType[bookType] === 'number'
+ bookTypeArr = Object.keys(BookType).filter(
+ (bookType) => typeof this.booksType[bookType] === 'number'
);
loading = false;
@@ -917,7 +836,7 @@ export class BookListComponent implements OnInit {
form: FormGroup;
- constructor(private store: Store, private fb: FormBuilder) { }
+ constructor(private store: Store, private fb: FormBuilder) {}
ngOnInit() {
this.get();
@@ -925,9 +844,10 @@ export class BookListComponent implements OnInit {
get() {
this.loading = true;
- this.store.dispatch(new GetBooks()).subscribe(() => {
- this.loading = false;
- });
+ this.store
+ .dispatch(new GetBooks())
+ .pipe(finalize(() => (this.loading = false)))
+ .subscribe(() => {});
}
createBook() {
@@ -965,35 +885,16 @@ Now, you can open your browser to see the changes:
#### Saving the book
-Open `book-list.component.html` in `app\books\book-list` folder and add the following `abp-button` to save the new book.
-
-```html
-
-
-
-
-
-
-```
-
-* This adds a save button to the bottom area of the modal:
-
-
-
Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below:
```js
import { Component, OnInit } from '@angular/core';
-import { Store, Select } from '@ngxs/store';
-import { BooksState } from '../../store/states';
+import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
-import { Books } from '../../store/models';
-import { GetBooks, CreateUpdateBook } from '../../store/actions'; //<== added CreateUpdateBook ==>
+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 { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap';
@@ -1001,16 +902,17 @@ import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap
selector: 'app-book-list',
templateUrl: './book-list.component.html',
styleUrls: ['./book-list.component.scss'],
- providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }]
+ providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }],
})
export class BookListComponent implements OnInit {
@Select(BooksState.getBooks)
- books$: Observable;
+ books$: Observable;
- booksType = Books.BookType;
+ booksType = BookType;
- bookTypeArr = Object.keys(Books.BookType).filter(
- bookType => typeof this.booksType[bookType] === 'number'
+ //added bookTypeArr array
+ bookTypeArr = Object.keys(BookType).filter(
+ (bookType) => typeof this.booksType[bookType] === 'number'
);
loading = false;
@@ -1019,7 +921,7 @@ export class BookListComponent implements OnInit {
form: FormGroup;
- constructor(private store: Store, private fb: FormBuilder) { }
+ constructor(private store: Store, private fb: FormBuilder) {}
ngOnInit() {
this.get();
@@ -1027,9 +929,10 @@ export class BookListComponent implements OnInit {
get() {
this.loading = true;
- this.store.dispatch(new GetBooks()).subscribe(() => {
- this.loading = false;
- });
+ this.store
+ .dispatch(new GetBooks())
+ .pipe(finalize(() => (this.loading = false)))
+ .subscribe(() => {});
}
createBook() {
@@ -1062,37 +965,46 @@ export class BookListComponent implements OnInit {
```
* We imported `CreateUpdateBook`.
-* We added `save` method
+* We added `save` method
-### Updating an existing book
-
-#### BooksService
+Open `book-list.component.html` in `app\books\book-list` folder and add the following `abp-button` to save the new book.
-Open the `books.service.ts` in `app\books\shared` folder and add the `getById` and `update` methods.
+```html
+
+
+
+
+
+
+```
-```js
-getById(id: string): Observable {
- return this.restService.request({
- method: 'GET',
- url: `/api/app/book/${id}`
- });
-}
+Find the `