feat: master search tool and page (#1190)

Fixes: #1189
This commit is contained in:
Karan Takalkar
2020-08-10 16:49:46 +05:30
committed by GitHub
parent a27169d8fe
commit bc57a061c4
17 changed files with 426 additions and 38 deletions
+2
View File
@@ -29,6 +29,7 @@ import { RouteReusableStrategy } from './route/route-reusable-strategy';
/** Custom Modules */
import { SharedModule } from '../shared/shared.module';
import { NotificationsModule } from '../notifications/notifications.module';
import { SearchModule } from 'app/search/search.module';
/** Custom Components */
import { ShellComponent } from './shell/shell.component';
@@ -45,6 +46,7 @@ import { ContentComponent } from './shell/content/content.component';
@NgModule({
imports: [
SharedModule,
SearchModule,
NotificationsModule,
HttpClientModule,
TranslateModule,
@@ -41,21 +41,7 @@
<span fxFlex="auto"></span>
<!--
TODO: Search
Separate into its own component in shared module for reuse.
Implement functionality.
-->
<div *ngIf="searchVisible" class="search-bar" fxFlex="auto" fxHide.lt-sm="true" [@fadeInOut]>
<mat-form-field class="search">
<mat-label>Search</mat-label>
<input matInput type="text" (focusout)="toggleSearchVisibility()">
</mat-form-field>
</div>
<button mat-icon-button class="ml-1" (click)="toggleSearchVisibility()" fxHide.lt-sm="true">
<fa-icon icon="search" size="lg"></fa-icon>
</button>
<mifosx-search-tool></mifosx-search-tool>
<mifosx-language-selector class="ml-1 language" fxHide.lt-md></mifosx-language-selector>
@@ -2,7 +2,6 @@
import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';
import { MatSidenav } from '@angular/material';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { style, animate, transition, trigger } from '@angular/animations';
import { Router } from '@angular/router';
/** rxjs Imports */
@@ -18,18 +17,7 @@ import { AuthenticationService } from '../../authentication/authentication.servi
@Component({
selector: 'mifosx-toolbar',
templateUrl: './toolbar.component.html',
styleUrls: ['./toolbar.component.scss'],
animations: [
trigger('fadeInOut', [
transition(':enter', [
style({ opacity: 0 }),
animate(500, style({ opacity: 1 }))
]),
transition(':leave', [
animate(500, style({ opacity: 0 }))
])
])
]
styleUrls: ['./toolbar.component.scss']
})
export class ToolbarComponent implements OnInit {
@@ -39,8 +27,6 @@ export class ToolbarComponent implements OnInit {
map(result => result.matches)
);
/** Sets the initial visibility of search input as hidden. Visible if true. */
searchVisible = false;
/** Sets the initial state of sidenav as collapsed. Not collapsed if false. */
sidenavCollapsed = true;
@@ -84,13 +70,6 @@ export class ToolbarComponent implements OnInit {
this.collapse.emit(this.sidenavCollapsed);
}
/**
* Toggles the visibility of search input with fadeInOut animation.
*/
toggleSearchVisibility() {
this.searchVisible = !this.searchVisible;
}
/**
* Logs out the authenticated user and redirects to login page.
*/
@@ -2,7 +2,6 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
/** Routing Imports */
import { Route } from '../core/route/route.service';
@@ -0,0 +1,22 @@
<div class="container">
<mat-card fxLayout="column" fxLayoutGap="2%">
<mat-error *ngIf="overload">Searched query resulted more than 200 records, showing first 200 records.</mat-error>
<mat-list>
<mat-list-item (click)="navigate(entity)" *ngFor="let entity of searchResults">
<h3 matLine><span class="link">{{ entity.entityName }}</span> | #{{ entity.entityAccountNo }}</h3>
<p matLine>
<span>Entity: {{ entity.entityType }}
| {{ ['CLIENT','GROUP', 'CENTER'].includes(entity.entityType) ? 'Office' : 'Client' }}: {{ entity.parentName }}
</span>
</p>
</mat-list-item>
</mat-list>
</mat-card>
</div>
@@ -0,0 +1,18 @@
mat-list {
padding-top: 0px;
}
mat-list-item {
.link {
color: blue;
text-decoration: underline;
width: fit-content;
&:hover{
cursor: pointer;
}
}
margin-block: 20px;
&:hover{
background-color: #f5f5f5;
}
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SearchPageComponent } from './search-page.component';
describe('SearchPageComponent', () => {
let component: SearchPageComponent;
let fixture: ComponentFixture<SearchPageComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SearchPageComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SearchPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,62 @@
/** Angular Imports */
import { Component } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
/**
* Search Page Component
*/
@Component({
selector: 'mifosx-search-page',
templateUrl: './search-page.component.html',
styleUrls: ['./search-page.component.scss']
})
export class SearchPageComponent {
/** Search Results */
searchResults: any;
/** Flags if number of search results exceed 200 */
overload: boolean;
/**
* @param {ActivatedRoute} route Activated Route
* @param {Router} router Router
*/
constructor(private route: ActivatedRoute,
private router: Router) {
this.route.data.subscribe(( data: { searchResults: any }) => {
this.searchResults = data.searchResults;
this.overload = this.searchResults.length > 200 ? true : false;
if (this.overload) {
this.searchResults = this.searchResults.slice(0, 200);
}
});
}
/**
* Returns link to entity view page.
* @param {any} entity Entity
*/
navigate(entity: any) {
switch (entity.entityType) {
case 'CLIENT':
this.router.navigate(['clients', entity.entityId]);
break;
case 'CENTER':
this.router.navigate(['centers', entity.entityId]);
break;
case 'GROUP':
this.router.navigate(['groups', entity.entityId]);
break;
case 'SHARE':
this.router.navigate(['clients', entity.parentId, 'sharesaccounts', entity.entityId]);
break;
case 'SAVING':
this.router.navigate(['clients', entity.parentId, 'savingsaccounts', entity.entityId]);
break;
case 'LOAN':
this.router.navigate(['clients', entity.parentId, 'loans', entity.entityId]);
break;
}
}
}
+37
View File
@@ -0,0 +1,37 @@
/** Angular Imports */
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
/** Routing Imports */
import { Route } from '../core/route/route.service';
/** Translation Imports */
import { extract } from '../core/i18n/i18n.service';
/** Custom Components */
import { SearchPageComponent } from './search-page/search-page.component';
/** Custom Resolvers */
import { SearchResolver } from './search.resolver';
/** Search Routes */
const routes: Routes = [
Route.withShell([
{
path: 'search',
component: SearchPageComponent,
data: { title: extract('Search'), breadcrumb: 'Search' },
runGuardsAndResolvers: 'paramsOrQueryParamsChange',
resolve: {
searchResults: SearchResolver
}
}
])
];
@NgModule({
imports: [RouterModule.forChild(routes)],
providers: [SearchResolver],
exports: [RouterModule]
})
export class SearchRoutingModule { }
@@ -0,0 +1,23 @@
<div *ngIf="searchVisible" class="search-bar" fxFlex="auto" fxHide.lt-sm="true" fxLayout="row" fxLayoutGap="2%" [@fadeInOut]>
<mat-form-field class="search">
<mat-label>Search</mat-label>
<input matInput type="text" [formControl]="query" (keydown.enter)="search()">
</mat-form-field>
<mat-form-field class="resource">
<mat-label>Resource</mat-label>
<mat-select [formControl]="resource">
<mat-option *ngFor="let option of resourceOptions" [value]="option.value">
{{ option.name }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="search-icon">
<button mat-icon-button (click)="toggleSearchVisibility()" fxHide.lt-sm="true">
<fa-icon icon="search" size="lg"></fa-icon>
</button>
</div>
@@ -0,0 +1,18 @@
.search-bar {
.search {
max-width: 400px;
width: 100%;
font-size: 1rem;
}
.resource {
max-width: 90px;
width: 100%;
font-size: 1rem;
}
}
.search-icon {
margin-left: 1rem;
display: flex;
align-items: center;
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SearchToolComponent } from './search-tool.component';
describe('SearchToolComponent', () => {
let component: SearchToolComponent;
let fixture: ComponentFixture<SearchToolComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SearchToolComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SearchToolComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,88 @@
/** Angular Imports */
import { Component } from '@angular/core';
import { style, animate, transition, trigger } from '@angular/animations';
import { Router } from '@angular/router';
import { FormControl } from '@angular/forms';
/**
* Search Tool Component
*/
@Component({
selector: 'mifosx-search-tool',
templateUrl: './search-tool.component.html',
styleUrls: ['./search-tool.component.scss'],
animations: [
trigger('fadeInOut', [
transition(':enter', [
style({ opacity: 0 }),
animate(500, style({ opacity: 1 }))
]),
transition(':leave', [
animate(500, style({ opacity: 0 }))
])
])
]
})
export class SearchToolComponent {
/** Query Form Control */
query = new FormControl('');
/** Resource Form Control */
resource = new FormControl('');
/** Sets the initial visibility of search input as hidden. Visible if true. */
searchVisible = false;
/** Resource Options */
resourceOptions: any[] = [
{
name: 'All',
value: 'clients,clientIdentifiers,groups,savings,shares,loans'
},
{
name: 'Clients',
value: 'clients,clientIdentifiers'
},
{
name: 'Groups',
value: 'groups'
},
{
name: 'Savings',
value: 'savings'
},
{
name: 'Shares',
value: 'shares'
},
{
name: 'Loans',
value: 'loans'
},
];
/**
* @param {Router} router Router
*/
constructor(private router: Router) {
this.resource.patchValue('clients,clientIdentifiers,groups,savings,shares,loans');
}
/**
* Toggles the visibility of search input with fadeInOut animation.
*/
toggleSearchVisibility() {
this.searchVisible = !this.searchVisible;
}
/**
* Searches server for query and resource.
*/
search() {
const queryParams: any = {
query: this.query.value,
resource: this.resource.value
};
this.router.navigate(['/search'], { queryParams: queryParams });
}
}
+25
View File
@@ -0,0 +1,25 @@
/** Angular Imports */
import { NgModule } from '@angular/core';
/** Custom Modules */
import { SharedModule } from '../shared/shared.module';
import { SearchRoutingModule } from './search-routing.module';
/** Custom Components */
import { SearchToolComponent } from './search-tool/search-tool.component';
import { SearchPageComponent } from './search-page/search-page.component';
/**
* Search Module
*/
@NgModule({
declarations: [SearchToolComponent, SearchPageComponent],
imports: [
SharedModule,
SearchRoutingModule
],
exports: [
SearchToolComponent
]
})
export class SearchModule { }
+33
View File
@@ -0,0 +1,33 @@
/** Angular Imports */
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
/** rxjs Imports */
import { Observable } from 'rxjs';
/** Custom Services */
import { SearchService } from './search.service';
/**
* Search Results data resolver.
*/
@Injectable()
export class SearchResolver implements Resolve<Object> {
/**
* @param {SearchService} searchService Notifications service.
*/
constructor(private searchService: SearchService) {}
/**
* Returns the Search Resultsdata.
* @param {ActivatedRouteSnapshot} route Route Snapshot
* @returns {Observable<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const query = route.queryParams['query'];
const resource = route.queryParams['resource'];
return this.searchService.getSearchResults(query, resource);
}
}
+12
View File
@@ -0,0 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { SearchService } from './search.service';
describe('SearchService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: SearchService = TestBed.get(SearchService);
expect(service).toBeTruthy();
});
});
+34
View File
@@ -0,0 +1,34 @@
/** Angular Imports */
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
/** rxjs Imports */
import { Observable } from 'rxjs';
/**
* Search service.
*/
@Injectable({
providedIn: 'root'
})
export class SearchService {
/**
* @param {HttpClient} http Http Client to send requests.
*/
constructor(private http: HttpClient) { }
/**
* @param {string} query Query String
* @param {string} resource Entity resource
* @returns {Observable<any>} Search Results.
*/
getSearchResults(query: string, resource: string): Observable<any> {
const httpParams = new HttpParams()
.set('exactMatch', 'false')
.set('query', query)
.set('resource', resource);
return this.http.get('/search', { params: httpParams });
}
}