Implement global search part

This commit is contained in:
Steven Zou 2017-02-19 22:07:56 +08:00
parent b7ce474ad8
commit 5e48c0fabb
14 changed files with 274 additions and 18 deletions

View File

@ -10,6 +10,7 @@ import { GlobalSearchComponent } from './global-search/global-search.component';
import { FooterComponent } from './footer/footer.component';
import { HarborShellComponent } from './harbor-shell/harbor-shell.component';
import { AccountSettingsModalComponent } from './account-settings/account-settings-modal.component';
import { SearchResultComponent } from './global-search/search-result.component';
import { BaseRoutingModule } from './base-routing.module';
@ -26,7 +27,8 @@ import { BaseRoutingModule } from './base-routing.module';
GlobalSearchComponent,
FooterComponent,
HarborShellComponent,
AccountSettingsModalComponent
AccountSettingsModalComponent,
SearchResultComponent
],
exports: [ HarborShellComponent ]
})

View File

@ -1,5 +1,5 @@
<form class="search">
<label for="search_input">
<input id="search_input" type="text" placeholder="Search Harbor...">
<input #globalSearchBox id="search_input" type="text" (keyup)="search(globalSearchBox.value)" placeholder="Search Harbor...">
</label>
</form>

View File

@ -1,10 +1,44 @@
import { Component} from '@angular/core';
import { Component, Output, EventEmitter, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
import { SearchEvent } from '../search-event';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
const deBounceTime = 500; //ms
@Component({
selector: 'global-search',
templateUrl: "global-search.component.html"
})
export class GlobalSearchComponent{
// constructor(private router: Router){}
export class GlobalSearchComponent implements OnInit {
//Publish search event to parent
@Output() searchEvt = new EventEmitter<SearchEvent>();
//Keep search term as Subject
private searchTerms = new Subject<string>();
//Implement ngOnIni
ngOnInit(): void {
this.searchTerms
.debounceTime(deBounceTime)
.distinctUntilChanged()
.subscribe(term => {
this.searchEvt.emit({
term: term
});
});
}
//Handle the term inputting event
search(term: string): void {
//Send event only when term is not empty
let nextTerm = term.trim();
if (nextTerm != "") {
this.searchTerms.next(nextTerm);
}
}
}

View File

@ -0,0 +1,41 @@
import { Injectable } from '@angular/core';
import { Headers, Http, RequestOptions } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { SearchResults } from './search-results';
const searchEndpoint = "/api/search";
/**
* Declare service to handle the global search
*
*
* @export
* @class GlobalSearchService
*/
@Injectable()
export class GlobalSearchService {
private headers = new Headers({
"Content-Type": 'application/json'
});
private options = new RequestOptions({
headers: this.headers
});
constructor(private http: Http) { }
/**
* Search related artifacts with the provided keyword
*
* @param {string} keyword
* @returns {Promise<SearchResults>}
*
* @memberOf GlobalSearchService
*/
doSearch(term: string): Promise<SearchResults> {
let searchUrl = searchEndpoint + "?q=" + term;
return this.http.get(searchUrl, this.options).toPromise()
.then(response => response.json() as SearchResults)
.catch(error => error);
}
}

View File

@ -0,0 +1,39 @@
.search-overlay {
display: block;
position: absolute;
height: 94%;
width: 97%;
/*shoud be lesser than 1000 to aoivd override the popup menu*/
z-index: 999;
box-sizing: border-box;
background: #fafafa;
}
.search-header {
display: inline-block;
width: 100%;
position: relative;
}
.search-title {
margin-top: 0px;
font-size: 28px;
letter-spacing: normal;
color: #000;
}
.search-close {
position: absolute;
right: 24px;
cursor: pointer;
}
.search-parent-override {
position: relative !important;
}
.search-spinner {
top: 50%;
left: 50%;
position: absolute;
}

View File

@ -0,0 +1,11 @@
<div class="search-overlay" *ngIf="state">
<div class="search-header">
<span class="search-title">Search results</span>
<span class="search-close" (mouseover)="mouseAction(true)" (mouseout)="mouseAction(false)">
<clr-icon shape="close" [class.is-highlight]="hover" size="36" (click)="close()"></clr-icon>
</span>
</div>
<!-- spinner -->
<div class="spinner spinner-lg search-spinner" [hidden]="done">Search...</div>
<div>Results is showing here!</div>
</div>

View File

@ -0,0 +1,80 @@
import { Component, Output, EventEmitter } from '@angular/core';
import { GlobalSearchService } from './global-search.service';
import { SearchResults } from './search-results';
@Component({
selector: "search-result",
templateUrl: "search-result.component.html",
styleUrls: ["search-result.component.css"],
providers: [GlobalSearchService]
})
export class SearchResultComponent {
@Output() closeEvt = new EventEmitter<boolean>();
searchResults: SearchResults;
//Open or close
private stateIndicator: boolean = false;
//Search in progress
private onGoing: boolean = true;
//Whether or not mouse point is onto the close indicator
private mouseOn: boolean = false;
constructor(private search: GlobalSearchService) { }
public get state(): boolean {
return this.stateIndicator;
}
public get done(): boolean {
return !this.onGoing;
}
public get hover(): boolean {
return this.mouseOn;
}
//Handle mouse event of close indicator
mouseAction(over: boolean): void {
this.mouseOn = over;
}
//Show the results
show(): void {
this.stateIndicator = true;
}
//Close the result page
close(): void {
//Tell shell close
this.closeEvt.emit(true);
this.stateIndicator = false;
}
//Call search service to complete the search request
doSearch(term: string): void {
//Confirm page is displayed
if (!this.stateIndicator) {
this.show();
}
//Show spinner
this.onGoing = true;
this.search.doSearch(term)
.then(searchResults => {
this.onGoing = false;
this.searchResults = searchResults;
console.info(searchResults);
})
.catch(error => {
this.onGoing = false;
console.error(error);//TODO: Use general erro handler
});
}
}

View File

@ -0,0 +1,7 @@
import { Project } from '../../project/project';
import { Repository } from '../../repository/repository';
export class SearchResults {
projects: Project[];
repositories: Repository[];
}

View File

@ -0,0 +1,7 @@
.side-nav-override {
box-shadow: 6px 0px 0px 0px #ccc;
}
.container-override {
position: relative !important;
}

View File

@ -1,11 +1,13 @@
<clr-main-container>
<navigator (showAccountSettingsModal)="openModal($event)"></navigator>
<navigator (showAccountSettingsModal)="openModal($event)" (searchEvt)="doSearch($event)"></navigator>
<global-message></global-message>
<div class="content-container">
<div class="content-area">
<div class="content-area" [class.container-override]="showSearch">
<!-- Only appear when searching -->
<search-result (closeEvt)="searchClose($event)"></search-result>
<router-outlet></router-outlet>
</div>
<nav class="sidenav">
<nav class="sidenav" [class.side-nav-override]="showSearch">
<section class="sidenav-content">
<a routerLink="/harbor/projects" routerLinkActive="active" class="nav-link">
Projects

View File

@ -3,12 +3,15 @@ import { Router } from '@angular/router';
import { SessionService } from '../../shared/session.service';
import { ModalEvent } from '../modal-event';
import { SearchEvent } from '../search-event';
import { AccountSettingsModalComponent } from '../account-settings/account-settings-modal.component';
import { SearchResultComponent } from '../global-search/search-result.component';
@Component({
selector: 'harbor-shell',
templateUrl: 'harbor-shell.component.html'
templateUrl: 'harbor-shell.component.html',
styleUrls: ["harbor-shell.component.css"]
})
export class HarborShellComponent implements OnInit {
@ -16,6 +19,13 @@ export class HarborShellComponent implements OnInit {
@ViewChild(AccountSettingsModalComponent)
private accountSettingsModal: AccountSettingsModalComponent;
@ViewChild(SearchResultComponent)
private searchResultComponet: SearchResultComponent;
//To indicator whwther or not the search results page is displayed
//We need to use this property to do some overriding work
private isSearchResultsOpened: boolean = false;
constructor(private session: SessionService) { }
ngOnInit() {
@ -26,6 +36,10 @@ export class HarborShellComponent implements OnInit {
}
}
public get showSearch(): boolean {
return this.isSearchResultsOpened;
}
//Open modal dialog
openModal(event: ModalEvent): void {
switch (event.modalName) {
@ -36,13 +50,21 @@ export class HarborShellComponent implements OnInit {
}
}
//Close the modal dialog
closeModal(event: ModalEvent): void {
switch (event.modalName) {
case "account-settings":
this.accountSettingsModal.close();
default:
break;
//Handle the global search event and then let the result page to trigger api
doSearch(event: SearchEvent): void {
//Once this method is called
//the search results page must be opened
this.isSearchResultsOpened = true;
//Call the child component to do the real work
this.searchResultComponet.doSearch(event.term);
}
//Search results page closed
//remove the related ovevriding things
searchClose(event: boolean): void {
if(event){
this.isSearchResultsOpened = false;
}
}
}

View File

@ -5,7 +5,7 @@
<span class="title">Harbor</span>
</a>
</div>
<global-search></global-search>
<global-search (searchEvt)="transferSearchEvent($event)"></global-search>
<div class="header-actions">
<clr-dropdown [clrMenuPosition]="'bottom-right'" class="dropdown">
<button class="nav-text" clrDropdownToggle>

View File

@ -1,7 +1,8 @@
import { Component, Output, EventEmitter } from '@angular/core';
import { Router } from '@angular/router';
import { ModalEvent } from '../modal-event';
import { ModalEvent } from '../modal-event';
import { SearchEvent } from '../search-event';
@Component({
selector: 'navigator',
@ -10,6 +11,7 @@ import { ModalEvent } from '../modal-event';
export class NavigatorComponent {
// constructor(private router: Router){}
@Output() showAccountSettingsModal = new EventEmitter<ModalEvent>();
@Output() searchEvt = new EventEmitter<SearchEvent>();
//Open the account setting dialog
open():void {
@ -18,4 +20,9 @@ export class NavigatorComponent {
modalFlag: true
});
}
//Only transfer the search event to the parent shell
transferSearchEvent(evt: SearchEvent): void {
this.searchEvt.emit(evt);
}
}

View File

@ -0,0 +1,4 @@
//Define a object to store the search event
export class SearchEvent {
term: string;
}