Merge "Fixing GUI docs"
diff --git a/src/app/core/confirm/confirm.service.spec.ts b/src/app/core/confirm/confirm.service.spec.ts
index 84a61de..d05899d 100644
--- a/src/app/core/confirm/confirm.service.spec.ts
+++ b/src/app/core/confirm/confirm.service.spec.ts
@@ -23,6 +23,8 @@
 let service: IXosConfirm;
 let modal;
 let modalInstance;
+let q;
+let scope;
 
 describe('The XosConfirm service', () => {
 
@@ -33,10 +35,14 @@
 
     angular.mock.inject((
       XosConfirm: IXosConfirm,
-      $uibModal: any
+      $uibModal: any,
+      $q: ng.IQService,
+      $rootScope: ng.IScope
     ) => {
       service = XosConfirm;
       modal = $uibModal;
+      q = $q;
+      scope = $rootScope;
     });
   });
 
@@ -54,19 +60,31 @@
         }]
       };
 
-    it('should open a modal', () => {
-      spyOn(modal, 'open');
+    it('should open the modal', () => {
+      spyOn(modal, 'open').and.returnValue('fake');
       modalInstance = service.open(test1);
       expect(modal.open).toHaveBeenCalled();
+      expect(modalInstance).toEqual('fake');
+      expect(service.modalInstance).toEqual('fake');
     });
-  });
 
-  // describe('the close method', () => {
-  //
-  // });
-  //
-  // describe('the dismiss method', () => {
-  //
-  // });
+    it('should close the modal', (done) => {
+      const p = q.defer();
+      const cb = jasmine.createSpy('cb').and.returnValue(p.promise);
+      service.modalInstance = {
+        close: jasmine.createSpy('close')
+      };
+
+      service.close(cb);
+      expect(cb).toHaveBeenCalled();
+      p.resolve();
+      scope.$apply();
+      expect(service.modalInstance.close).toHaveBeenCalled();
+      done();
+    });
+
+
+
+  });
 
 });
diff --git a/src/app/core/confirm/confirm.service.ts b/src/app/core/confirm/confirm.service.ts
index a8e802e..39336e3 100644
--- a/src/app/core/confirm/confirm.service.ts
+++ b/src/app/core/confirm/confirm.service.ts
@@ -18,36 +18,44 @@
 import {IXosConfirmConfig} from './confirm';
 
 export interface IXosConfirm {
+  modalInstance: any;
   open(config: IXosConfirmConfig) : void;
-  close(cb: Function) : void;
+  close(cb: any) : void;
   dismiss() : void;
 }
 
 export class XosConfirm implements IXosConfirm {
 
-  static $inject = ['$uibModal'];
+  static $inject = ['$uibModal', '$log'];
   public modalInstance;
 
   constructor(
     private $uibModal : any,
+    private $log: ng.ILogService,
   ) {
 
   }
 
   public open(config: IXosConfirmConfig) {
-
+    this.$log.debug('[XosConfirm] called open');
     this.modalInstance = this.$uibModal.open({
-      keyboard: false,
+      keyboard: true,
       component: 'xosConfirm',
       backdrop: 'static',
       resolve: {
         config: () => config
       }
     });
+
     return this.modalInstance;
   }
 
-  public close(cb: Function) {
+  public close(cb: any) {
+    // check if model instance exists
+    if (angular.isUndefined(this.modalInstance)) {
+      this.$log.debug('[XosConfirm] called close without a modalInstance');
+      return;
+    }
     cb()
       .then(() => {
         this.modalInstance.close();
diff --git a/src/app/core/header/header.spec.ts b/src/app/core/header/header.spec.ts
index 13f8b5b..eb236b1 100644
--- a/src/app/core/header/header.spec.ts
+++ b/src/app/core/header/header.spec.ts
@@ -51,11 +51,26 @@
 const infoNotification = {
   model: 'TestModel',
   msg: {
-    changed_fields: ['backend_status'],
+    changed_fields: ['backend_status', 'backend_code'],
     pk: 1,
     object: {
       name: 'TestName',
-      backend_status: '0 - In Progress'
+      backend_status: 'In Progress',
+      backend_code: 0
+    }
+  }
+};
+
+const noNotification = {
+  model: 'TestModel',
+  skip_notification: true,
+  msg: {
+    changed_fields: ['backend_status', 'backend_code'],
+    pk: 1,
+    object: {
+      name: 'TestName',
+      backend_status: 'In Progress',
+      backend_code: 0
     }
   }
 };
@@ -74,6 +89,9 @@
       .value('toastrConfig', MockToastrConfig)
       .value('AuthService', MockAuth)
       .value('XosNavigationService', {})
+      .value('ConfigHelpers', {
+        stateWithParamsForJs: () => null
+      })
       .value('XosKeyboardShortcut', MockXosKeyboardShortcut)
       .value('StyleConfig', {
         logo: 'cord-logo.png',
@@ -92,6 +110,7 @@
 
     // clear notifications
     isolatedScope.notifications = [];
+    MockToastr.info.calls.reset();
   }));
 
   it('should render the appropriate logo', () => {
@@ -116,6 +135,8 @@
   });
 
   it('should configure toastr', () => {
+    delete MockToastrConfig['onTap'];
+
     expect(MockToastrConfig).toEqual({
       newestOnTop: false,
       positionClass: 'toast-top-right',
@@ -129,7 +150,14 @@
     sendEvent(infoNotification);
     scope.$digest();
 
-    expect(MockToastr.info).toHaveBeenCalledWith('Synchronization started for: TestName', 'TestModel');
+    expect(MockToastr.info).toHaveBeenCalledWith('Synchronization started for: TestName', 'TestModel', {extraData: {dest: null}});
+  });
+
+  it('should not display a toastr for a new event that use skip_notification', () => {
+    sendEvent(noNotification);
+    scope.$digest();
+
+    expect(MockToastr.info).not.toHaveBeenCalled();
   });
 
   // TODO test error and success toaster call
diff --git a/src/app/core/header/header.ts b/src/app/core/header/header.ts
index a89199d..a465f8f 100644
--- a/src/app/core/header/header.ts
+++ b/src/app/core/header/header.ts
@@ -26,13 +26,29 @@
 import {IXosStyleConfig} from '../../../index';
 import {IXosSearchService, IXosSearchResult} from '../../datasources/helpers/search.service';
 import {IXosKeyboardShortcutService} from '../services/keyboard-shortcut';
+import {Subscription} from 'rxjs';
+import {IXosConfigHelpersService} from '../services/helpers/config.helpers';
 
 export interface INotification extends IWSEvent {
   viewed?: boolean;
 }
 
 class HeaderController {
-  static $inject = ['$scope', '$rootScope', '$state', 'AuthService', 'SynchronizerStore', 'toastr', 'toastrConfig', 'XosNavigationService', 'StyleConfig', 'SearchService', 'XosKeyboardShortcut'];
+  static $inject = [
+    '$log',
+    '$scope',
+    '$rootScope',
+    '$state',
+    'AuthService',
+    'SynchronizerStore',
+    'toastr',
+    'toastrConfig',
+    'XosNavigationService',
+    'StyleConfig',
+    'SearchService',
+    'XosKeyboardShortcut',
+    'ConfigHelpers'
+  ];
   public notifications: INotification[] = [];
   public newNotifications: INotification[] = [];
   public version: string;
@@ -42,7 +58,10 @@
   public query: string;
   public search: (query: string) => any[];
 
+  private syncStoreSubscription: Subscription;
+
   constructor(
+    private $log: ng.ILogService,
     private $scope: angular.IScope,
     private $rootScope: ng.IScope,
     private $state: IStateService,
@@ -53,8 +72,14 @@
     private NavigationService: IXosNavigationService,
     private StyleConfig: IXosStyleConfig,
     private SearchService: IXosSearchService,
-    private XosKeyboardShortcut: IXosKeyboardShortcutService
+    private XosKeyboardShortcut: IXosKeyboardShortcutService,
+    private ConfigHelpers: IXosConfigHelpersService
   ) {
+
+  }
+
+  $onInit() {
+    this.$log.info('[XosHeader] Setup');
     this.version = require('../../../../package.json').version;
     angular.extend(this.toastrConfig, {
       newestOnTop: false,
@@ -62,10 +87,9 @@
       preventDuplicates: false,
       preventOpenDuplicates: false,
       progressBar: true,
-      // autoDismiss: false,
-      // closeButton: false,
-      // timeOut: 0,
-      // tapToDismiss: false
+      onTap: (toast) => {
+        this.$state.go(toast.scope.extraData.dest.name, toast.scope.extraData.dest.params);
+      }
     });
 
     this.search = (query: string) => {
@@ -94,27 +118,43 @@
 
     this.userEmail = this.authService.getUser() ? this.authService.getUser().email : '';
 
-    this.syncStore.query()
+    this.syncStoreSubscription = this.syncStore.query()
       .subscribe(
         (event: IWSEvent) => {
-          $scope.$evalAsync(() => {
+          this.$scope.$evalAsync(() => {
+
+            if (event.model === 'Diag') {
+              // NOTE skip notifications for Diag model
+              return;
+            }
+
             let toastrMsg: string;
             let toastrLevel: string;
-            if (event.msg.object.backend_status.indexOf('0') > -1) {
+            if (event.msg.object.backend_code === 0) {
               toastrMsg = 'Synchronization started for:';
               toastrLevel = 'info';
             }
-            else if (event.msg.object.backend_status.indexOf('1') > -1) {
+            else if (event.msg.object.backend_code === 1) {
               toastrMsg = 'Synchronization succedeed for:';
               toastrLevel = 'success';
             }
-            else if (event.msg.object.backend_status.indexOf('2') > -1) {
+            else if (event.msg.object.backend_code === 2) {
               toastrMsg = 'Synchronization failed for:';
               toastrLevel = 'error';
             }
 
             if (toastrLevel && toastrMsg) {
-              this.toastr[toastrLevel](`${toastrMsg} ${event.msg.object.name}`, event.model);
+              let modelName = event.msg.object.name;
+              let modelClassName = event.model;
+              if (angular.isUndefined(event.msg.object.name) || event.msg.object.name === null) {
+                modelName = `${event.msg.object.leaf_model_name} [${event.msg.object.id}]`;
+              }
+
+              const dest = this.ConfigHelpers.stateWithParamsForJs(modelClassName, event.msg.object);
+
+              if (!event.skip_notification) {
+                this.toastr[toastrLevel](`${toastrMsg} ${modelName}`, modelClassName, {extraData: {dest: dest}});
+              }
             }
             // this.notifications.unshift(event);
             // this.newNotifications = this.getNewNotifications(this.notifications);
@@ -123,6 +163,11 @@
       );
   }
 
+  $onDestroy() {
+    this.$log.info('[XosHeader] Teardown');
+    this.syncStoreSubscription.unsubscribe();
+  }
+
   public getLogo(): string {
     return require(`../../images/brand/${this.StyleConfig.logo}`);
   }
diff --git a/src/app/core/services/helpers/config.helpers.ts b/src/app/core/services/helpers/config.helpers.ts
index e4a0a70..f566e7a 100644
--- a/src/app/core/services/helpers/config.helpers.ts
+++ b/src/app/core/services/helpers/config.helpers.ts
@@ -153,6 +153,7 @@
           color: 'red',
           cb: (item) => {
             let obj = angular.copy(item);
+            const objName = (angular.isUndefined(obj.name)) ? 'instance' : obj.name;
 
             item.$delete()
               .then((res) => {
@@ -160,10 +161,10 @@
                   // TODO understand why it does not go directly in catch
                   throw new Error();
                 }
-                this.toastr.info(`${model.name} ${obj.name} succesfully deleted`);
+                this.toastr.info(`${model.name} ${objName} successfully deleted`);
               })
               .catch(() => {
-                this.toastr.error(`Error while deleting ${obj.name}`);
+                this.toastr.error(`Error while deleting ${objName}`);
               });
           }
         }
@@ -303,7 +304,7 @@
         return;
       }
 
-      const model = angular.copy(item);
+      const itemCopy = angular.copy(item);
 
       // TODO remove ManyToMany relations and save them separately (how??)
       delete item.networks;
@@ -315,24 +316,26 @@
         }
       });
 
+      const itemName = (angular.isUndefined(itemCopy.name)) ? model.name : itemCopy.name;
+
       item.$save()
         .then((res) => {
           formCfg.feedback = {
             show: true,
-            message: `${model.name} succesfully saved`,
+            message: `${itemName} successfully saved`,
             type: 'success',
             closeBtn: true
           };
-          this.toastr.success(`${model.name} succesfully saved`);
+          this.toastr.success(`${itemName} successfully saved`);
         })
         .catch(err => {
           formCfg.feedback = {
             show: true,
-            message: `Error while saving ${model.name}: ${err.error}. ${err.specific_error || ''}`,
+            message: `Error while saving ${itemName}: ${err.error}. ${err.specific_error || ''}`,
             type: 'danger',
             closeBtn: true
           };
-          this.toastr.error(err.specific_error || '', `Error while saving ${model.name}: ${err.error}`);
+          this.toastr.error(err.specific_error || '', `Error while saving ${itemName}: ${err.error}`);
         });
     };
 
diff --git a/src/app/core/services/keyboard-shortcut.ts b/src/app/core/services/keyboard-shortcut.ts
index b8678f7..9b98b45 100644
--- a/src/app/core/services/keyboard-shortcut.ts
+++ b/src/app/core/services/keyboard-shortcut.ts
@@ -106,8 +106,8 @@
         return;
       }
 
-      if (this.allowedModifiers.indexOf(e.key) > -1) {
-        this.addActiveModifierKey(e.key);
+      if (this.allowedModifiers.indexOf(e.key.toLowerCase()) > -1) {
+        this.addActiveModifierKey(e.key.toLowerCase());
         return;
       }
 
@@ -126,8 +126,8 @@
     });
 
     $('body').on('keyup', (e) => {
-      if (this.allowedModifiers.indexOf(e.key) > -1) {
-        this.removeActiveModifierKey(e.key);
+      if (this.allowedModifiers.indexOf(e.key.toLowerCase()) > -1) {
+        this.removeActiveModifierKey(e.key.toLowerCase());
         return;
       }
     });
diff --git a/src/app/core/side-panel/side-panel.scss b/src/app/core/side-panel/side-panel.scss
index a6c718b..46e84ff 100644
--- a/src/app/core/side-panel/side-panel.scss
+++ b/src/app/core/side-panel/side-panel.scss
@@ -31,7 +31,7 @@
     width: $side-panel-width;
     height: 100%;
     position: fixed;
-    z-index: 9999;
+    z-index: 1049;
     top: 0;
     right: -$side-panel-width;
     background: $background-dark-color;
diff --git a/src/app/core/side-panel/side-panel.ts b/src/app/core/side-panel/side-panel.ts
index 9b2b78f..ba1f5b0 100644
--- a/src/app/core/side-panel/side-panel.ts
+++ b/src/app/core/side-panel/side-panel.ts
@@ -39,5 +39,5 @@
 export const xosSidePanel: angular.IComponentOptions = {
   template: require('./side-panel.html'),
   controllerAs: 'vm',
-  controller: XosSidePanelController
+  controller: XosSidePanelController,
 };
diff --git a/src/app/datasources/stores/synchronizer.store.ts b/src/app/datasources/stores/synchronizer.store.ts
index 635e6db..e1b26b3 100644
--- a/src/app/datasources/stores/synchronizer.store.ts
+++ b/src/app/datasources/stores/synchronizer.store.ts
@@ -36,7 +36,7 @@
         if (!e.msg || !e.msg.changed_fields) {
           return false;
         }
-        return e.msg.changed_fields.indexOf('backend_status') > -1;
+        return (e.msg.changed_fields.indexOf('backend_status') > -1 || e.msg.changed_fields.indexOf('backend_code') > -1) && !e.skip_notification;
       })
       .subscribe(
         (event: IWSEvent) => {
diff --git a/src/app/datasources/websocket/global.ts b/src/app/datasources/websocket/global.ts
index 1367dc7..32f6f4a 100644
--- a/src/app/datasources/websocket/global.ts
+++ b/src/app/datasources/websocket/global.ts
@@ -23,6 +23,7 @@
 
 export interface IWSEvent {
   model: string;
+  skip_notification?: boolean;
   msg: {
     changed_fields: string[],
     object?: any,
@@ -60,18 +61,20 @@
           return;
         }
 
-        this.$log.info(`[WebSocket] Received Event for: ${data.model} [${data.msg.pk}]`);
+        this.$log.info(`[WebSocket] Received Event for: ${data.model} [${data.msg.pk}]`, data);
 
         this._events.next(data);
 
         // NOTE update observers of parent classes
         if (data.msg.object.class_names && angular.isString(data.msg.object.class_names)) {
           const models = data.msg.object.class_names.split(',');
+          let event: IWSEvent = angular.copy(data);
           _.forEach(models, (m: string) => {
             // send event only if the parent class is not the same as the model class
-            if (data.model !== m) {
-              data.model = m;
-              this._events.next(data);
+            if (event.model !== m && m !== 'object') {
+              event.model = m;
+              event.skip_notification = true;
+              this._events.next(event);
             }
           });
         }
diff --git a/src/decorators.ts b/src/decorators.ts
index 790293d..ab80a39 100644
--- a/src/decorators.ts
+++ b/src/decorators.ts
@@ -42,11 +42,12 @@
         // eg: the first parameter is the group name
 
         const msg = arguments[0];
-        if (!isLogEnabled() && msg.indexOf('WebSocket') === 0) {
+
+        if (!isLogEnabled() && msg.indexOf('WebSocket') === -1) {
           return;
         }
 
-        if (!isEventLogEnabled() && msg.indexOf('WebSocket') > 0) {
+        if (!isEventLogEnabled() && msg.indexOf('WebSocket') > -1) {
           return;
         }