Added child routes, and config defined routes

Change-Id: I61c5a49a330a63737312d1eb6077aab02236a44f
diff --git a/conf/app/app.config.dev.ts b/conf/app/app.config.dev.ts
index 7b6fa12..50570d7 100644
--- a/conf/app/app.config.dev.ts
+++ b/conf/app/app.config.dev.ts
@@ -1,10 +1,4 @@
-/// <reference path="../../../typings/index.d.ts"/>
-
-export interface IAppConfig {
-    apiEndpoint: string;
-    websocketClient: string;
-}
-
+import {IAppConfig} from './interfaces';
 export const AppConfig: IAppConfig = {
     apiEndpoint: 'http://xos.dev:3000/api',
     websocketClient: 'http://xos.dev:3000'
diff --git a/conf/app/app.config.production.ts b/conf/app/app.config.production.ts
index 361db95..c6e774c 100644
--- a/conf/app/app.config.production.ts
+++ b/conf/app/app.config.production.ts
@@ -1,10 +1,4 @@
-/// <reference path="../../../typings/index.d.ts"/>
-
-export interface IAppConfig {
-    apiEndpoint: string;
-    websocketClient: string;
-}
-
+import {IAppConfig} from './interfaces';
 export const AppConfig: IAppConfig = {
     apiEndpoint: '/api',
     websocketClient: ''
diff --git a/conf/app/app.config.test.ts b/conf/app/app.config.test.ts
index 7d215af..109776c 100644
--- a/conf/app/app.config.test.ts
+++ b/conf/app/app.config.test.ts
@@ -1,10 +1,4 @@
-/// <reference path="../../../typings/index.d.ts"/>
-
-export interface IAppConfig {
-    apiEndpoint: string;
-    websocketClient: string;
-}
-
+import {IAppConfig} from './interfaces';
 export const AppConfig: IAppConfig = {
     apiEndpoint: 'http://xos-test:3000/api',
     websocketClient: 'http://xos-test:3000'
diff --git a/conf/app/interfaces.ts b/conf/app/interfaces.ts
new file mode 100644
index 0000000..e162d95
--- /dev/null
+++ b/conf/app/interfaces.ts
@@ -0,0 +1,11 @@
+import {IXosNavigationRoute} from '../../src/app/core/services/navigation';
+export interface IStyleConfig {
+  projectName: string;
+  favicon: string;
+  routes: IXosNavigationRoute[];
+}
+
+export interface IAppConfig {
+  apiEndpoint: string;
+  websocketClient: string;
+}
diff --git a/conf/app/style.config.cord.ts b/conf/app/style.config.cord.ts
index 120725f..3e1135b 100644
--- a/conf/app/style.config.cord.ts
+++ b/conf/app/style.config.cord.ts
@@ -1,11 +1,11 @@
-/// <reference path="../../../typings/index.d.ts"/>
-
-export interface IStyleConfig {
-    projectName: string;
-    favicon: string;
-}
-
+import {IStyleConfig} from './interfaces';
 export const StyleConfig: IStyleConfig = {
     projectName: 'CORD',
-    favicon: 'cord-favicon.png'
+    favicon: 'cord-favicon.png',
+    routes: [
+        {
+            label: 'Slices',
+            state: 'xos.core.slices'
+        }
+    ]
 };
diff --git a/conf/app/style.config.opencloud.ts b/conf/app/style.config.opencloud.ts
index 9423866..c4d735d 100644
--- a/conf/app/style.config.opencloud.ts
+++ b/conf/app/style.config.opencloud.ts
@@ -1,11 +1,11 @@
-/// <reference path="../../../typings/index.d.ts"/>
-
-export interface IStyleConfig {
-    projectName: string;
-    favicon: string;
-}
-
+import {IStyleConfig} from './interfaces';
 export const StyleConfig: IStyleConfig = {
     projectName: 'OpenCloud',
-    favicon: 'opencloud-favicon.png'
+    favicon: 'opencloud-favicon.png',
+    routes: [
+        {
+            label: 'Slices',
+            state: 'xos.core.slices'
+        }
+    ]
 };
diff --git a/conf/karma-auto.conf.js b/conf/karma-auto.conf.js
index e66de9c..a5ea084 100644
--- a/conf/karma-auto.conf.js
+++ b/conf/karma-auto.conf.js
@@ -12,7 +12,7 @@
     },
     browsers: [
       'PhantomJS',
-      'Chrome'
+      // 'Chrome'
     ],
     frameworks: [
       'jasmine',
diff --git a/gulp_tasks/misc.js b/gulp_tasks/misc.js
index e9fe0f0..6a02ecc 100644
--- a/gulp_tasks/misc.js
+++ b/gulp_tasks/misc.js
@@ -4,6 +4,7 @@
 const del = require('del');
 const filter = require('gulp-filter');
 const rename = require('gulp-rename');
+const replace = require('gulp-replace');
 
 const conf = require('../conf/gulp.conf');
 const cfgFolder = path.join(conf.paths.src, 'app/config');
@@ -12,7 +13,8 @@
 gulp.task('other', other);
 gulp.task('brand', styleConfig);
 gulp.task('appConfig', appConfig);
-gulp.task('config', gulp.series('brand', 'appConfig'));
+gulp.task('copyCfgInterfaces', copyCfgInterfaces);
+gulp.task('config', gulp.series('copyCfgInterfaces', 'brand', 'appConfig'));
 
 function clean() {
   return del([conf.paths.dist, conf.paths.tmp]);
@@ -39,6 +41,7 @@
 }
 
 function styleConfig() {
+  // TODO copy interfaces
   const env = process.env.BRAND || 'cord';
   return gulp.src([
     path.join(conf.paths.appConfig, `style.config.${env}.ts`)
@@ -47,6 +50,14 @@
     .pipe(gulp.dest(cfgFolder));
 }
 
+function copyCfgInterfaces() {
+  return gulp.src([
+    path.join(conf.paths.appConfig, `interfaces.ts`)
+  ])
+    .pipe(replace('../../src/app/core/services/navigation', '../core/services/navigation'))
+    .pipe(gulp.dest(cfgFolder));
+}
+
 function other() {
   const fileFilter = filter(file => file.stat.isFile());
 
diff --git a/package.json b/package.json
index ef2db62..d455cf3 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
     "gulp-insert": "^0.5.0",
     "gulp-ng-annotate": "^1.1.0",
     "gulp-rename": "^1.2.2",
+    "gulp-replace": "^0.5.4",
     "gulp-sass": "^2.1.1",
     "gulp-util": "^3.0.7",
     "html-loader": "^0.4.3",
diff --git a/src/app/config/app.config.ts b/src/app/config/app.config.ts
index 7b6fa12..50570d7 100644
--- a/src/app/config/app.config.ts
+++ b/src/app/config/app.config.ts
@@ -1,10 +1,4 @@
-/// <reference path="../../../typings/index.d.ts"/>
-
-export interface IAppConfig {
-    apiEndpoint: string;
-    websocketClient: string;
-}
-
+import {IAppConfig} from './interfaces';
 export const AppConfig: IAppConfig = {
     apiEndpoint: 'http://xos.dev:3000/api',
     websocketClient: 'http://xos.dev:3000'
diff --git a/src/app/config/interfaces.ts b/src/app/config/interfaces.ts
new file mode 100644
index 0000000..5c13afe
--- /dev/null
+++ b/src/app/config/interfaces.ts
@@ -0,0 +1,11 @@
+import {IXosNavigationRoute} from '../core/services/navigation';
+export interface IStyleConfig {
+  projectName: string;
+  favicon: string;
+  routes: IXosNavigationRoute[];
+}
+
+export interface IAppConfig {
+  apiEndpoint: string;
+  websocketClient: string;
+}
diff --git a/src/app/config/style.config.ts b/src/app/config/style.config.ts
index 120725f..3e1135b 100644
--- a/src/app/config/style.config.ts
+++ b/src/app/config/style.config.ts
@@ -1,11 +1,11 @@
-/// <reference path="../../../typings/index.d.ts"/>
-
-export interface IStyleConfig {
-    projectName: string;
-    favicon: string;
-}
-
+import {IStyleConfig} from './interfaces';
 export const StyleConfig: IStyleConfig = {
     projectName: 'CORD',
-    favicon: 'cord-favicon.png'
+    favicon: 'cord-favicon.png',
+    routes: [
+        {
+            label: 'Slices',
+            state: 'xos.core.slices'
+        }
+    ]
 };
diff --git a/src/app/core/nav/nav.html b/src/app/core/nav/nav.html
index 2bcdadd..a1292b8 100644
--- a/src/app/core/nav/nav.html
+++ b/src/app/core/nav/nav.html
@@ -1,8 +1,23 @@
 <div class="nav">
   <ul>
-    <li ng-repeat="route in vm.routes" ui-sref-active="active" ng-class="vm.isRouteActive(route)">
-      <a ng-if="route.state" ui-sref="{{route.state}}">{{route.label}}</a>
-      <a ng-if="route.url" href="#/{{route.url}}">{{route.label}}</a>
+    <li
+      ng-repeat="route in vm.routes track by $index"
+      ui-sref-active="active"
+      ng-class="vm.isRouteActive(route)">
+      <a ng-if="route.state" ui-sref="{{route.state}}" ng-click="vm.activateRoute(route)">
+        <i ng-if="route.children" class="fa fa-chevron-right"></i>
+        {{route.label}}
+      </a>
+      <a ng-if="route.url" href="#/{{route.url}}" ng-click="vm.activateRoute(route)">
+        <i ng-if="route.children" class="fa fa-chevron-right"></i>
+        {{route.label}}
+      </a>
+      <ul class="child-routes" ng-if="route.children" ng-class="{opened: route.opened}">
+        <li ng-repeat="childRoute in route.children" ui-sref-active="active">
+          <a ng-if="childRoute.state" ui-sref="{{childRoute.state}}">{{childRoute.label}}</a>
+          <a ng-if="childRoute.url" href="#/{{childRoute.url}}">{{childRoute.label}}</a>
+        </li>
+      </ul>
     </li>
   </ul>
 </div>
diff --git a/src/app/core/nav/nav.scss b/src/app/core/nav/nav.scss
index 5c2c85c..dbe60d7 100644
--- a/src/app/core/nav/nav.scss
+++ b/src/app/core/nav/nav.scss
@@ -15,7 +15,6 @@
     > li {
       display: flex;
       flex-direction: column;
-      padding: 10px 20px;
       border-bottom: 1px solid darken(grey, 20);
 
       &.active {
@@ -31,8 +30,29 @@
       }
 
       > a {
+        padding: 10px 20px;
         cursor: pointer;
       }
+
+      // child router
+      > ul {
+        height: 0;
+        overflow: hidden;
+        transition: .5s all;
+
+        > li {
+          padding-left: 20px;
+          background: darken(grey, 15);
+
+          &:hover, &.active {
+            background: darken(grey, 20);
+          }
+        }
+      }
+
+      > ul.opened {
+        height: auto;
+      }
     }
   }
 }
diff --git a/src/app/core/nav/nav.spec.ts b/src/app/core/nav/nav.spec.ts
new file mode 100644
index 0000000..fbed8ea
--- /dev/null
+++ b/src/app/core/nav/nav.spec.ts
@@ -0,0 +1,57 @@
+/// <reference path="../../../../typings/index.d.ts" />
+
+import * as $ from 'jquery';
+import 'jasmine-jquery';
+import * as angular from 'angular';
+import 'angular-mocks';
+import {IXosNavigationRoute} from '../services/navigation';
+import {xosNav} from './nav';
+
+let element, scope: angular.IRootScopeService, compile: ng.ICompileService, isolatedScope;
+
+let baseRoutes: IXosNavigationRoute[] = [
+  {label: 'Home', state: 'xos'},
+  {label: 'Core', state: 'xos.core'}
+];
+
+const NavigationService = function(){
+  this.query = () => baseRoutes;
+};
+
+describe('Nav component', () => {
+  beforeEach(() => {
+    angular
+      .module('xosNav', ['app/core/nav/nav.html', 'ui.router'])
+      .component('xosNav', xosNav)
+      .service('NavigationService', NavigationService);
+    angular.mock.module('xosNav');
+  });
+
+  beforeEach(angular.mock.inject(($rootScope: ng.IRootScopeService, $compile: ng.ICompileService) => {
+    scope = $rootScope;
+    compile = $compile;
+    element = $compile('<xos-nav></xos-nav>')($rootScope);
+    $rootScope.$digest();
+    isolatedScope = element.isolateScope();
+
+    // clear routes
+    isolatedScope.routes = [];
+  }));
+
+  it('should render a list of routes', () => {
+    const routes = $('.nav ul li', element);
+    expect(routes.length).toBe(2);
+  });
+
+  it('should render child routes', () => {
+    baseRoutes = [
+      {label: 'Home', state: 'xos'},
+      {label: 'Core', state: 'xos.core', children: [
+        {label: 'Slices', state: 'xos.core.slices', parent: 'xos.core'}
+      ]}
+    ];
+    scope.$apply();
+    const childRouteContainer = $('.child-routes', element);
+    expect(childRouteContainer.length).toBe(1);
+  });
+});
diff --git a/src/app/core/nav/nav.ts b/src/app/core/nav/nav.ts
index d978ef0..cd249b9 100644
--- a/src/app/core/nav/nav.ts
+++ b/src/app/core/nav/nav.ts
@@ -2,10 +2,11 @@
 import {IXosNavigationService, IXosNavigationRoute} from '../services/navigation';
 
 class NavCtrl {
-  static $inject = ['$state', 'NavigationService'];
+  static $inject = ['$scope', '$state', 'NavigationService'];
   public routes: IXosNavigationRoute[];
 
   constructor(
+    private $scope: ng.IScope,
     private $state: angular.ui.IStateService,
     private navigationService: IXosNavigationService
   ) {
@@ -13,12 +14,19 @@
     // - Base routes (defined from configuration based on BRAND)
     // - Autogenerated routes (nested somewhere)
     // - Service Routes (dynamically added)
-    this.routes = this.navigationService.query();
+
+    this.$scope.$watch(() => this.navigationService.query(), (routes) => {
+      this.routes = routes;
+    });
   }
 
   isRouteActive(route: IXosNavigationRoute) {
     return this.$state.current.url === route.url ? 'active' : '';
   }
+
+  activateRoute(route: IXosNavigationRoute) {
+    route.opened = !route.opened;
+  }
 }
 
 export const xosNav: angular.IComponentOptions = {
diff --git a/src/app/core/services/helpers/config.helpers.spec.ts b/src/app/core/services/helpers/config.helpers.spec.ts
index 44260d8..af424e4 100644
--- a/src/app/core/services/helpers/config.helpers.spec.ts
+++ b/src/app/core/services/helpers/config.helpers.spec.ts
@@ -50,13 +50,13 @@
     it('should format an array of strings', () => {
       let strings: string[] = ['camelCase', 'snake_case', 'kebab-case'];
       let labels = ['Camel case', 'Snake case', 'Kebab case'];
-      expect(service.toLabel(strings)).toEqual(labels);
+      expect(service.toLabels(strings)).toEqual(labels);
     });
 
     it('should set plural on an array of strings', () => {
       let strings: string[] = ['camelCase', 'snake_case', 'kebab-case'];
       let labels = ['Camel cases', 'Snake cases', 'Kebab cases'];
-      expect(service.toLabel(strings, true)).toEqual(labels);
+      expect(service.toLabels(strings, true)).toEqual(labels);
     });
   });
 
diff --git a/src/app/core/services/helpers/config.helpers.ts b/src/app/core/services/helpers/config.helpers.ts
index 44d3cdc..3e0af96 100644
--- a/src/app/core/services/helpers/config.helpers.ts
+++ b/src/app/core/services/helpers/config.helpers.ts
@@ -11,6 +11,7 @@
   modeldefToTableCfg(fields: IXosModelDefsField[]): any[]; // TODO use a proper interface
   pluralize(string: string, quantity?: number, count?: boolean): string;
   toLabel(string: string, pluralize?: boolean): string;
+  toLabels(string: string[], pluralize?: boolean): string[];
 }
 
 export class ConfigHelpers {
@@ -24,13 +25,15 @@
     return pluralize(string, quantity, count);
   }
 
-  toLabel(string: string, pluralize?: boolean): string {
-
-    if (angular.isArray(string)) {
-      return _.map(string, s => {
+  toLabels(strings: string[], pluralize?: boolean): string[] {
+    if (angular.isArray(strings)) {
+      return _.map(strings, s => {
         return this.toLabel(s, pluralize);
       });
     }
+  }
+
+  toLabel(string: string, pluralize?: boolean): string {
 
     if (pluralize) {
       string = this.pluralize(string);
diff --git a/src/app/core/services/navigation.spec.ts b/src/app/core/services/navigation.spec.ts
index 1096ec7..c5fb85d 100644
--- a/src/app/core/services/navigation.spec.ts
+++ b/src/app/core/services/navigation.spec.ts
@@ -6,9 +6,7 @@
 
 let service: IXosNavigationService;
 
-const defaultRoutes: IXosNavigationRoute[] = [
-  {label: 'Home', state: 'xos.dashboard'}
-];
+let defaultRoutes: IXosNavigationRoute[];
 
 describe('The Navigation service', () => {
 
@@ -18,6 +16,7 @@
     NavigationService: IXosNavigationService,
   ) => {
     service = NavigationService;
+    defaultRoutes = angular.copy(service.query());
   }));
 
   it('should return navigation routes', () => {
@@ -31,7 +30,17 @@
     ];
     service.add(testRoutes[0]);
     service.add(testRoutes[1]);
-    expect(service.query()).toEqual(defaultRoutes.concat(testRoutes));
+    const serviceRoutes = service.query();
+    expect(serviceRoutes).toEqual(defaultRoutes.concat(testRoutes));
+  });
+
+  it('should add a child route', () => {
+    const testRoute: IXosNavigationRoute = {
+      label: 'TestState', state: 'xos.test', parent: 'xos.core'
+    };
+    service.add(testRoute);
+    defaultRoutes[1].children = [testRoute];
+    expect(service.query()).toEqual(defaultRoutes);
   });
 
   it('should not add route that have both url and state', () => {
diff --git a/src/app/core/services/navigation.ts b/src/app/core/services/navigation.ts
index 04eb575..c840878 100644
--- a/src/app/core/services/navigation.ts
+++ b/src/app/core/services/navigation.ts
@@ -1,7 +1,15 @@
+/// <reference path="../../../../typings/index.d.ts" />
+
+import * as _ from 'lodash';
+import {StyleConfig} from '../../config/style.config';
+
 export interface IXosNavigationRoute {
   label: string;
   state?: string;
   url?: string;
+  parent?: string;
+  children?: [IXosNavigationRoute];
+  opened?: boolean;
 }
 
 export interface IXosNavigationService {
@@ -13,12 +21,18 @@
   private routes: IXosNavigationRoute[];
 
   constructor() {
-    this.routes = [
+    const defaultRoutes = [
+      {
+        label: 'Core',
+        state: 'xos.core'
+      },
       {
         label: 'Home',
         state: 'xos.dashboard'
       }
     ];
+    // adding configuration defined routes
+    this.routes = StyleConfig.routes.concat(defaultRoutes).reverse();
   }
 
   query() {
@@ -29,6 +43,21 @@
     if (angular.isDefined(route.state) && angular.isDefined(route.url)) {
       throw new Error('[XosNavigation] You can\'t provide both state and url');
     }
-    this.routes.push(route);
+
+
+    if (angular.isDefined(route.parent)) {
+      // route parent should be a state for now
+      const parentRoute = _.find(this.routes, {state: route.parent});
+
+      if (angular.isArray(parentRoute.children)) {
+        parentRoute.children.push(route);
+      }
+      else {
+        parentRoute.children = [route];
+      }
+    }
+    else {
+      this.routes.push(route);
+    }
   }
 }
diff --git a/src/app/core/services/runtime-states.ts b/src/app/core/services/runtime-states.ts
index 401075a..bc99a8e 100644
--- a/src/app/core/services/runtime-states.ts
+++ b/src/app/core/services/runtime-states.ts
@@ -1,10 +1,10 @@
 import {IXosState} from '../../../index';
 export interface IRuntimeStatesService {
-  addState(name: string, state: angular.ui.IState): void;
+  addState(name: string, state: ng.ui.IState): void;
 }
 
-export function RuntimeStates($stateProvider: angular.ui.IStateProvider): angular.IServiceProvider {
-  this.$get = function($state: angular.ui.IStateService) { // for example
+export function RuntimeStates($stateProvider: ng.ui.IStateProvider): ng.IServiceProvider {
+  this.$get = function($state: ng.ui.IStateService) {
     return {
       addState: function(name: string, state: IXosState) {
         $stateProvider.state(name, state);
diff --git a/src/index.ts b/src/index.ts
index 7021f86..d27ac96 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -38,20 +38,28 @@
   .factory('NoHyperlinksInterceptor', NoHyperlinksInterceptor)
   .component('xos', main)
   .run((
+    $location: ng.ILocationService,
+    $state: ng.ui.IStateService,
     ModelDefs: IModeldefsService,
     RuntimeStates: IRuntimeStatesService,
     NavigationService: IXosNavigationService,
     ConfigHelpers: IXosConfigHelpersService,
     PageTitle: IXosPageTitleService
   ) => {
+
+    // save the last visited state before reload
+    const lastRoute = window.location.hash.replace('#', '');
+
     // Dinamically add a  core states
     ModelDefs.get()
       .then((models: IModeldef[]) => {
         // TODO move in a separate service and test
         _.forEach(models, (m: IModeldef) => {
+          const stateUrl = `/${ConfigHelpers.pluralize(m.name.toLowerCase())}`;
+          const stateName = `xos.core.${ConfigHelpers.pluralize(m.name.toLowerCase())}`;
           const state: IXosState = {
-            parent: 'xos',
-            url: ConfigHelpers.pluralize(m.name.toLowerCase()),
+            parent: 'core',
+            url: stateUrl,
             component: 'xosCrud',
             data: {
               model: m.name,
@@ -60,8 +68,19 @@
               }
             }
           };
-          RuntimeStates.addState(ConfigHelpers.pluralize(m.name.toLowerCase()), state);
-          NavigationService.add({label: ConfigHelpers.pluralize(m.name), url: ConfigHelpers.pluralize(m.name.toLowerCase())});
+
+          RuntimeStates.addState(stateName, state);
+          NavigationService.add({
+            label: ConfigHelpers.pluralize(m.name),
+            state: stateName,
+            parent: 'xos.core'
+          });
         });
+
+        // after setting up dynamic routes, redirect to previous state
+        $location.path(lastRoute);
+        // $state.get().forEach(s => {
+        //   console.log($state.href(s.name));
+        // });
       });
   });
diff --git a/src/routes.ts b/src/routes.ts
index c6de435..1dd1b43 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -7,10 +7,8 @@
   $locationProvider.html5Mode(false).hashPrefix('');
   $urlRouterProvider.otherwise('/');
 
-  // TODO onload redirect to correct URL
-  // routes are created asynchronously so by default any time you reload
-  // you end up in /
-
+  // declare here static endpoints,
+  // core related endpoints are dynamically generated
   $stateProvider
     .state('xos', {
       abstract: true,
@@ -22,9 +20,15 @@
       parent: 'xos',
       template: '<h1>Dashboard</h1>'
     })
-    .state('xos.nodes', {
-      url: 'nodes',
+    .state('xos.core', {
+      url: 'core',
       parent: 'xos',
-      template: '<h1>Nodes</h1>'
+      abstract: true,
+      template: '<div ui-view=></div>'
+    })
+    .state('test', {
+      url: '/test',
+      parent: 'xos.core',
+      template: '<h1>Child</h1>'
     });
 }