mirror of https://github.com/abpframework/abp.git
18 changed files with 310 additions and 219 deletions
@ -0,0 +1,102 @@ |
|||||
|
<div id="wrapper" class="card"> |
||||
|
<div class="card-header"> |
||||
|
<div class="row"> |
||||
|
<div class="col col-md-6"> |
||||
|
<h5 class="card-title"> |
||||
|
Books |
||||
|
</h5> |
||||
|
</div> |
||||
|
<div class="text-right col col-md-6"> |
||||
|
<button id="create-role" class="btn btn-primary" type="button" (click)="onAdd()"> |
||||
|
<i class="fa fa-plus mr-1"></i> <span>New book</span> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="card-body"> |
||||
|
<p-table [value]="books$ | async" [loading]="loading" [paginator]="true" [rows]="10"> |
||||
|
<ng-template pTemplate="header"> |
||||
|
<tr> |
||||
|
<th>Actions</th> |
||||
|
<th>Book name</th> |
||||
|
<th>Book type</th> |
||||
|
<th>Publish date</th> |
||||
|
<th>Price</th> |
||||
|
</tr> |
||||
|
</ng-template> |
||||
|
<ng-template pTemplate="body" let-data> |
||||
|
<tr> |
||||
|
<td> |
||||
|
<div ngbDropdown class="d-inline-block"> |
||||
|
<button |
||||
|
class="btn btn-primary btn-sm dropdown-toggle" |
||||
|
data-toggle="dropdown" |
||||
|
aria-haspopup="true" |
||||
|
ngbDropdownToggle |
||||
|
> |
||||
|
<i class="fa fa-cog mr-1"></i>Actions |
||||
|
</button> |
||||
|
<div ngbDropdownMenu> |
||||
|
<button ngbDropdownItem (click)="onEdit(data.id)">Edit</button> |
||||
|
<button ngbDropdownItem (click)="delete(data.id, data.name)"> |
||||
|
Delete |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</td> |
||||
|
<td>{{ data.name }}</td> |
||||
|
<td>{{ booksType[data.type] }}</td> |
||||
|
<td>{{ data.publishDate | date }}</td> |
||||
|
<td>{{ data.price }}</td> |
||||
|
</tr> |
||||
|
</ng-template> |
||||
|
</p-table> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<abp-modal [(visible)]="isModalOpen"> |
||||
|
<ng-template #abpHeader> |
||||
|
<h3>{{ selectedBook.id ? 'Edit' : 'New Book' }}</h3> |
||||
|
</ng-template> |
||||
|
|
||||
|
<ng-template #abpBody> |
||||
|
<form [formGroup]="form"> |
||||
|
<div class="form-group"> |
||||
|
<label for="book-name">Name</label><span> * </span> |
||||
|
<input type="text" id="book-name" class="form-control" formControlName="name" autofocus /> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<label for="book-price">Price</label><span> * </span> |
||||
|
<input type="number" id="book-price" class="form-control" formControlName="price" /> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<label for="book-type">Type</label><span> * </span> |
||||
|
<select class="form-control" id="book-type" formControlName="type"> |
||||
|
<option [ngValue]="null">Select a book type</option> |
||||
|
<option [ngValue]="booksType[type]" *ngFor="let type of bookTypeArr"> {{ type }}</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<label>Publish date</label><span> * </span> |
||||
|
<input |
||||
|
#datepicker="ngbDatepicker" |
||||
|
class="form-control" |
||||
|
name="datepicker" |
||||
|
formControlName="publishDate" |
||||
|
ngbDatepicker |
||||
|
(click)="datepicker.toggle()" |
||||
|
/> |
||||
|
</div> |
||||
|
</form> |
||||
|
</ng-template> |
||||
|
|
||||
|
<ng-template #abpFooter> |
||||
|
<button type="button" class="btn btn-secondary" #abpClose> |
||||
|
Cancel |
||||
|
</button> |
||||
|
<abp-button iconClass="fa fa-check" (click)="save()">Save</abp-button> |
||||
|
</ng-template> |
||||
|
</abp-modal> |
||||
@ -0,0 +1,25 @@ |
|||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; |
||||
|
|
||||
|
import { BookListComponent } from './book-list.component'; |
||||
|
|
||||
|
describe('BookListComponent', () => { |
||||
|
let component: BookListComponent; |
||||
|
let fixture: ComponentFixture<BookListComponent>; |
||||
|
|
||||
|
beforeEach(async(() => { |
||||
|
TestBed.configureTestingModule({ |
||||
|
declarations: [ BookListComponent ] |
||||
|
}) |
||||
|
.compileComponents(); |
||||
|
})); |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
fixture = TestBed.createComponent(BookListComponent); |
||||
|
component = fixture.componentInstance; |
||||
|
fixture.detectChanges(); |
||||
|
}); |
||||
|
|
||||
|
it('should create', () => { |
||||
|
expect(component).toBeTruthy(); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,91 @@ |
|||||
|
import { Component, OnInit } from '@angular/core'; |
||||
|
import { Store, Select } from '@ngxs/store'; |
||||
|
import { BooksState } from '../../store/states'; |
||||
|
import { Observable } from 'rxjs'; |
||||
|
import { Books } from '../../store/models'; |
||||
|
import { GetBooks, CreateUpdateBook, DeleteBook } from '../../store/actions'; |
||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
||||
|
import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; |
||||
|
import { BooksService } from '../shared/books.service'; |
||||
|
import { ConfirmationService, Toaster } from '@abp/ng.theme.shared'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-book-list', |
||||
|
templateUrl: './book-list.component.html', |
||||
|
styleUrls: ['./book-list.component.scss'], |
||||
|
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], |
||||
|
}) |
||||
|
export class BookListComponent implements OnInit { |
||||
|
@Select(BooksState.getBooks) |
||||
|
books$: Observable<Books.Book[]>; |
||||
|
|
||||
|
booksType = Books.BookType; |
||||
|
|
||||
|
loading = false; |
||||
|
|
||||
|
isModalOpen = false; |
||||
|
|
||||
|
form: FormGroup; |
||||
|
|
||||
|
bookTypeArr = Object.keys(Books.BookType).filter(bookType => typeof this.booksType[bookType] === 'number'); |
||||
|
|
||||
|
selectedBook = {} as Books.Book; |
||||
|
|
||||
|
constructor( |
||||
|
private store: Store, |
||||
|
private fb: FormBuilder, |
||||
|
private booksService: BooksService, |
||||
|
private confirmationService: ConfirmationService, |
||||
|
) {} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.loading = true; |
||||
|
this.store.dispatch(new GetBooks()).subscribe(() => { |
||||
|
this.loading = false; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
buildForm() { |
||||
|
this.form = this.fb.group({ |
||||
|
name: [this.selectedBook.name || '', Validators.required], |
||||
|
type: this.selectedBook.type || null, |
||||
|
publishDate: this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null, |
||||
|
price: this.selectedBook.price || null, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
onAdd() { |
||||
|
this.selectedBook = {} as Books.Book; |
||||
|
this.buildForm(); |
||||
|
this.isModalOpen = true; |
||||
|
} |
||||
|
|
||||
|
onEdit(id: string) { |
||||
|
this.booksService.getById(id).subscribe(book => { |
||||
|
this.selectedBook = book; |
||||
|
this.buildForm(); |
||||
|
this.isModalOpen = true; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
save() { |
||||
|
if (this.form.invalid) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.store.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id)).subscribe(() => { |
||||
|
this.isModalOpen = false; |
||||
|
this.form.reset(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
delete(id: string, name: string) { |
||||
|
this.confirmationService |
||||
|
.error(`${name} will be deleted. Do you confirm that?`, 'Are you sure?') |
||||
|
.subscribe(status => { |
||||
|
if (status === Toaster.Status.confirm) { |
||||
|
this.store.dispatch(new DeleteBook(id)); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -1,108 +1 @@ |
|||||
<div id="wrapper" class="card"> |
<router-outlet></router-outlet> |
||||
<div class="card-header"> |
|
||||
<div class="row"> |
|
||||
<div class="col col-md-6"> |
|
||||
<h5 class="card-title"> |
|
||||
Books |
|
||||
</h5> |
|
||||
</div> |
|
||||
<div class="text-right col col-md-6"> |
|
||||
<button id="create-role" class="btn btn-primary" type="button" (click)="onAdd()"> |
|
||||
<i class="fa fa-plus mr-1"></i> <span>New book</span> |
|
||||
</button> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div class="card-body"> |
|
||||
<p-table [value]="books$ | async" [loading]="loading" [paginator]="true" [rows]="10"> |
|
||||
<ng-template pTemplate="header"> |
|
||||
<tr> |
|
||||
<th>Actions</th> |
|
||||
<th>Book name</th> |
|
||||
<th>Book type</th> |
|
||||
<th>Publish date</th> |
|
||||
<th>Price</th> |
|
||||
</tr> |
|
||||
</ng-template> |
|
||||
<ng-template pTemplate="body" let-data> |
|
||||
<tr> |
|
||||
<td> |
|
||||
<div ngbDropdown class="d-inline-block"> |
|
||||
<button |
|
||||
class="btn btn-primary btn-sm dropdown-toggle" |
|
||||
data-toggle="dropdown" |
|
||||
aria-haspopup="true" |
|
||||
ngbDropdownToggle |
|
||||
> |
|
||||
<i class="fa fa-cog mr-1"></i>Actions |
|
||||
</button> |
|
||||
<div ngbDropdownMenu> |
|
||||
<button ngbDropdownItem (click)="onEdit(data.id)">Edit</button> |
|
||||
<!-- <button ngbDropdownItem (click)="delete(data.id, data.name)"> |
|
||||
Delete |
|
||||
</button> --> |
|
||||
</div> |
|
||||
</div> |
|
||||
</td> |
|
||||
<td>{{ data.name }}</td> |
|
||||
<td>{{ bookType[data.type] }}</td> |
|
||||
<td>{{ data.publishDate | date }}</td> |
|
||||
<td>{{ data.price }}</td> |
|
||||
</tr> |
|
||||
</ng-template> |
|
||||
</p-table> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<abp-modal [(visible)]="isModalOpen"> |
|
||||
<ng-template #abpHeader> |
|
||||
<h3>{{ selectedBook?.id ? 'Edit' : 'New Book' }}</h3> |
|
||||
</ng-template> |
|
||||
|
|
||||
<ng-template #abpBody> |
|
||||
<form [formGroup]="form"> |
|
||||
<div class="form-group"> |
|
||||
<label for="book-name">Name</label><span> * </span> |
|
||||
<input type="text" id="book-name" class="form-control" formControlName="name" autofocus /> |
|
||||
</div> |
|
||||
|
|
||||
<div class="form-group"> |
|
||||
<label for="book-price">Price</label> |
|
||||
<input type="number" id="book-price" class="form-control" formControlName="price" /> |
|
||||
</div> |
|
||||
|
|
||||
<div class="form-group"> |
|
||||
<label for="book-type">Type</label> |
|
||||
<select class="form-control" id="book-type" formControlName="type"> |
|
||||
<option [ngValue]="null">Select a book type</option> |
|
||||
<option [ngValue]="bookType[type]" *ngFor="let type of bookTypes"> {{ type }}</option> |
|
||||
</select> |
|
||||
</div> |
|
||||
|
|
||||
<div class="form-group"> |
|
||||
<label>Publish date</label> |
|
||||
<input |
|
||||
#datepicker="ngbDatepicker" |
|
||||
class="form-control" |
|
||||
name="datepicker" |
|
||||
formControlName="publishDate" |
|
||||
ngbDatepicker |
|
||||
(click)="datepicker.toggle()" |
|
||||
/> |
|
||||
</div> |
|
||||
</form> |
|
||||
</ng-template> |
|
||||
|
|
||||
<ng-template #abpFooter> |
|
||||
<button type="button" class="btn btn-secondary" #abpClose> |
|
||||
Cancel |
|
||||
</button> |
|
||||
<abp-button |
|
||||
[requestType]="['POST', 'PUT']" |
|
||||
requestURLContainSearchValue="book" |
|
||||
iconClass="fa fa-check" |
|
||||
(click)="save()" |
|
||||
>Save</abp-button |
|
||||
> |
|
||||
</ng-template> |
|
||||
</abp-modal> |
|
||||
|
|||||
@ -1,73 +1,8 @@ |
|||||
import { Component, OnInit } from '@angular/core'; |
import { Component } from '@angular/core'; |
||||
import { Store, Select } from '@ngxs/store'; |
|
||||
import { BooksState } from '../store/states'; |
|
||||
import { Observable } from 'rxjs'; |
|
||||
import { Books } from '../store/models'; |
|
||||
import { BooksGet, BooksSave } from '../store/actions'; |
|
||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; |
|
||||
import { NgbDateAdapter, NgbDateNativeAdapter } from '@ng-bootstrap/ng-bootstrap'; |
|
||||
import { BooksService } from './services/books.service'; |
|
||||
|
|
||||
@Component({ |
@Component({ |
||||
selector: 'app-books', |
selector: 'app-books', |
||||
templateUrl: './books.component.html', |
templateUrl: './books.component.html', |
||||
styleUrls: ['./books.component.scss'], |
styleUrls: ['./books.component.scss'], |
||||
providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], |
|
||||
}) |
}) |
||||
export class BooksComponent implements OnInit { |
export class BooksComponent {} |
||||
@Select(BooksState.getBooks) |
|
||||
books$: Observable<Books.Item[]>; |
|
||||
|
|
||||
bookType = Books.Type; |
|
||||
|
|
||||
bookTypes = Object.keys(Books.Type).filter(bookType => typeof this.bookType[bookType] === 'number'); |
|
||||
|
|
||||
loading = false; |
|
||||
|
|
||||
isModalOpen = false; |
|
||||
|
|
||||
form: FormGroup; |
|
||||
|
|
||||
selectedBook = {} as Books.Item; |
|
||||
|
|
||||
constructor(private store: Store, private fb: FormBuilder, private booksService: BooksService) {} |
|
||||
|
|
||||
ngOnInit() { |
|
||||
this.loading = true; |
|
||||
this.store.dispatch(new BooksGet()).subscribe(() => (this.loading = false)); |
|
||||
} |
|
||||
|
|
||||
buildForm() { |
|
||||
this.form = this.fb.group({ |
|
||||
name: [this.selectedBook.name || '', Validators.required], |
|
||||
type: this.selectedBook.type || null, |
|
||||
publishDate: this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null, |
|
||||
price: this.selectedBook.price || null, |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
onAdd() { |
|
||||
this.selectedBook = {} as Books.Item; |
|
||||
this.buildForm(); |
|
||||
this.isModalOpen = true; |
|
||||
} |
|
||||
|
|
||||
onEdit(id: string) { |
|
||||
this.booksService.getById(id).subscribe(book => { |
|
||||
this.selectedBook = book; |
|
||||
this.buildForm(); |
|
||||
this.isModalOpen = true; |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
save() { |
|
||||
if (this.form.invalid) { |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
this.store.dispatch(new BooksSave(this.form.value, this.selectedBook.id)).subscribe(() => { |
|
||||
this.isModalOpen = false; |
|
||||
this.form.reset(); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
|
|||||
@ -1,10 +1,15 @@ |
|||||
import { Books } from '../models'; |
import { Books } from '../models'; |
||||
|
|
||||
export class BooksGet { |
export class GetBooks { |
||||
static readonly type = '[Books] Get'; |
static readonly type = '[Books] Get'; |
||||
} |
} |
||||
|
|
||||
export class BooksSave { |
export class CreateUpdateBook { |
||||
static readonly type = '[Books] Save'; |
static readonly type = '[Books] Create Update Book'; |
||||
constructor(public payload: Books.SaveRequest, public id?: string) {} |
constructor(public payload: Books.CreateUpdateBookInput, public id?: string) {} |
||||
|
} |
||||
|
|
||||
|
export class DeleteBook { |
||||
|
static readonly type = '[Books] Delete'; |
||||
|
constructor(public id: string) {} |
||||
} |
} |
||||
|
|||||
@ -1,36 +1,47 @@ |
|||||
import { State, Action, StateContext, Selector } from '@ngxs/store'; |
import { State, Action, StateContext, Selector } from '@ngxs/store'; |
||||
import { BooksGet, BooksSave } from '../actions/books.actions'; |
|
||||
import { Books } from '../models/books'; |
import { Books } from '../models/books'; |
||||
import { BooksService } from '../../books/services/books.service'; |
import { BooksService } from '../../books/shared/books.service'; |
||||
import { tap, switchMap } from 'rxjs/operators'; |
import { tap, switchMap } from 'rxjs/operators'; |
||||
|
import { GetBooks, CreateUpdateBook, DeleteBook } from '../actions/books.actions'; |
||||
|
|
||||
@State<Books.State>({ |
@State<Books.State>({ |
||||
name: 'BooksState', |
name: 'BooksState', |
||||
defaults: { data: {} } as Books.State, |
defaults: { books: {} } as Books.State, |
||||
}) |
}) |
||||
export class BooksState { |
export class BooksState { |
||||
@Selector() |
@Selector() |
||||
static getBooks({ data }: Books.State) { |
static getBooks({ books }: Books.State) { |
||||
return data.items || []; |
return books.items || []; |
||||
} |
} |
||||
|
|
||||
constructor(private booksService: BooksService) {} |
constructor(private booksService: BooksService) {} |
||||
|
|
||||
@Action(BooksGet) |
@Action(GetBooks) |
||||
getBooks({ patchState }: StateContext<Books.State>) { |
get({ patchState }: StateContext<Books.State>) { |
||||
return this.booksService.get().pipe( |
return this.booksService.get().pipe( |
||||
tap(data => { |
tap(books => { |
||||
patchState({ |
patchState({ |
||||
data, |
books, |
||||
}); |
}); |
||||
}), |
}), |
||||
); |
); |
||||
} |
} |
||||
|
|
||||
@Action(BooksSave) |
@Action(CreateUpdateBook) |
||||
addBooks({ dispatch }: StateContext<Books.State>, { payload, id }: BooksSave) { |
save({ dispatch }: StateContext<Books.State>, { payload, id }: CreateUpdateBook) { |
||||
return (id ? this.booksService.update(payload, id) : this.booksService.add(payload)).pipe( |
let request; |
||||
switchMap(() => dispatch(new BooksGet())), |
|
||||
); |
if (id) { |
||||
|
request = this.booksService.update(payload, id); |
||||
|
} else { |
||||
|
request = this.booksService.create(payload); |
||||
|
} |
||||
|
|
||||
|
return request.pipe(switchMap(() => dispatch(new GetBooks()))); |
||||
|
} |
||||
|
|
||||
|
@Action(DeleteBook) |
||||
|
delete({ dispatch }: StateContext<Books.State>, { id }: DeleteBook) { |
||||
|
return this.booksService.delete(id).pipe(switchMap(() => dispatch(new GetBooks()))); |
||||
} |
} |
||||
} |
} |
||||
|
|||||
Loading…
Reference in new issue