From 206bfdf42b808afb893214a6a6c828ff272f8f82 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr>
Date: Tue, 28 Jun 2022 12:13:38 +0200
Subject: [PATCH] Webpages => done

---
 .../app/instance/instance.component.spec.ts   | 46 +++++++++---------
 client/src/app/instance/instance.component.ts |  3 +-
 .../components/progress-bar.component.spec.ts | 10 ----
 .../webpage/containers/webpage.component.ts   |  1 +
 .../dynamic-router-link.component.html        |  3 +-
 .../dynamic-router-link.component.ts          |  1 +
 .../parsers/dynamic-router-link-parser.ts     | 47 +++++++++----------
 .../containers/portal-home.component.spec.ts  |  5 --
 client/src/test-data.ts                       | 15 ------
 conf-dev/create-db.sh                         |  2 +-
 10 files changed, 54 insertions(+), 79 deletions(-)

diff --git a/client/src/app/instance/instance.component.spec.ts b/client/src/app/instance/instance.component.spec.ts
index c501001a..fc0f2365 100644
--- a/client/src/app/instance/instance.component.spec.ts
+++ b/client/src/app/instance/instance.component.spec.ts
@@ -9,22 +9,27 @@ import { of } from 'rxjs';
 import { InstanceComponent } from './instance.component';
 import { AppConfigService } from 'src/app/app-config.service';
 import * as authActions from 'src/app/auth/auth.actions';
-import { Instance } from '../metamodel/models';
+import { Instance, WebpageFamily, Webpage } from '../metamodel/models';
 import { UserProfile } from '../auth/user-profile.model';
 import * as datasetFamilyActions from '../metamodel/actions/dataset-family.actions';
 import * as datasetActions from '../metamodel/actions/dataset.actions';
 import * as datasetGroupActions from 'src/app/metamodel/actions/dataset-group.actions';
+import * as webpageFamilyActions from 'src/app/metamodel/actions/webpage-family.actions';
+import * as webpageActions from 'src/app/metamodel/actions/webpage.actions';
 
 describe('[Instance] InstanceComponent', () => {
-    @Component({ selector: 'app-navbar', template: '' })
+    @Component({ selector: 'app-instance-navbar', template: '' })
     class NavbarStubComponent {
-        @Input() links: {label: string, icon: string, routerLink: string}[];
         @Input() isAuthenticated: boolean;
         @Input() userProfile: UserProfile = null;
-        @Input() baseHref: string;
+        @Input() userRoles: string[];
         @Input() authenticationEnabled: boolean;
         @Input() apiUrl: string;
+        @Input() adminRoles: string[];
         @Input() instance: Instance;
+        @Input() webpageFamilyList: WebpageFamily[] = null;
+        @Input() webpageList: Webpage[] = null;
+        @Input() firstWebpage: Webpage = null;
     }
 
     let component: InstanceComponent;
@@ -73,11 +78,6 @@ describe('[Instance] InstanceComponent', () => {
             design_background_color: 'darker green',
             design_logo: '/path/to/logo',
             design_favicon: '/path/to/favicon',
-            home_component: 'HomeComponent',
-            home_component_config: {
-                home_component_text: 'Description',
-                home_component_logo: '/path/to/logo'
-            },
             samp_enabled: true,
             back_to_portal: true,
             search_by_criteria_allowed: true,
@@ -90,32 +90,34 @@ describe('[Instance] InstanceComponent', () => {
             nb_dataset_families: 1,
             nb_datasets: 2
         };
+
+        const webpage: Webpage = {
+            id: 1,
+            label: 'Home',
+            title: 'Home',
+            display: 10,
+            icon: 'fas fa-home',
+            content: '<p>Hello</p>',
+            id_webpage_family: 1
+        }
+
         component.instance = of(instance);
+        component.firstWebpage = of(webpage);
         const spy = jest.spyOn(store, 'dispatch');
-        const expectedLinks = [
-            { label: 'Home', icon: 'fas fa-home', routerLink: 'home' },
-            { label: 'Search', icon: 'fas fa-search', routerLink: 'search' },
-            { label: 'Search multiple', icon: 'fas fa-search-plus', routerLink: 'search-multiple' },
-            { label: 'Documentation', icon: 'fas fa-question', routerLink: 'documentation' }
-        ];
         component.ngOnInit();
         Promise.resolve(null).then(function() {
-            expect(spy).toHaveBeenCalledTimes(3);
+            expect(spy).toHaveBeenCalledTimes(5);
             expect(spy).toHaveBeenCalledWith(datasetFamilyActions.loadDatasetFamilyList());
             expect(spy).toHaveBeenCalledWith(datasetActions.loadDatasetList());
             expect(spy).toHaveBeenCalledWith(datasetGroupActions.loadDatasetGroupList());
-            expect(component.links).toEqual(expectedLinks);
+            expect(spy).toHaveBeenCalledWith(webpageFamilyActions.loadWebpageFamilyList());
+            expect(spy).toHaveBeenCalledWith(webpageActions.loadWebpageList());
             expect(component.favIcon.href).toEqual('http://localhost/undefined/instance/myInstance/file-explorer/path/to/favicon');
             expect(component.title.textContent).toEqual('My Instance');
             done();
         });
     });
 
-    it('#getBaseHref() should return base href config key value', () => {
-        appConfigServiceStub.baseHref = '/my-project';
-        expect(component.getBaseHref()).toBe('/my-project');
-    });
-
     it('#authenticationEnabled() should return authentication enabled config key value', () => {
         appConfigServiceStub.authenticationEnabled = true;
         expect(component.getAuthenticationEnabled()).toBeTruthy();
diff --git a/client/src/app/instance/instance.component.ts b/client/src/app/instance/instance.component.ts
index c6d60394..2545b07d 100644
--- a/client/src/app/instance/instance.component.ts
+++ b/client/src/app/instance/instance.component.ts
@@ -95,7 +95,7 @@ export class InstanceComponent implements OnInit, OnDestroy {
             }
         });
         this.firstWebpageSubscription = this.firstWebpage.subscribe(webpage => {
-            if (webpage) {
+            if (webpage && this.router.url === '/instance/default') {
                 this.router.navigate(['webpage', webpage.id], { relativeTo: this.route });
             }
         });
@@ -170,5 +170,6 @@ export class InstanceComponent implements OnInit, OnDestroy {
      */
     ngOnDestroy() {
         if (this.instanceSubscription) this.instanceSubscription.unsubscribe();
+        if (this.firstWebpageSubscription) this.firstWebpageSubscription.unsubscribe();
     }
 }
diff --git a/client/src/app/instance/search/components/progress-bar.component.spec.ts b/client/src/app/instance/search/components/progress-bar.component.spec.ts
index 280fd988..bc4b4f02 100644
--- a/client/src/app/instance/search/components/progress-bar.component.spec.ts
+++ b/client/src/app/instance/search/components/progress-bar.component.spec.ts
@@ -54,11 +54,6 @@ describe('[Instance][Search][Component] ProgressBarComponent', () => {
             design_background_color: 'darker green',
             design_logo: 'path/to/logo',
             design_favicon: 'path/to/favicon',
-            home_component: 'HomeComponent',
-            home_component_config: {
-                home_component_text: 'Description',
-                home_component_logo: 'path/to/logo'
-            },
             samp_enabled: true,
             back_to_portal: true,
             search_by_criteria_allowed: true,
@@ -93,11 +88,6 @@ describe('[Instance][Search][Component] ProgressBarComponent', () => {
             design_background_color: 'darker green',
             design_logo: 'path/to/logo',
             design_favicon: 'path/to/favicon',
-            home_component: 'HomeComponent',
-            home_component_config: {
-                home_component_text: 'Description',
-                home_component_logo: 'path/to/logo'
-            },
             samp_enabled: true,
             back_to_portal: true,
             search_by_criteria_allowed: true,
diff --git a/client/src/app/instance/webpage/containers/webpage.component.ts b/client/src/app/instance/webpage/containers/webpage.component.ts
index c659ad6a..b1d9d4b1 100644
--- a/client/src/app/instance/webpage/containers/webpage.component.ts
+++ b/client/src/app/instance/webpage/containers/webpage.component.ts
@@ -24,6 +24,7 @@ import * as webpageSelector from 'src/app/metamodel/selectors/webpage.selector';
     templateUrl: 'webpage.component.html'
 })
 export class WebpageComponent {
+    public title: HTMLLinkElement = document.querySelector('#title');
     public webpageListIsLoading: Observable<boolean>;
     public webpageListIsLoaded: Observable<boolean>;
     public webpage: Observable<Webpage>;
diff --git a/client/src/app/instance/webpage/hooks/components/dynamic-router-link.component.html b/client/src/app/instance/webpage/hooks/components/dynamic-router-link.component.html
index aa0d6e16..2f43ac7e 100644
--- a/client/src/app/instance/webpage/hooks/components/dynamic-router-link.component.html
+++ b/client/src/app/instance/webpage/hooks/components/dynamic-router-link.component.html
@@ -1,6 +1,7 @@
 <a *ngIf="isExternalLink()"
-    href="{{ link }}"
+    [href]="link"
     [ngClass]="css"
+    [target]="target ? target : '_self'"
 >
     <ng-container *ngTemplateOutlet="contentTpl"></ng-container>
 </a>
diff --git a/client/src/app/instance/webpage/hooks/components/dynamic-router-link.component.ts b/client/src/app/instance/webpage/hooks/components/dynamic-router-link.component.ts
index 4cd0537e..d0e20e09 100644
--- a/client/src/app/instance/webpage/hooks/components/dynamic-router-link.component.ts
+++ b/client/src/app/instance/webpage/hooks/components/dynamic-router-link.component.ts
@@ -10,6 +10,7 @@ export class DynamicRouterLinkComponent {
     @Input() queryParams: {[key: string]: any};
     @Input() anchorFragment: string;
     @Input() css: string;
+    @Input() target: string;
 
     isExternalLink() {
         return this.link.startsWith('http');
diff --git a/client/src/app/instance/webpage/hooks/parsers/dynamic-router-link-parser.ts b/client/src/app/instance/webpage/hooks/parsers/dynamic-router-link-parser.ts
index 9a283f52..cc0242d8 100644
--- a/client/src/app/instance/webpage/hooks/parsers/dynamic-router-link-parser.ts
+++ b/client/src/app/instance/webpage/hooks/parsers/dynamic-router-link-parser.ts
@@ -9,6 +9,7 @@ export class DynamicRouterLinkParser implements HookParser {
     linkClosingTagRegex: RegExp;
     hrefAttrRegex: RegExp;
     classAttrRegex: RegExp;
+    targetAttrRegex: RegExp;
 
     constructor(private hookFinder: HookFinder) {
         const hrefAttr = '\\s+href\=\\"([^\\"]*?)\\"';
@@ -20,6 +21,7 @@ export class DynamicRouterLinkParser implements HookParser {
         this.linkClosingTagRegex = new RegExp('<\\/a>',  'gim');
         this.hrefAttrRegex = new RegExp(hrefAttr, 'im');
         this.classAttrRegex = new RegExp('\\s+class\=\\"([^\\"]*?)\\"', 'im');
+        this.targetAttrRegex = new RegExp('\\s+target\=\\"([^\\"]*?)\\"', 'im')
     }
 
     public findHooks(content: string, context: any): Array<HookPosition> {
@@ -39,8 +41,18 @@ export class DynamicRouterLinkParser implements HookParser {
         // We can reuse the hrefAttrRegex here as its first capture group is the relative part of the url, 
         // e.g. '/jedi/windu' from 'https://www.mysite.com/jedi/windu', which is what we need
         const hrefAttrMatch = hookValue.openingTag.match(this.hrefAttrRegex);
-        let relativeLink = hrefAttrMatch[1];
-        
+        let link = hrefAttrMatch[1];
+
+        // The relative part of the link may still contain the query string and the 
+        // anchor fragment, so we need to split it up accordingly
+        const anchorFragmentSplit = link.split('#');
+        link = anchorFragmentSplit[0];
+        const anchorFragment = anchorFragmentSplit.length > 1 ? anchorFragmentSplit[1] : null;
+
+        const queryParamsSplit = link.split('?');
+        link = queryParamsSplit[0];
+        const queryParams = queryParamsSplit.length > 1 ? this.parseQueryString(queryParamsSplit[1]) : {};
+
         // Select css part
         let css = null;
         const classAttrMatch = hookValue.openingTag.match(this.classAttrRegex);
@@ -48,38 +60,25 @@ export class DynamicRouterLinkParser implements HookParser {
             css = classAttrMatch[1];
         }
 
-        // The relative part of the link may still contain the query string and the 
-        // anchor fragment, so we need to split it up accordingly
-        const anchorFragmentSplit = relativeLink.split('#');
-        relativeLink = anchorFragmentSplit[0];
-        const anchorFragment = anchorFragmentSplit.length > 1 ? anchorFragmentSplit[1] : null;
-
-        const queryParamsSplit = relativeLink.split('?');
-        relativeLink = queryParamsSplit[0];
-        const queryParams = queryParamsSplit.length > 1 ? this.parseQueryString(queryParamsSplit[1]) : {};
+        // Select target part
+        let target = null;
+        const targetAttrMatch = hookValue.openingTag.match(this.targetAttrRegex);
+        if (targetAttrMatch) {
+            target = targetAttrMatch[1];
+        }
 
         // Give all of these to our DynamicRouterLinkComponent as inputs and we're done!
         return {
             inputs: {
-                link: relativeLink,
+                link,
                 queryParams: queryParams,
                 anchorFragment: anchorFragment,
-                css
+                css,
+                target
             }
         };
     }
 
-    /**
-     * A helper function that safely escapes the special regex chars of any string so it
-     * can be used literally in a Regex.
-     * Approach by coolaj86 & Darren Cook @ https://stackoverflow.com/a/6969486/3099523
-     *
-     * @param string - The string to escape
-     */
-    private escapeRegExp(string) {
-        return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
-    }
-
     /**
      * A helper function that transforms a query string into a QueryParams object
      * Approach by Wolfgang Kuehn @ https://stackoverflow.com/a/8649003/3099523
diff --git a/client/src/app/portal/containers/portal-home.component.spec.ts b/client/src/app/portal/containers/portal-home.component.spec.ts
index 66dd2b62..0c9d4f20 100644
--- a/client/src/app/portal/containers/portal-home.component.spec.ts
+++ b/client/src/app/portal/containers/portal-home.component.spec.ts
@@ -53,11 +53,6 @@ describe('[Instance][Portal][Container] PortalHomeComponent', () => {
         expect(component).toBeDefined();
     });
 
-    it('#getBaseHref() should return base href config key value', () => {
-        appConfigServiceStub.baseHref = '/my-project';
-        expect(component.getBaseHref()).toBe('/my-project');
-    });
-
     it('#authenticationEnabled() should return authentication enabled config key value', () => {
         appConfigServiceStub.authenticationEnabled = true;
         expect(component.getAuthenticationEnabled()).toBeTruthy();
diff --git a/client/src/test-data.ts b/client/src/test-data.ts
index a7f29cf4..40464d82 100644
--- a/client/src/test-data.ts
+++ b/client/src/test-data.ts
@@ -60,11 +60,6 @@ export const INSTANCE_LIST: Instance[] = [
         design_background_color: 'darker green',
         design_logo: 'path/to/logo',
         design_favicon: 'path/to/favicon',
-        home_component: 'HomeComponent',
-        home_component_config: {
-            home_component_text: 'Description',
-            home_component_logo: 'path/to/logo'
-        },
         samp_enabled: true,
         back_to_portal: true,
         search_by_criteria_allowed: false,
@@ -93,11 +88,6 @@ export const INSTANCE_LIST: Instance[] = [
         design_background_color: 'darker green',
         design_logo: 'path/to/logo',
         design_favicon: 'path/to/favicon',
-        home_component: 'HomeComponent',
-        home_component_config: {
-            home_component_text: 'Description',
-            home_component_logo: 'path/to/logo'
-        },
         samp_enabled: true,
         back_to_portal: true,
         search_by_criteria_allowed: false,
@@ -128,11 +118,6 @@ export const INSTANCE: Instance = {
     design_background_color: 'darker green',
     design_logo: '/path/to/logo',
     design_favicon: '/path/to/favicon',
-    home_component: 'HomeComponent',
-    home_component_config: {
-        home_component_text: 'Description',
-        home_component_logo: '/path/to/logo'
-    },
     samp_enabled: true,
     back_to_portal: true,
     search_by_criteria_allowed: false,
diff --git a/conf-dev/create-db.sh b/conf-dev/create-db.sh
index 8a87a2b9..85345caa 100644
--- a/conf-dev/create-db.sh
+++ b/conf-dev/create-db.sh
@@ -112,4 +112,4 @@ curl -d '{"id":15,"name":"src_id","label":"src_id","form_label":"SRC ID","descri
 
 # Add webpages
 curl -d '{"label":"Default","icon":null,"display":10}' --header 'Content-Type: application/json' -X POST http://localhost/instance/default/webpage-family
-curl -d '{"label":"Home","icon":"fas fa-home","display":10,"title":"Home","content":"<div class=\"row align-items-center jumbotron\"><div class=\"col-6 col-md-4 order-md-2 mx-auto text-center\"><img class=\"img-fluid mb-3 mb-md-0\" src=\"http://localhost:8080/instance/default/file-explorer/home_component_logo.png\" alt=\"Instance logo\"></div><div class=\"col-md-8 order-md-1 text-justify pr-md-5\"><h2 class=\"mb-3\">Welcome to the ANIS default instance</h2><p class=\"lead\">This service provides several sub-services to interact with the database.<br>Here is a brief presentation of these sub-services:</p><ul class=\"lead\"><li><a href=\"https://drf-gitlab.cea.fr/svom/sdb/api-import/-/wikis/home\">/import/</a> =&gt; This sub-service allows you to import data L0, L1 or SR3/SR4</li><li>/export-rest =&gt; =&gt; This sub-service allows you to request and export data from the database in json format with a specific URL. To build this URL you could <a class=\"btn btn-warning\" href=\"instance/default/search\">Go to search form</a></li></ul></div></div>"}' --header 'Content-Type: application/json' -X POST http://localhost/webpage-family/1/webpage
+curl -d '{"label":"Home","icon":"fas fa-home","display":10,"title":"Home","content":"<div class=\"row align-items-center jumbotron\"><div class=\"col-6 col-md-4 order-md-2 mx-auto text-center\"><img class=\"img-fluid mb-3 mb-md-0\" src=\"http://localhost:8080/instance/default/file-explorer/home_component_logo.png\" alt=\"Instance logo\"></div><div class=\"col-md-8 order-md-1 text-justify pr-md-5\"><h2 class=\"mb-3\">Welcome to the ANIS default instance</h2><p class=\"lead\">This service provides several sub-services to interact with the database.<br>Here is a brief presentation of these sub-services:</p><ul class=\"lead\"><li><a href=\"https://drf-gitlab.cea.fr/svom/sdb/api-import/-/wikis/home\">/import/</a> =&gt; This sub-service allows you to import data L0, L1 or SR3/SR4</li><li>/export-rest =&gt; =&gt; This sub-service allows you to request and export data from the database in json format with a specific URL. To build this URL you could <a class=\"btn btn-warning\" href=\"../../search\">Go to search form</a></li></ul></div></div>"}' --header 'Content-Type: application/json' -X POST http://localhost/webpage-family/1/webpage
-- 
GitLab