561943 - Nodeprovider: Features Prio 1

Added parameter 'navigator.restoreTreeState'

Change-Id: I0430813c5f2dd1c53d865e0b3aae85aa6f3dbf16
Signed-off-by: Matthias Koller <m.koller@peak-solution.de>
diff --git a/README.md b/README.md
index 49dc779..53e285c 100644
--- a/README.md
+++ b/README.md
@@ -447,7 +447,11 @@
 ### System scope
 
 System scoped preference are applied globally.
-
+* Restore tree state
+  The  navigator is able to save the state of the tree in the Browsers local storage and reopen the tree in the saved state after page reload or logout.
+  
+  The parameter `navigator.restoreTreeState` with value `true` will save the state of the tree.
+  
 * Node provider  
   The navigation tree structure can be defined via a node provider. The default node provider is set in `nucleus/webclient/src/main/webapp/src/app/navigator/defaultnodeprovider.json`.  
   It is recommended not to change the default node provider. Instead new node providers can be added as preferences.  
diff --git a/nucleus/webclient/src/main/webapp/src/app/navigator/mdm-navigator.component.html b/nucleus/webclient/src/main/webapp/src/app/navigator/mdm-navigator.component.html
index 87dbf69..edca911 100644
--- a/nucleus/webclient/src/main/webapp/src/app/navigator/mdm-navigator.component.html
+++ b/nucleus/webclient/src/main/webapp/src/app/navigator/mdm-navigator.component.html
@@ -20,9 +20,8 @@
     selectionMode="multiple"
     [(selection)]="selectedNodes"
     (onNodeExpand)="onNodeExpand($event)"
+    (onNodeCollapse)="onNodeCollapse($event)"
     (onNodeSelect)="onNodeSelect($event)"
-    (onNodeExpand)="saveNavigatorStateInLocalStorage($event)"
-    (onNodeCollapse)="saveNavigatorStateInLocalStorage($event)"
     [contextMenu]="cm"
     >
   <ng-template let-node pTemplate="default">
diff --git a/nucleus/webclient/src/main/webapp/src/app/navigator/mdm-navigator.component.ts b/nucleus/webclient/src/main/webapp/src/app/navigator/mdm-navigator.component.ts
index f029c98..5732364 100644
--- a/nucleus/webclient/src/main/webapp/src/app/navigator/mdm-navigator.component.ts
+++ b/nucleus/webclient/src/main/webapp/src/app/navigator/mdm-navigator.component.ts
@@ -19,7 +19,6 @@
 import { map, tap, startWith, filter, flatMap } from 'rxjs/operators';
 
 import { MDMItem } from '../core/mdm-item';
-import { Node } from '../navigator/node';
 import { NodeService } from '../navigator/node.service';
 import { QueryService } from '../tableview/query.service';
 import { NodeproviderService, MDMTreeNode } from './nodeprovider.service';
@@ -31,7 +30,8 @@
 import { TranslateService } from '@ngx-translate/core';
 import { streamTranslate, TRANSLATE } from '../core/mdm-core.module';
 import { ActivatedRoute, Router } from '@angular/router';
-import { forkJoin, combineLatest, Subscription } from 'rxjs';
+import { combineLatest, Observable, Subscription } from 'rxjs';
+import { PreferenceService } from '@core/preference.service';
 
 @Component({
   selector: 'mdm-navigator',
@@ -48,6 +48,7 @@
 
   selectedNodes: TreeNode[] = [];
   nodes: TreeNode[] = [];
+  restoreTreeState: Observable<boolean>;
   lastClickTime = 0;
   nodeProviderSubscription: Subscription;
 
@@ -71,6 +72,7 @@
     private navigatorService: NavigatorService,
     private notificationService: MDMNotificationService,
     private translateService: TranslateService,
+    private preferenceService: PreferenceService,
     private router: Router,
     private route: ActivatedRoute) {
   }
@@ -87,6 +89,10 @@
       (msg: string) => this.AlrtItemTypeNotSupported = msg);
 
 
+    this.restoreTreeState = this.preferenceService.getPreference("navigator.restoreTreeState").pipe(
+        map(prefs => prefs.map(p => p.value).some(p => p === 'true'))
+    );
+
     // make sure navigate parameter is handled after nodes are loaded
     this.loadRootNodes().subscribe(
       treeNodes => {
@@ -120,41 +126,38 @@
   }
   
   restoreLastNavigatorState() {
-    const rootNodesOfTreeBefore = this.getStoredNavigatorState();
+    this.restoreTreeState.subscribe(restoreState => {
+      if (restoreState) {
+        const navigatorState = this.getStoredNavigatorState();
 
-    if (rootNodesOfTreeBefore != null) {
-      this.nodeProviderSubscription.unsubscribe();
-      this.nodeproviderService.setActiveNodeprovider(rootNodesOfTreeBefore.nodeprovider_id);
-      this.loadRootNodes().subscribe(rootNodes => {
-        this.nodes = rootNodes;
-        this.restoreTreeNodes(this.nodes, rootNodesOfTreeBefore.nodes);
-        this.subscribeToNodeProviderChanges();
-      });
+        if (navigatorState != null) {
+          this.nodeProviderSubscription.unsubscribe();
+          this.nodeproviderService.setActiveNodeprovider(navigatorState.nodeprovider_id);
+          this.loadRootNodes().subscribe(rootNodes => {
+            this.nodes = rootNodes;
+            this.restoreTreeNodes(this.nodes, navigatorState.nodes);
+            this.subscribeToNodeProviderChanges();
+          });
+        }
+      }
+    });
+  }
+
+  restoreTreeNodes(treeNodes: TreeNode[], labelsToExpand: Map<string, any>) {
+    for (let nodeToRestore of Array.from(labelsToExpand.entries())) {
+      const nodeToExpand = this.findTreeNodeByLabel(treeNodes, nodeToRestore[0]);
+      this.loadNodeItemAndExpand(nodeToExpand, nodeToRestore[1]);
     }
   }
 
-  restoreTreeNodes(nodesToRestore: TreeNode[], lastNodes: TreeNode[]) {
-    for (let nodeToRestore of nodesToRestore) {
-      const lastNode = this.findTreeNode(lastNodes, nodeToRestore.data);
-      this.loadNodeItemAndExpand(nodeToRestore, lastNode);
-    }
-  }
-
-  loadNodeItemAndExpand(nodeOfCurrentTree: TreeNode, nodeOfTreeBefore: TreeNode) {
-    if (nodeOfTreeBefore != null && !nodeOfCurrentTree.leaf && nodeOfTreeBefore.expanded) {
-      return this.getChildren(nodeOfCurrentTree.data).subscribe(
+  loadNodeItemAndExpand(nodeToExpand: TreeNode, labelsToExpand: Map<string, any>) {
+    if (nodeToExpand) {
+      return this.getChildren(nodeToExpand.data).subscribe(
         nodes => {
-          nodeOfCurrentTree.children = nodes;
+          nodeToExpand.children = nodes;
+          nodeToExpand.expanded = true;
 
-          const lastChilds = nodeOfTreeBefore.children;
-          nodeOfCurrentTree.expanded = true;
-
-          for (const child of nodeOfCurrentTree.children) {
-            const lastNode = this.findTreeNode(lastChilds, child.data);
-            this.loadNodeItemAndExpand(child, lastNode);
-          }
-
-
+          this.restoreTreeNodes(nodeToExpand.children, labelsToExpand);
         },
         error => this.notificationService.notifyError(
           this.translateService.instant('navigator.mdm-navigator.err-cannot-load-nodes'), error)
@@ -162,10 +165,10 @@
     }
   }
 
-  findTreeNode(nodes: TreeNode[], mdmNode: MDMTreeNode) {
+  findTreeNodeByLabel(nodes: TreeNode[], nodeLabel: string) {
     for (const node of nodes) {
       const data = node.data;
-      if (data != null && data.source === mdmNode.source && data.type === mdmNode.type && data.id === mdmNode.id) {
+      if (data != null && data.label === nodeLabel) {
         return node;
       }
     }
@@ -236,7 +239,9 @@
 
     if (event.originalEvent.timeStamp - this.lastClickTime < 300) {
       if (!event.node.expanded && !event.node.children) {
-        this.loadNode(event.node as TreeNode);
+        this.onNodeExpand(event);
+      } else {
+        this.onNodeCollapse(event);
       }
       event.node.expanded = !event.node.expanded;
     }
@@ -245,29 +250,37 @@
 
   onNodeExpand(event) {
     if (event.node) {
-      this.loadNode(event.node as TreeNode);
+      this.loadChildren(event.node as TreeNode);
+      this.saveNavigatorStateInLocalStorage();
     }
   }
 
-  loadNode(node: TreeNode) {
-    if (node && node.children == undefined) {
-      return this.loadNodeItem(node);
+  onNodeCollapse(event) {
+    if (event.node) {
+      this.unloadChildren(event.node as TreeNode);
+      this.saveNavigatorStateInLocalStorage();
     }
   }
 
-  loadNodeItem(node: TreeNode) {
-    if (!node.leaf) {
-      return this.getChildren(node.data as MDMTreeNode).pipe(
+  loadChildren(parentNode: TreeNode) {
+    if (parentNode && parentNode.children == undefined && !parentNode.leaf) {
+      return this.getChildren(parentNode.data as MDMTreeNode).pipe(
         startWith([this.loadingNode]),
-        tap(nodes => (nodes && nodes.length === 0) ? node.leaf = true : node.leaf = false))
+        tap(nodes => (nodes && nodes.length === 0) ? parentNode.leaf = true : parentNode.leaf = false))
         .subscribe(
-          nodes => node.children = nodes,
+          nodes => parentNode.children = nodes,
           error => this.notificationService.notifyError(
             this.translateService.instant('navigator.mdm-navigator.err-cannot-load-nodes'), error)
         );
     }
   }
 
+  unloadChildren(parentNode: TreeNode) {
+    if (parentNode) {
+      parentNode.children = undefined;
+    }
+  }
+
   reloadTree() {
     this.loadRootNodes()
       .subscribe(
@@ -313,7 +326,7 @@
 
   refresh() {
     for (const node of this.selectedNodes) {
-      this.loadNodeItem(node);
+      this.loadChildren(node);
     }
   }
 
@@ -321,110 +334,73 @@
     this.restoreLastNavigatorState();
   }
 
-  saveNavigatorStateInLocalStorage(event) {
-    this.nodeproviderService.getActiveNodeprovider().subscribe(np => {
-      let jsonVal = JSON.stringify({ nodeprovider_id: np, nodes: this.nodes }, this.replacer);
-      console.log('storedRootNodes', jsonVal);
-      localStorage.setItem('storedRootNodes', jsonVal);
+  saveNavigatorStateInLocalStorage() {
+    this.restoreTreeState.subscribe(restoreState => {
+      if (restoreState) {
+        this.nodeproviderService.getActiveNodeprovider().subscribe(np => {
+          let jsonVal = JSON.stringify({ nodeprovider_id: np, nodes: this.getOpenNodes(this.nodes) }, this.replacer);
+          localStorage.setItem('storedRootNodes', jsonVal);
+        });
+      }
     });
   }
 
-  getStoredNavigatorState() : { nodeprovider_id: string, nodes: TreeNode[] }{
-    return JSON.parse(localStorage.getItem('storedRootNodes'));
+  getStoredNavigatorState() : { nodeprovider_id: string, nodes: Map<string, any> }{
+    return JSON.parse(localStorage.getItem('storedRootNodes'), this.reviver);
   }
 
   replacer(key, value) {
-    if (key == 'parent') {
-      return undefined;
+    const originalObject = this[key];
+    if(originalObject instanceof Map) {
+      return {
+        dataType: 'Map',
+        value: Array.from(originalObject.entries()), // or with spread: value: [...originalObject]
+      };
     } else {
       return value;
     }
+  }
 
+  reviver(key, value) {
+    if(typeof value === 'object' && value !== null) {
+      if (value.dataType === 'Map') {
+        return new Map(value.value);
+      }
+    }
+    return value;
+  }
+
+  getOpenNodes(treeNodes: TreeNode[]) : Map<string, any> {
+    let m = new Map<string, any>();
+
+    treeNodes.filter(n => n.children).forEach(n => m.set(n.label, this.getOpenNodes(n.children)));
+    return m;
   }
 
   openInTree(items: MDMItem[]) {
     this.selectedNodes = [];
     items.forEach(item => {
-      this.nodeproviderService.getTreePath(item).subscribe(treepath => 
-          this.expandTreePath(treepath));
-      /*let pathTypes = this.nodeproviderService.getPathTypes(item.type);
-      if (pathTypes.length === 0) {
-        alert(this.AlrtItemTypeNotSupported);
-      } else {
-        let env = this.nodes.find(e => item.source === e.data.source);
-        env.expanded = true;
-        this.openChildrenRecursive(item, env, pathTypes, 1);
-      }*/
+      this.nodeproviderService.getTreePath(item).subscribe(treepath => this.expandTreePath(treepath));
     });
   }
-expandTreePath(treePath: MDMTreeNode[]) {
-  console.log("expand", treePath);
-  let current = this.nodes;
-  for (let mdmTreeNode of treePath.reverse()) {
-    console.log("current", current, "mdmTreeNode", mdmTreeNode);
-    current = this.expandChild(current, mdmTreeNode);
-  }
-  console.log("current", current);
-  this.onNodeSelect(current);
-}
 
-expandChild(children: TreeNode[], treePath: MDMTreeNode) {
-  let s = children.filter(node => node.data.source === treePath.source && node.data.id === treePath.id)[0];
-  s.expanded = true;
-  return s.children;
-}
-
-/*
-  openChildrenRecursive(item: MDMItem, current: TreeNode, pathTypes: string[], iii: number) {
-    if (current.children) {
-      this.expander(item, current, pathTypes, iii);
-    } else {
-      this.getChildren(current).pipe(
-        startWith([this.loadingNode]),
-        tap(n => current.children = n))
-        .subscribe(
-          () => this.expander(item, current, pathTypes, iii),
-          error => this.notificationService.notifyError(
-            this.translateService.instant('navigator.mdm-navigator.err-cannot-open-node'), error)
-        );
+  expandTreePath(treePath: MDMTreeNode[]) {
+    console.log("expand", treePath);
+    let current = this.nodes;
+    for (let mdmTreeNode of treePath.reverse()) {
+      console.log("current", current, "mdmTreeNode", mdmTreeNode);
+      current = this.expandChild(current, mdmTreeNode);
     }
+    console.log("current", current);
+    this.onNodeSelect(current);
   }
 
-  expander(item: MDMItem, current: TreeNode, pathTypes: string[], iii: number) {
-    let expandList: string[] = [];
-    this.nodeService.searchNodes('filter=' + item.type + '.Id eq "' + item.id + '"',
-      item.source, pathTypes[iii])
-      .subscribe(
-        nodes => {
-          expandList = nodes.map(n => n.id);
-          current.children.filter(node => expandList.findIndex(
-            i => node.data ? i === node.data.id : false) > -1
-          )
-            .forEach(node => {
-              if (++iii < pathTypes.length) {
-                node.expanded = true;
-                this.openChildrenRecursive(item, node, pathTypes, iii);
-                this.scrollToSelectionPrimeNgDataTable(node);
-              } else {
-                this.selectedNodes.push(node);
-                let length = this.selectedNodes.length;
-                if (length === 1) {
-                  this.nodeService.getNodeFromItem(this.selectedNodes[length - 1].data)
-                    .subscribe(
-                      n => this.navigatorService.setSelectedNode(n),
-                      error => this.notificationService.notifyError(
-                        this.translateService.instant('navigator.mdm-navigator.err-cannot-open-node'), error)
-                    );
-                }
-                this.scrollToSelectionPrimeNgDataTable(node);
-              }
-            });
-        },
-        error => this.notificationService.notifyError(
-          this.translateService.instant('navigator.mdm-navigator.err-cannot-open-node'), error)
-      );
+  expandChild(children: TreeNode[], treePath: MDMTreeNode) {
+    let s = children.filter(node => node.data.source === treePath.source && node.data.id === treePath.id)[0];
+    s.expanded = true;
+    return s.children;
   }
-*/
+
   /**
    * PrimeNG does not support scroll to view. This methods implements a
    * workaround by using HTML element IDs.