Searching for models into client cache

Change-Id: Ib65b1565153040684083fbc21a59e8c8365628fd
diff --git a/src/app/core/header/header.html b/src/app/core/header/header.html
index 5025258..c0bebe2 100644
--- a/src/app/core/header/header.html
+++ b/src/app/core/header/header.html
@@ -1,23 +1,13 @@
-<!--<header class="header">-->
-  <!--<p class="header-title">-->
-    <!--<a href="#/" target="_blank">-->
-      <!--{{vm.title}}-->
-    <!--</a>-->
-  <!--</p>-->
-  <!--<p class="header-date notification">-->
-    <!--<i ng-if="vm.newNotifications.length > 0" class="badge"></i>-->
-    <!--<i class="fa fa-bell" ng-click="vm.showNotification = !vm.showNotification"></i>-->
-    <!--<div class="notification-panel" ng-show="vm.showNotification">-->
-      <!--<ul>-->
-        <!--<li ng-repeat="n in vm.notifications track by $index" ng-click="vm.viewNotification(n)" ng-class="{viewed: n.viewed}">-->
-          <!--<b>{{n.model}}</b><br>-->
-          <!--<i>{{n.msg.object.name}} status is {{n.msg.object.backend_status}}</i>-->
-        <!--</li>-->
-      <!--</ul>-->
-    <!--</div>-->
-  <!--</p>-->
-<!--</header>-->
-
+<!-- Custom template for Typeahead -->
+<script type="text/ng-template" id="customTemplate.html">
+  <a>
+    <span ng-bind-html="match.label.label | uibTypeaheadHighlight:query"></span>
+    <code class="pull-right">
+      {{match.label.type}}
+    </code>
+  </a>
+</script>
+<!-- END Custom template for Typeahead -->
 
 <!-- Header -->
 <nav class="navbar navbar-default navbar-fixed-top">
@@ -40,7 +30,8 @@
           placeholder="Navigate routes (press 'f' to select)"
           style="width: 275px"
           ng-model="vm.query"
-          uib-typeahead="state.label for state in vm.states | filter:$viewValue | limitTo:8"
+          uib-typeahead="state for state in vm.search($viewValue) | limitTo:30"
+          typeahead-template-url="customTemplate.html"
           typeahead-on-select="vm.routeSelected($item, $model, $label)">
       </form>
       <ul class="nav navbar-nav navbar-right">
diff --git a/src/app/core/header/header.scss b/src/app/core/header/header.scss
index 263740c..ca97ae2 100644
--- a/src/app/core/header/header.scss
+++ b/src/app/core/header/header.scss
@@ -18,4 +18,27 @@
   .navbar-default {
     background: #2a2d35 !important;
   }
+
+  .dropdown-menu {
+    background: #2a2d35 !important;
+    min-width: 275px;
+    max-height: 600px;
+    overflow-y: scroll;
+
+    .active > a {
+      border-left: 6px solid #f6a821;
+      background: #494b54;
+      color: #fff !important;
+    }
+
+    a {
+      color: #c0c4c8 !important;
+      display: block;
+      width: 100%;
+    }
+
+    a:hover {
+      background: #494b54 !important;
+    }
+  }
 }
\ No newline at end of file
diff --git a/src/app/core/header/header.spec.ts b/src/app/core/header/header.spec.ts
index 62fa368..0bd6877 100644
--- a/src/app/core/header/header.spec.ts
+++ b/src/app/core/header/header.spec.ts
@@ -54,7 +54,8 @@
       .value('NavigationService', {})
       .value('StyleConfig', {
         logo: 'cord-logo.png',
-      });
+      })
+      .value('SearchService', {});
 
     angular.mock.module('xosHeader');
   });
diff --git a/src/app/core/header/header.ts b/src/app/core/header/header.ts
index 0a98494..620aecd 100644
--- a/src/app/core/header/header.ts
+++ b/src/app/core/header/header.ts
@@ -4,23 +4,24 @@
 import {IXosAuthService} from '../../datasources/rest/auth.rest';
 import {IXosNavigationService, IXosNavigationRoute} from '../services/navigation';
 import {IStateService} from 'angular-ui-router';
-import * as _ from 'lodash';
 import * as $ from 'jquery';
 import {IXosStyleConfig} from '../../../index';
+import {IXosSearchService, IXosSearchResult} from '../../datasources/helpers/search.service';
 
 export interface INotification extends IWSEvent {
   viewed?: boolean;
 }
 
 class HeaderController {
-  static $inject = ['$scope', '$rootScope', '$state', 'AuthService', 'SynchronizerStore', 'toastr', 'toastrConfig', 'NavigationService', 'StyleConfig'];
+  static $inject = ['$scope', '$rootScope', '$state', 'AuthService', 'SynchronizerStore', 'toastr', 'toastrConfig', 'NavigationService', 'StyleConfig', 'SearchService'];
   public notifications: INotification[] = [];
   public newNotifications: INotification[] = [];
   public version: string;
   public userEmail: string;
-  public routeSelected: (route: IXosNavigationRoute) => void;
+  public routeSelected: (route: IXosSearchResult) => void;
   public states: IXosNavigationRoute[];
   public query: string;
+  public search: (query: string) => any[];
 
   constructor(
     private $scope: angular.IScope,
@@ -31,7 +32,8 @@
     private toastr: ng.toastr.IToastrService,
     private toastrConfig: ng.toastr.IToastrConfig,
     private NavigationService: IXosNavigationService,
-    private StyleConfig: IXosStyleConfig
+    private StyleConfig: IXosStyleConfig,
+    private SearchService: IXosSearchService
   ) {
     this.version = require('../../../../package.json').version;
     angular.extend(this.toastrConfig, {
@@ -46,33 +48,30 @@
       // tapToDismiss: false
     });
 
-    this.$rootScope.$on('xos.core.modelSetup', () => {
-      this.states = this.NavigationService.query().reduce((list, state) => {
-        // if it does not have child (otherwise it is abstract)
-        if (!state.children || state.children.length === 0) {
-          list.push(state);
-        }
-        // else push child
-        if (state.children && state.children.length > 0) {
-          state.children.forEach(c => {
-            list.push(c);
-          });
-        }
-        return list;
-      }, []);
-      this.states = _.uniqBy(this.states, 'state');
-    });
+    // this.$rootScope.$on('xos.core.modelSetup', () => {
+    //   this.states = _.uniqBy(this.states, 'state');
+    // });
+
+    this.search = (query: string) => {
+      return this.SearchService.search(query);
+    };
 
     // listen for keypress
     $(document).on('keyup', (e) => {
       if (e.key === 'f') {
         $('.navbar-form input').focus();
       }
+      // console.log(this.SearchService.getStates());
     });
 
     // redirect to selected page
-    this.routeSelected = (item: IXosNavigationRoute) => {
-      this.$state.go(item.state);
+    this.routeSelected = (item: IXosSearchResult) => {
+      if (angular.isString(item.state)) {
+        this.$state.go(item.state);
+      }
+      else {
+        this.$state.go(item.state.name, item.state.params);
+      }
       this.query = null;
     };
 
diff --git a/src/app/core/services/helpers/config.helpers.ts b/src/app/core/services/helpers/config.helpers.ts
index fd5e8e0..d0fbed9 100644
--- a/src/app/core/services/helpers/config.helpers.ts
+++ b/src/app/core/services/helpers/config.helpers.ts
@@ -30,6 +30,7 @@
   urlFromCoreModel(model: string): string;
   stateFromCoreModel(name: string): string;
   stateWithParams(name: string, model: any): string;
+  stateWithParamsForJs(name: string, model: any): any;
 }
 
 export class ConfigHelpers {
@@ -192,6 +193,12 @@
     return `${state}({id: ${model['id']}})`;
   }
 
+  public stateWithParamsForJs(name: string, model: any): any {
+    // TODO test and interface
+    const state = this.stateFromCoreModel(name);
+    return {name: state, params: {id: model.id}};
+  }
+
   public modelFieldToInputCfg(fields: IXosModelDefsField[]): IXosFormInput[] {
 
     return _.map(fields, (f: IXosModelDefsField) => {
diff --git a/src/app/datasources/helpers/search.service.ts b/src/app/datasources/helpers/search.service.ts
new file mode 100644
index 0000000..8cd98a7
--- /dev/null
+++ b/src/app/datasources/helpers/search.service.ts
@@ -0,0 +1,62 @@
+import * as _ from 'lodash';
+import {IXosNavigationService} from '../../core/services/navigation';
+import {IXosState} from '../../../index';
+import {IModelStoreService} from '../stores/model.store';
+import {IXosConfigHelpersService} from '../../core/services/helpers/config.helpers';
+
+export interface IXosSearchResult {
+  label: string;
+  state: string | {name: string, params: any};
+  type?: string;
+}
+
+export interface IXosSearchService {
+  search(query: string): IXosSearchResult[];
+}
+
+export class SearchService {
+  static $inject = ['$rootScope', 'NavigationService', 'ModelStore', 'ConfigHelpers'];
+  private states: IXosState[];
+
+  constructor (
+    private $rootScope: ng.IScope,
+    private NavigationService: IXosNavigationService,
+    private ModelStore: IModelStoreService,
+    private ConfigHelpers: IXosConfigHelpersService
+  ) {
+    this.$rootScope.$on('xos.core.modelSetup', () => {
+      this.states = this.NavigationService.query().reduce((list, state) => {
+        // if it does not have child (otherwise it is abstract)
+        if (!state.children || state.children.length === 0) {
+          list.push(state);
+        }
+        // else push child
+        if (state.children && state.children.length > 0) {
+          state.children.forEach(c => {
+            list.push(c);
+          });
+        }
+        return list;
+      }, []);
+      this.states = _.uniqBy(this.states, 'state');
+    });
+  }
+
+  public search(query: string): IXosSearchResult[] {
+    const routes: IXosSearchResult[] = _.filter(this.states, s => {
+      return s.label.toLowerCase().indexOf(query) > -1;
+    }).map(r => {
+      r.type = 'View';
+      return r;
+    });
+
+    const models = _.map(this.ModelStore.search(query), m => {
+      return {
+        label: m.humanReadableName ? m.humanReadableName : m.name,
+        state: this.ConfigHelpers.stateWithParamsForJs(m.modelName, m),
+        type: m.modelName
+      };
+    });
+    return routes.concat(models);
+  }
+}
diff --git a/src/app/datasources/index.ts b/src/app/datasources/index.ts
index 7a14a9a..04c58db 100644
--- a/src/app/datasources/index.ts
+++ b/src/app/datasources/index.ts
@@ -6,6 +6,7 @@
 import {SynchronizerStore} from './stores/synchronizer.store';
 import {ModeldefsService} from './rest/modeldefs.rest';
 import {xosCore} from '../core/index';
+import {SearchService} from './helpers/search.service';
 
 export const xosDataSources = 'xosDataSources';
 
@@ -13,11 +14,9 @@
   .module('xosDataSources', ['ngCookies', 'ngResource', xosCore])
   .service('ModelRest', ModelRest)
   .service('AuthService', AuthService)
-  .service('WebSocket', WebSocketEvent);
-
-angular
-  .module('xosDataSources')
+  .service('WebSocket', WebSocketEvent)
   .service('StoreHelpers', StoreHelpers)
   .service('SynchronizerStore', SynchronizerStore)
   .service('ModelStore', ModelStore)
-  .service('ModelDefs', ModeldefsService);
+  .service('ModelDefs', ModeldefsService)
+  .service('SearchService', SearchService);
diff --git a/src/app/datasources/stores/model.store.ts b/src/app/datasources/stores/model.store.ts
index fdcac63..4c6c09d 100644
--- a/src/app/datasources/stores/model.store.ts
+++ b/src/app/datasources/stores/model.store.ts
@@ -1,5 +1,5 @@
 /// <reference path="../../../../typings/index.d.ts"/>
-
+import * as _ from 'lodash';
 import {BehaviorSubject, Observable} from 'rxjs/Rx';
 import {IWSEvent, IWSEventService} from '../websocket/global';
 import {IXosResourceService} from '../rest/model.rest';
@@ -7,6 +7,7 @@
 
 export interface  IModelStoreService {
   query(model: string): Observable<any>;
+  search(modelName: string): any[];
 }
 
 export class ModelStore {
@@ -39,6 +40,26 @@
     return this._collections[model].asObservable();
   }
 
+  public search(modelName: string): any[] {
+    return _.reduce(Object.keys(this._collections), (results, k) => {
+      // console.log(k, this._collections[k].value)
+      const partialRes = _.filter(this._collections[k].value, i => {
+        if (i.humanReadableName) {
+          return i.humanReadableName.toLowerCase().indexOf(modelName) > -1;
+        }
+        else if (i.name) {
+          return i.name.toLowerCase().indexOf(modelName) > -1;
+        }
+        return false;
+      })
+        .map(m => {
+          m.modelName = k;
+          return m;
+        });
+      return results.concat(partialRes);
+    }, []);
+  }
+
   public get(model: string, id: number) {
     // TODO implement a get method
   }