From bc57a061c403b1d5a8011c425924df033b09814d Mon Sep 17 00:00:00 2001 From: Karan Takalkar Date: Mon, 10 Aug 2020 16:49:46 +0530 Subject: [PATCH] feat: master search tool and page (#1190) Fixes: #1189 --- src/app/core/core.module.ts | 2 + .../core/shell/toolbar/toolbar.component.html | 16 +--- .../core/shell/toolbar/toolbar.component.ts | 23 +---- .../notifications-routing.module.ts | 1 - .../search-page/search-page.component.html | 22 +++++ .../search-page/search-page.component.scss | 18 ++++ .../search-page/search-page.component.spec.ts | 25 ++++++ .../search-page/search-page.component.ts | 62 +++++++++++++ src/app/search/search-routing.module.ts | 37 ++++++++ .../search-tool/search-tool.component.html | 23 +++++ .../search-tool/search-tool.component.scss | 18 ++++ .../search-tool/search-tool.component.spec.ts | 25 ++++++ .../search-tool/search-tool.component.ts | 88 +++++++++++++++++++ src/app/search/search.module.ts | 25 ++++++ src/app/search/search.resolver.ts | 33 +++++++ src/app/search/search.service.spec.ts | 12 +++ src/app/search/search.service.ts | 34 +++++++ 17 files changed, 426 insertions(+), 38 deletions(-) create mode 100644 src/app/search/search-page/search-page.component.html create mode 100644 src/app/search/search-page/search-page.component.scss create mode 100644 src/app/search/search-page/search-page.component.spec.ts create mode 100644 src/app/search/search-page/search-page.component.ts create mode 100644 src/app/search/search-routing.module.ts create mode 100644 src/app/search/search-tool/search-tool.component.html create mode 100644 src/app/search/search-tool/search-tool.component.scss create mode 100644 src/app/search/search-tool/search-tool.component.spec.ts create mode 100644 src/app/search/search-tool/search-tool.component.ts create mode 100644 src/app/search/search.module.ts create mode 100644 src/app/search/search.resolver.ts create mode 100644 src/app/search/search.service.spec.ts create mode 100644 src/app/search/search.service.ts diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 8b019b05f..4969c5c90 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -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, diff --git a/src/app/core/shell/toolbar/toolbar.component.html b/src/app/core/shell/toolbar/toolbar.component.html index 32e1e470c..39e584f33 100644 --- a/src/app/core/shell/toolbar/toolbar.component.html +++ b/src/app/core/shell/toolbar/toolbar.component.html @@ -41,21 +41,7 @@ - - - - + diff --git a/src/app/core/shell/toolbar/toolbar.component.ts b/src/app/core/shell/toolbar/toolbar.component.ts index 120394fcb..5f5266716 100644 --- a/src/app/core/shell/toolbar/toolbar.component.ts +++ b/src/app/core/shell/toolbar/toolbar.component.ts @@ -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. */ diff --git a/src/app/notifications/notifications-routing.module.ts b/src/app/notifications/notifications-routing.module.ts index 927a7cacc..4f42e651d 100644 --- a/src/app/notifications/notifications-routing.module.ts +++ b/src/app/notifications/notifications-routing.module.ts @@ -2,7 +2,6 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; - /** Routing Imports */ import { Route } from '../core/route/route.service'; diff --git a/src/app/search/search-page/search-page.component.html b/src/app/search/search-page/search-page.component.html new file mode 100644 index 000000000..94d5da6c5 --- /dev/null +++ b/src/app/search/search-page/search-page.component.html @@ -0,0 +1,22 @@ +
+ + + + Searched query resulted more than 200 records, showing first 200 records. + + + + +

{{ entity.entityName }} | #{{ entity.entityAccountNo }}

+

+ Entity: {{ entity.entityType }} + | {{ ['CLIENT','GROUP', 'CENTER'].includes(entity.entityType) ? 'Office' : 'Client' }}: {{ entity.parentName }} + +

+
+ +
+ +
+ +
diff --git a/src/app/search/search-page/search-page.component.scss b/src/app/search/search-page/search-page.component.scss new file mode 100644 index 000000000..36433b509 --- /dev/null +++ b/src/app/search/search-page/search-page.component.scss @@ -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; + } +} diff --git a/src/app/search/search-page/search-page.component.spec.ts b/src/app/search/search-page/search-page.component.spec.ts new file mode 100644 index 000000000..c39a155f0 --- /dev/null +++ b/src/app/search/search-page/search-page.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SearchPageComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SearchPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/search/search-page/search-page.component.ts b/src/app/search/search-page/search-page.component.ts new file mode 100644 index 000000000..f4b4d2b66 --- /dev/null +++ b/src/app/search/search-page/search-page.component.ts @@ -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; + } + } + +} diff --git a/src/app/search/search-routing.module.ts b/src/app/search/search-routing.module.ts new file mode 100644 index 000000000..6308eda8e --- /dev/null +++ b/src/app/search/search-routing.module.ts @@ -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 { } diff --git a/src/app/search/search-tool/search-tool.component.html b/src/app/search/search-tool/search-tool.component.html new file mode 100644 index 000000000..e8011428f --- /dev/null +++ b/src/app/search/search-tool/search-tool.component.html @@ -0,0 +1,23 @@ + + +
+ +
diff --git a/src/app/search/search-tool/search-tool.component.scss b/src/app/search/search-tool/search-tool.component.scss new file mode 100644 index 000000000..430ae07dc --- /dev/null +++ b/src/app/search/search-tool/search-tool.component.scss @@ -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; +} diff --git a/src/app/search/search-tool/search-tool.component.spec.ts b/src/app/search/search-tool/search-tool.component.spec.ts new file mode 100644 index 000000000..1359ef779 --- /dev/null +++ b/src/app/search/search-tool/search-tool.component.spec.ts @@ -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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SearchToolComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SearchToolComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/search/search-tool/search-tool.component.ts b/src/app/search/search-tool/search-tool.component.ts new file mode 100644 index 000000000..227fe1d45 --- /dev/null +++ b/src/app/search/search-tool/search-tool.component.ts @@ -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 }); + } + +} diff --git a/src/app/search/search.module.ts b/src/app/search/search.module.ts new file mode 100644 index 000000000..a13eaf117 --- /dev/null +++ b/src/app/search/search.module.ts @@ -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 { } diff --git a/src/app/search/search.resolver.ts b/src/app/search/search.resolver.ts new file mode 100644 index 000000000..5ec9518cc --- /dev/null +++ b/src/app/search/search.resolver.ts @@ -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 { + + /** + * @param {SearchService} searchService Notifications service. + */ + constructor(private searchService: SearchService) {} + + /** + * Returns the Search Resultsdata. + * @param {ActivatedRouteSnapshot} route Route Snapshot + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const query = route.queryParams['query']; + const resource = route.queryParams['resource']; + return this.searchService.getSearchResults(query, resource); + } + +} diff --git a/src/app/search/search.service.spec.ts b/src/app/search/search.service.spec.ts new file mode 100644 index 000000000..5571a3261 --- /dev/null +++ b/src/app/search/search.service.spec.ts @@ -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(); + }); +}); diff --git a/src/app/search/search.service.ts b/src/app/search/search.service.ts new file mode 100644 index 000000000..34e45f717 --- /dev/null +++ b/src/app/search/search.service.ts @@ -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} Search Results. + */ + getSearchResults(query: string, resource: string): Observable { + const httpParams = new HttpParams() + .set('exactMatch', 'false') + .set('query', query) + .set('resource', resource); + return this.http.get('/search', { params: httpParams }); + } + +}