feat: add server-side pagination, sort and search to groups component (#507)
- Add server-side pagination, sort and search to groups component - Refactor code to conform to current guidelines See: #234
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
/** Angular Imports */
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { Routes, RouterModule } from '@angular/router';
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
@@ -7,11 +8,12 @@ import { Route } from '../core/route/route.service';
|
|||||||
/** Translation Imports */
|
/** Translation Imports */
|
||||||
import { extract } from '../core/i18n/i18n.service';
|
import { extract } from '../core/i18n/i18n.service';
|
||||||
|
|
||||||
|
/** Custom Imports */
|
||||||
import { GroupsComponent } from './groups.component';
|
import { GroupsComponent } from './groups.component';
|
||||||
|
|
||||||
|
/** Groups Routes */
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
Route.withShell([
|
Route.withShell([
|
||||||
|
|
||||||
{
|
{
|
||||||
path: 'groups',
|
path: 'groups',
|
||||||
component: GroupsComponent,
|
component: GroupsComponent,
|
||||||
@@ -20,6 +22,11 @@ const routes: Routes = [
|
|||||||
])
|
])
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups Routing Module
|
||||||
|
*
|
||||||
|
* Configures the groups routes.
|
||||||
|
*/
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forChild(routes)],
|
imports: [RouterModule.forChild(routes)],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
|
|||||||
@@ -1,51 +1,55 @@
|
|||||||
<div class="container m-t-40 m-b-40">
|
<mat-card class="container">
|
||||||
|
<div fxLayout="row" fxLayoutAlign="start center">
|
||||||
|
<div class="search-box">
|
||||||
|
<mat-form-field>
|
||||||
|
<input matInput placeholder="Search by Name" [formControl]="name">
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-checkbox #showClosedGroups labelPosition="after" (change)="changeShowClosedGroups()">Show Closed Groups</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="action-button" fxLayoutGap="25px">
|
||||||
|
<button mat-raised-button color="primary">
|
||||||
|
<fa-icon icon="plus"></fa-icon> Create Group
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<mat-form-field class="filter">
|
<table mat-table [dataSource]="dataSource" matSort>
|
||||||
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
|
|
||||||
</mat-form-field>
|
<ng-container matColumnDef="name">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header> Name </th>
|
||||||
<div class="mat-elevation-z8">
|
<td mat-cell *matCellDef="let group"> {{ group.name }} </td>
|
||||||
<table mat-table [dataSource]="dataSource" matSort>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Name Column -->
|
<ng-container matColumnDef="accountNo">
|
||||||
<ng-container matColumnDef="name">
|
<th mat-header-cell *matHeaderCellDef> Account # </th>
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header='name'> Name </th>
|
<td mat-cell *matCellDef="let group"> {{ group.accountNo }} </td>
|
||||||
<td mat-cell *matCellDef="let row"> {{row?.name}} </td>
|
</ng-container>
|
||||||
</ng-container>
|
|
||||||
|
<ng-container matColumnDef="externalId">
|
||||||
<!-- Name Column -->
|
<th mat-header-cell *matHeaderCellDef> ExternalID </th>
|
||||||
<ng-container matColumnDef="accountno">
|
<td mat-cell *matCellDef="let group"> {{ group.externalId }} </td>
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header='accountno'> Account # </th>
|
</ng-container>
|
||||||
<td mat-cell *matCellDef="let row"> {{row?.accountNo}} </td>
|
|
||||||
</ng-container>
|
<ng-container matColumnDef="status">
|
||||||
|
<th mat-header-cell *matHeaderCellDef> Status </th>
|
||||||
<!-- Account no Column -->
|
<td mat-cell *matCellDef="let row">
|
||||||
<ng-container matColumnDef="externalid">
|
<div [ngClass]="row.active|statusLookup">
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header='externalid'> ExternalID </th>
|
<fa-icon icon="stop"></fa-icon>
|
||||||
<td mat-cell *matCellDef="let row"> {{row?.externalId}} </td>
|
</div>
|
||||||
</ng-container>
|
</td>
|
||||||
|
</ng-container>
|
||||||
<!-- External ID Column -->
|
|
||||||
<ng-container matColumnDef="status">
|
<ng-container matColumnDef="officeName">
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header='status'> Status </th>
|
<th mat-header-cell *matHeaderCellDef mat-sort-header> Office Name </th>
|
||||||
<td mat-cell *matCellDef="let row"> <div [className]="row.active === true ? 'active' : 'inactive'">
|
<td mat-cell *matCellDef="let group"> {{ group.officeName }} </td>
|
||||||
<i class="fa fa-circle fa-2x"> </i>
|
</ng-container>
|
||||||
</div> </td>
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||||
</ng-container>
|
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [routerLink]="[ ]"></tr>
|
||||||
|
|
||||||
<!-- Status Column -->
|
</table>
|
||||||
<ng-container matColumnDef="office">
|
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header='office'> Office </th>
|
<mat-paginator [length]="dataSource?.records$ | async" [pageSize]="10" [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons></mat-paginator>
|
||||||
<td mat-cell *matCellDef="let row"> {{row?.officeName}} </td>
|
|
||||||
</ng-container>
|
</mat-card>
|
||||||
|
|
||||||
|
|
||||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
|
||||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;" [routerLink]="[ ]">
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<mat-paginator [pageSizeOptions]="[10, 25, 100]"></mat-paginator>
|
|
||||||
</div>
|
|
||||||
@@ -1,27 +1,10 @@
|
|||||||
|
.container {
|
||||||
|
.action-button{
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
.search-button{
|
||||||
width: 100%;
|
min-width: 30px;
|
||||||
|
margin: 4px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.mat-form-field {
|
|
||||||
font-size: 14px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
td, th {
|
|
||||||
width: 15%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter{
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inactive {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active {
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,113 @@
|
|||||||
import { Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
/** Angular Imports */
|
||||||
import {MatPaginator, MatSort, MatTableDataSource} from '@angular/material';
|
import { Component, OnInit, ViewChild, AfterViewInit } from '@angular/core';
|
||||||
|
import { MatPaginator, MatSort, MatCheckbox } from '@angular/material';
|
||||||
|
import { FormControl } from '@angular/forms';
|
||||||
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
/** rxjs Imports */
|
||||||
|
import { merge } from 'rxjs';
|
||||||
|
import { tap, startWith, map, distinctUntilChanged, debounceTime} from 'rxjs/operators';
|
||||||
|
|
||||||
|
/** Custom Services */
|
||||||
import { GroupsService } from './groups.service';
|
import { GroupsService } from './groups.service';
|
||||||
import {DataSource} from '@angular/cdk/collections';
|
|
||||||
import { Observable } from 'rxjs';
|
/** Custom Data Source */
|
||||||
|
import { GroupsDataSource } from './groups.datasource';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups component.
|
||||||
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'mifosx-app-groups',
|
selector: 'mifosx-app-groups',
|
||||||
templateUrl: './groups.component.html',
|
templateUrl: './groups.component.html',
|
||||||
styleUrls: ['./groups.component.scss']
|
styleUrls: ['./groups.component.scss']
|
||||||
})
|
})
|
||||||
export class GroupsComponent implements OnInit {
|
export class GroupsComponent implements OnInit, AfterViewInit {
|
||||||
|
@ViewChild('showClosedGroups') showClosedGroups: MatCheckbox;
|
||||||
|
|
||||||
private ELEMENT_DATA: any = undefined;
|
/** Name form control. */
|
||||||
displayedColumns = ['name', 'accountno', 'externalid', 'status', 'office'];
|
name = new FormControl();
|
||||||
dataSource = new MatTableDataSource();
|
/** Columns to be displayed in groups table. */
|
||||||
|
displayedColumns = ['name', 'accountNo', 'externalId', 'status', 'officeName'];
|
||||||
|
/** Data source for groups table. */
|
||||||
|
dataSource: GroupsDataSource;
|
||||||
|
/** Groups filter. */
|
||||||
|
filterGroupsBy = [
|
||||||
|
{
|
||||||
|
type: 'name',
|
||||||
|
value: ''
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Paginator for groups table. */
|
||||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||||
|
/** Sorter for groups table. */
|
||||||
@ViewChild(MatSort) sort: MatSort;
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
constructor(private groupService: GroupsService) { }
|
|
||||||
|
constructor(private groupsService: GroupsService) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.groupService.getGroups()
|
this.getGroups();
|
||||||
.subscribe(
|
}
|
||||||
(res => {
|
|
||||||
console.log(res);
|
/**
|
||||||
// console.log(res.active);
|
* Subscribes to all search filters:
|
||||||
res.active = !!res.active;
|
* Name
|
||||||
this.dataSource = new MatTableDataSource(res);
|
* sort change and page change.
|
||||||
this.dataSource.paginator = this.paginator;
|
*/
|
||||||
this.dataSource.sort = this.sort;
|
ngAfterViewInit() {
|
||||||
|
|
||||||
|
this.name.valueChanges
|
||||||
|
.pipe(
|
||||||
|
debounceTime(500),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
tap((filterValue) => {
|
||||||
|
this.applyFilter(filterValue, 'name');
|
||||||
})
|
})
|
||||||
);
|
)
|
||||||
|
.subscribe();
|
||||||
|
|
||||||
|
this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);
|
||||||
|
|
||||||
|
merge(this.sort.sortChange, this.paginator.page)
|
||||||
|
.pipe(
|
||||||
|
tap(() => this.loadGroupsPage())
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changeShowClosedGroups() {
|
||||||
|
this.loadGroupsPage();
|
||||||
|
}
|
||||||
|
|
||||||
applyFilter(filterValue: string) {
|
/**
|
||||||
filterValue = filterValue.trim(); // Remove whitespace
|
* Loads a page of groups.
|
||||||
filterValue = filterValue.toLowerCase(); // Datasource defaults to lowercase matches
|
*/
|
||||||
this.dataSource.filter = filterValue;
|
loadGroupsPage() {
|
||||||
if (this.dataSource.paginator) {
|
if (!this.sort.direction) {
|
||||||
this.dataSource.paginator.firstPage();
|
delete this.sort.active;
|
||||||
}
|
}
|
||||||
|
this.dataSource.getGroups(this.filterGroupsBy, this.sort.active, this.sort.direction, this.paginator.pageIndex, this.paginator.pageSize, !this.showClosedGroups.checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters data in groups table based on passed value and poperty.
|
||||||
|
* @param {string} filterValue Value to filter data.
|
||||||
|
* @param {string} property Property to filter data by.
|
||||||
|
*/
|
||||||
|
applyFilter(filterValue: string, property: string) {
|
||||||
|
this.paginator.pageIndex = 0;
|
||||||
|
const findIndex = this.filterGroupsBy.findIndex(filter => filter.type === property);
|
||||||
|
this.filterGroupsBy[findIndex].value = filterValue;
|
||||||
|
this.loadGroupsPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the data source for groups table and loads the first page.
|
||||||
|
*/
|
||||||
|
getGroups() {
|
||||||
|
this.dataSource = new GroupsDataSource(this.groupsService);
|
||||||
|
this.dataSource.getGroups(this.filterGroupsBy, this.sort.active, this.sort.direction, this.paginator.pageIndex, this.paginator.pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/** Angular Imports */
|
||||||
|
import { CollectionViewer, DataSource } from '@angular/cdk/collections';
|
||||||
|
|
||||||
|
/** rxjs Imports */
|
||||||
|
import { Observable, BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
|
/** Custom Services */
|
||||||
|
import { GroupsService } from './groups.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups custom data source to implement server side filtering, pagination and sorting.
|
||||||
|
*/
|
||||||
|
export class GroupsDataSource implements DataSource<any> {
|
||||||
|
|
||||||
|
/** groups behavior subject to represent loaded groups page. */
|
||||||
|
private groupsSubject = new BehaviorSubject<any[]>([]);
|
||||||
|
/** Records subject to represent total number of filtered groups records. */
|
||||||
|
private recordsSubject = new BehaviorSubject<number>(0);
|
||||||
|
/** Records observable which can be subscribed to get the value of total number of filtered groups records. */
|
||||||
|
public records$ = this.recordsSubject.asObservable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GroupsService} groupsService Groups Service
|
||||||
|
*/
|
||||||
|
constructor(private groupsService: GroupsService) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets groups on the basis of provided parameters and emits the value.
|
||||||
|
* @param {any} filterBy Properties by which entries should be filtered.
|
||||||
|
* @param {string} orderBy Property by which entries should be sorted.
|
||||||
|
* @param {string} sortOrder Sort order: ascending or descending.
|
||||||
|
* @param {number} pageIndex Page number.
|
||||||
|
* @param {number} limit Number of entries within the page.
|
||||||
|
* @param {boolean} groupActive Specify whether to only filter active groups.
|
||||||
|
*/
|
||||||
|
getGroups(filterBy: any, orderBy: string = '', sortOrder: string = '', pageIndex: number = 0, limit: number = 10, groupActive: boolean = true) {
|
||||||
|
this.groupsSubject.next([]);
|
||||||
|
this.groupsService.getGroups(filterBy, orderBy, sortOrder, pageIndex * limit, limit)
|
||||||
|
.subscribe((groups: any) => {
|
||||||
|
groups.pageItems = (groupActive) ? (groups.pageItems.filter((group: any) => group.active)) : groups.pageItems;
|
||||||
|
this.recordsSubject.next(groups.totalFilteredRecords);
|
||||||
|
this.groupsSubject.next(groups.pageItems);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {CollectionViewer} collectionViewer
|
||||||
|
*/
|
||||||
|
connect(collectionViewer: CollectionViewer): Observable<any[]> {
|
||||||
|
return this.groupsSubject.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {CollectionViewer} collectionViewer
|
||||||
|
*/
|
||||||
|
disconnect(collectionViewer: CollectionViewer): void {
|
||||||
|
this.groupsSubject.complete();
|
||||||
|
this.recordsSubject.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,23 +1,29 @@
|
|||||||
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
/** Angular Imports */
|
||||||
import { CommonModule } from '@angular/common';
|
import { NgModule } from '@angular/core';
|
||||||
import { CoreModule } from '../core/core.module';
|
import { DatePipe } from '@angular/common';
|
||||||
import { MatTableModule } from '@angular/material/table';
|
|
||||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
|
||||||
|
|
||||||
|
/** Custom Modules */
|
||||||
|
import { GroupsRoutingModule } from './groups-routing.module';
|
||||||
|
import { SharedModule } from 'app/shared/shared.module';
|
||||||
|
import { PipesModule } from '../pipes/pipes.module';
|
||||||
|
|
||||||
|
/** Custom Components */
|
||||||
import { GroupsComponent } from './groups.component';
|
import { GroupsComponent } from './groups.component';
|
||||||
import { GroupsRoutingModule } from 'app/groups/groups-routing.module';
|
|
||||||
import { GroupsService } from './groups.service';
|
/**
|
||||||
|
* Groups Module
|
||||||
|
*
|
||||||
|
* All components related to Groups should be declared here.
|
||||||
|
*/
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
SharedModule,
|
||||||
GroupsRoutingModule,
|
GroupsRoutingModule,
|
||||||
CoreModule,
|
PipesModule
|
||||||
MatTableModule,
|
|
||||||
FlexLayoutModule
|
|
||||||
],
|
],
|
||||||
declarations: [GroupsComponent],
|
declarations: [
|
||||||
schemas: [
|
GroupsComponent
|
||||||
CUSTOM_ELEMENTS_SCHEMA
|
],
|
||||||
]
|
providers: [DatePipe]
|
||||||
})
|
})
|
||||||
export class GroupsModule { }
|
export class GroupsModule { }
|
||||||
|
|||||||
@@ -1,17 +1,45 @@
|
|||||||
|
/** Angular Imports */
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
|
||||||
|
/** rxjs Imports */
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups service.
|
||||||
|
*/
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
export class GroupsService {
|
export class GroupsService {
|
||||||
constructor(private http: HttpClient) { }
|
/**
|
||||||
|
* @param {HttpClient} http Http Client to send requests.
|
||||||
|
*/
|
||||||
|
constructor(private http: HttpClient) { }
|
||||||
|
|
||||||
getGroups(): Observable<any> {
|
/**
|
||||||
return this.http.get('/groups');
|
* @param {any} filterBy Properties by which entries should be filtered.
|
||||||
}
|
* @param {string} orderBy Property by which entries should be sorted.
|
||||||
|
* @param {string} sortOrder Sort order: ascending or descending.
|
||||||
|
* @param {number} offset Page offset.
|
||||||
|
* @param {number} limit Number of entries within the page.
|
||||||
|
* @returns {Observable<any>} Groups.
|
||||||
|
*/
|
||||||
|
getGroups(filterBy: any, orderBy: string, sortOrder: string, offset: number, limit: number): Observable<any> {
|
||||||
|
let httpParams = new HttpParams()
|
||||||
|
.set('offset', offset.toString())
|
||||||
|
.set('limit', limit.toString())
|
||||||
|
.set('sortOrder', sortOrder)
|
||||||
|
.set('orderBy', orderBy)
|
||||||
|
.set('paged', 'true');
|
||||||
|
// filterBy: name
|
||||||
|
filterBy.forEach(function (filter: any) {
|
||||||
|
if (filter.value) {
|
||||||
|
console.log(filter.value);
|
||||||
|
httpParams = httpParams.set(filter.type, filter.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return this.http.get('/groups', { params: httpParams });
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user