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> => This sub-service allows you to import data L0, L1 or SR3/SR4</li><li>/export-rest => => 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> => This sub-service allows you to import data L0, L1 or SR3/SR4</li><li>/export-rest => => 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