diff --git a/src/app/menu.resolver.ts b/src/app/menu.resolver.ts index af7f80eb69b..a2d42823c74 100644 --- a/src/app/menu.resolver.ts +++ b/src/app/menu.resolver.ts @@ -52,6 +52,7 @@ import { environment } from '../environments/environment'; import { SectionDataService } from './core/layout/section-data.service'; import { Section } from './core/layout/models/section.model'; import { NOTIFICATIONS_RECITER_SUGGESTION_PATH } from './admin/admin-notifications/admin-notifications-routing-paths'; +import { LinkIconMenuItemModel } from './shared/menu/menu-item/models/linkicon.model'; /** * Creates all of the app's menus @@ -133,35 +134,76 @@ export class MenuResolver implements Resolve { }; if (section.nestedSections && section.nestedSections.length) { section.nestedSections.forEach((nested) => { - menuList.push({ - id: `explore_nested_${nested.id}`, - parentID: `explore_${section.id}`, - active: false, - visible: true, + if (environment.layout.navbar.showExploreIcons && environment.layout.navbar.showExploreIcons.find(value => value.explore_id.match(nested.id)) !== undefined) { + const config = environment.layout.navbar.showExploreIcons.find(value => value.explore_id.match(nested.id)); + menuList.push({ + id: `explore_nested_${nested.id}`, + parentID: `explore_${section.id}`, + active: false, + visible: true, + model: { + type: MenuItemType.LINKICON, + text: `menu.section.explore_${nested.id}`, + icon: config.icon || undefined, + link: `/explore/${nested.id}` + } as LinkIconMenuItemModel + }); + } else { + menuList.push({ + id: `explore_nested_${nested.id}`, + parentID: `explore_${section.id}`, + active: false, + visible: true, + model: { + type: MenuItemType.LINK, + text: `menu.section.explore_${nested.id}`, + link: `/explore/${nested.id}` + } as LinkMenuItemModel + }); + } + }); + if (environment.layout.navbar.showExploreIcons && environment.layout.navbar.showExploreIcons.find(value => value.explore_id.match(section.id)) !== undefined) { + const config = environment.layout.navbar.showExploreIcons.find(value => value.explore_id.match(section.id)); + parentMenu = { + ...parentMenu, + model: { + type: MenuItemType.LINKICON, + text: `menu.section.explore_${section.id}`, + icon: config.icon || undefined, + } as LinkIconMenuItemModel, + }; + } else { + parentMenu = { + ...parentMenu, + index: 1, + model: { + type: MenuItemType.TEXT, + text: `menu.section.explore_${section.id}` + } as TextMenuItemModel, + }; + } + } else { + if (environment.layout.navbar.showExploreIcons && environment.layout.navbar.showExploreIcons.find(value => value.explore_id.match(section.id)) !== undefined) { + const config = environment.layout.navbar.showExploreIcons.find(value => value.explore_id.match(section.id)); + parentMenu = { + ...parentMenu, + model: { + type: MenuItemType.LINKICON, + text: `menu.section.explore_${section.id}`, + icon: config.icon || undefined, + link: `/explore/${section.id}` + } as LinkIconMenuItemModel + }; + } else { + parentMenu = { + ...parentMenu, model: { type: MenuItemType.LINK, - text: `menu.section.explore_${nested.id}`, - link: `/explore/${nested.id}` + text: `menu.section.explore_${section.id}`, + link: `/explore/${section.id}` } as LinkMenuItemModel - }); - }); - parentMenu = { - ...parentMenu, - index: 1, - model: { - type: MenuItemType.TEXT, - text: `menu.section.explore_${section.id}` - } as TextMenuItemModel, - }; - } else { - parentMenu = { - ...parentMenu, - model: { - type: MenuItemType.LINK, - text: `menu.section.explore_${section.id}`, - link: `/explore/${section.id}` - } as LinkMenuItemModel - }; + }; + } } menuList.push(parentMenu); }); @@ -170,7 +212,6 @@ export class MenuResolver implements Resolve { shouldPersistOnRouteChange: true }))); }); - this.createStatisticsMenu(); return this.waitForMenu$(MenuID.PUBLIC); } diff --git a/src/app/shared/menu/menu-item-type.model.ts b/src/app/shared/menu/menu-item-type.model.ts index 0063ed77e7a..fe0eea30852 100644 --- a/src/app/shared/menu/menu-item-type.model.ts +++ b/src/app/shared/menu/menu-item-type.model.ts @@ -2,5 +2,5 @@ * List of possible MenuItemTypes */ export enum MenuItemType { - TEXT, LINK, ALTMETRIC, SEARCH, ONCLICK, EXTERNAL + TEXT, LINK, ALTMETRIC, SEARCH, ONCLICK, EXTERNAL, LINKICON } diff --git a/src/app/shared/menu/menu-item/linkicon-menu-item.component.html b/src/app/shared/menu/menu-item/linkicon-menu-item.component.html new file mode 100644 index 00000000000..fd11bd13b50 --- /dev/null +++ b/src/app/shared/menu/menu-item/linkicon-menu-item.component.html @@ -0,0 +1,25 @@ + + + {{item.text | translate}} + + + +{{item.text | translate}} + diff --git a/src/app/shared/menu/menu-item/linkicon-menu-item.component.scss b/src/app/shared/menu/menu-item/linkicon-menu-item.component.scss new file mode 100644 index 00000000000..585100474b3 --- /dev/null +++ b/src/app/shared/menu/menu-item/linkicon-menu-item.component.scss @@ -0,0 +1,36 @@ + + .exploreicon { + text-align:center; + } + + p.exploreicon { + margin-bottom: 4px; + } + +.exploreicon { + @media screen and (max-width: map-get($grid-breakpoints, md)) { + text-align:left; + float:left; + padding-right: 16px; + + p.exploreicon { + margin-bottom: 1rem; + } + + } +} + +.nav-link { + @media screen and (max-width: map-get($grid-breakpoints, md)) { + vertical-align:middle; + border-bottom: 2px solid white; + } +} + +.nav-link > p { + @media screen and (max-width: map-get($grid-breakpoints, md)) { + margin-bottom:4px; + vertical-align:middle; + } +} + diff --git a/src/app/shared/menu/menu-item/linkicon-menu-item.component.spec.ts b/src/app/shared/menu/menu-item/linkicon-menu-item.component.spec.ts new file mode 100644 index 00000000000..d395fe9c422 --- /dev/null +++ b/src/app/shared/menu/menu-item/linkicon-menu-item.component.spec.ts @@ -0,0 +1,217 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { LinkIconMenuItemComponent } from './linkicon-menu-item.component'; +import { Router } from '@angular/router'; +import { RouterStub } from '../../testing/router.stub'; +import { RouterLinkDirectiveStub } from '../../testing/router-link-directive.stub'; + +describe('LinkIconMenuItemComponent', () => { + let component: LinkIconMenuItemComponent; + let fixture: ComponentFixture; + let debugElement: DebugElement; + let text; + let link; + let icon; + + describe('with icon and external link', () => { + + function init() { + text = 'HELLO'; + link = 'https://google.com/'; + icon = 'fa fa-user'; + } + + beforeEach(waitForAsync(() => { + init(); + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [LinkIconMenuItemComponent], + providers: [ + {provide: 'itemModelProvider', useValue: {text: text, link: link, icon: icon}}, + {provide: Router, useValue: RouterStub}, + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LinkIconMenuItemComponent); + component = fixture.componentInstance; + debugElement = fixture.debugElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should contain the correct text', () => { + const textContent = debugElement.query(By.css('a')).nativeElement.textContent; + expect(textContent).toEqual(text); + }); + + it('should have the right href attribute', () => { + const links = fixture.debugElement.queryAll(By.css('a')); + expect(links.length).toBe(1); + expect(links[0].nativeElement.href).toBe(link); + }); + + it('should set the right icon', () => { + const menuicon = fixture.debugElement.query(By.css('.exploreicon')).query(By.css('span')); + expect(menuicon.nativeElement.getAttribute('class')).toContain(icon); + }); + }); + + describe('with external link and no icon', () => { + function init() { + text = 'HELLO'; + link = 'https://google.com/'; + icon = undefined; + } + + beforeEach(waitForAsync(() => { + init(); + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [LinkIconMenuItemComponent], + providers: [ + { provide: 'itemModelProvider', useValue: { text: text, link: link, icon: icon } }, + { provide: Router, useValue: RouterStub }, + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LinkIconMenuItemComponent); + component = fixture.componentInstance; + debugElement = fixture.debugElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should contain the correct text', () => { + const textContent = debugElement.query(By.css('a')).nativeElement.textContent; + expect(textContent).toEqual(text); + }); + + it('should have the right href attribute', () => { + const links = fixture.debugElement.queryAll(By.css('a')); + expect(links.length).toBe(1); + expect(links[0].nativeElement.href).toBe(link); + }); + + it('should set no icon', () => { + expect(fixture.debugElement.query(By.css('.exploreicon'))).toBeNull(); + }); + }); + + describe('with internal route and icon', () => { + function init() { + text = 'HELLO'; + link = '/researcherprofiles'; + icon = 'fa fa-user'; + } + + beforeEach(waitForAsync(() => { + init(); + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [LinkIconMenuItemComponent, RouterLinkDirectiveStub], + providers: [ + { provide: 'itemModelProvider', useValue: { text: text, link: link, icon: icon } }, + { provide: Router, useValue: RouterStub }, + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LinkIconMenuItemComponent); + component = fixture.componentInstance; + debugElement = fixture.debugElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should contain the correct text', () => { + const textContent = debugElement.query(By.css('a')).nativeElement.textContent; + expect(textContent).toEqual(text); + }); + + it('should have the right routerLink attribute', () => { + const linkDes = fixture.debugElement.queryAll(By.directive(RouterLinkDirectiveStub)); + const routerLinkQuery = linkDes.map((de) => de.injector.get(RouterLinkDirectiveStub)); + + expect(routerLinkQuery.length).toBe(1); + expect(routerLinkQuery[0].routerLink).toContain(link); + }); + + it('should set the right icon', () => { + const menuicon = fixture.debugElement.query(By.css('.exploreicon')).query(By.css('span')); + expect(menuicon.nativeElement.getAttribute('class')).toContain(icon); + }); + + }); + + describe('with internal route and no icon', () => { + + function init() { + text = 'HELLO'; + link = '/researcherprofiles'; + icon = undefined; + } + + beforeEach(waitForAsync(() => { + init(); + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [LinkIconMenuItemComponent, RouterLinkDirectiveStub], + providers: [ + { provide: 'itemModelProvider', useValue: { text: text, link: link, icon: icon } }, + { provide: Router, useValue: RouterStub }, + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LinkIconMenuItemComponent); + component = fixture.componentInstance; + debugElement = fixture.debugElement; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should contain the correct text', () => { + const textContent = debugElement.query(By.css('a')).nativeElement.textContent; + expect(textContent).toEqual(text); + }); + + it('should have the right routerLink attribute', () => { + const linkDes = fixture.debugElement.queryAll(By.directive(RouterLinkDirectiveStub)); + const routerLinkQuery = linkDes.map((de) => de.injector.get(RouterLinkDirectiveStub)); + expect(routerLinkQuery.length).toBe(1); + expect(routerLinkQuery[0].routerLink).toContain(link); + }); + + it('should set no icon', () => { + expect(fixture.debugElement.query(By.css('.exploreicon'))).toBeNull(); + }); + }); +}); diff --git a/src/app/shared/menu/menu-item/linkicon-menu-item.component.ts b/src/app/shared/menu/menu-item/linkicon-menu-item.component.ts new file mode 100644 index 00000000000..70abecbf8f3 --- /dev/null +++ b/src/app/shared/menu/menu-item/linkicon-menu-item.component.ts @@ -0,0 +1,56 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { LinkIconMenuItemModel } from './models/linkicon.model'; +import { MenuItemType } from '../menu-item-type.model'; +import { rendersMenuItemForType } from '../menu-item.decorator'; +import { isNotEmpty } from '../../empty.util'; +import { environment } from '../../../../environments/environment'; +import { Router } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; + +/** + * Component that renders a menu section of type LINK + */ +@Component({ + selector: 'ds-linkicon-menu-item', + styleUrls: ['./linkicon-menu-item.component.scss'], + templateUrl: './linkicon-menu-item.component.html' +}) +@rendersMenuItemForType(MenuItemType.LINKICON) +export class LinkIconMenuItemComponent implements OnInit { + item: LinkIconMenuItemModel; + hasLink: boolean; + isExternal: boolean; + icon: string; + constructor( + @Inject('itemModelProvider') item: LinkIconMenuItemModel, + private router: Router, + protected translateService: TranslateService, + ) { + this.item = item; + } + + ngOnInit(): void { + this.hasLink = isNotEmpty(this.item.link); + // simple check + this.isExternal = this.hasLink && this.item.link.startsWith('http'); + } + + getRouterLink() { + if (this.hasLink) { + return environment.ui.nameSpace + this.item.link; + } + return undefined; + } + + navigate(event: any) { + event.preventDefault(); + if (this.getRouterLink()) { + this.router.navigate([this.getRouterLink()]); + } + event.stopPropagation(); + } + + href(): string { + return this.item.link; + } +} diff --git a/src/app/shared/menu/menu-item/models/linkicon.model.ts b/src/app/shared/menu/menu-item/models/linkicon.model.ts new file mode 100644 index 00000000000..3814b576bae --- /dev/null +++ b/src/app/shared/menu/menu-item/models/linkicon.model.ts @@ -0,0 +1,15 @@ +import { MenuItemModel } from './menu-item.model'; +import { MenuItemType } from './../../menu-item-type.model'; + +/** + * Model representing an Link and Icon Menu Section + */ +export class LinkIconMenuItemModel implements MenuItemModel { + type = MenuItemType.LINKICON; + text: string; + icon?: string; + style?: string; + link: string; + disabled: boolean; + +} diff --git a/src/app/shared/menu/menu.module.ts b/src/app/shared/menu/menu.module.ts index 28bdab99879..7bbc06f06c2 100644 --- a/src/app/shared/menu/menu.module.ts +++ b/src/app/shared/menu/menu.module.ts @@ -4,6 +4,7 @@ import { NgModule } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; import { RouterModule } from '@angular/router'; import { LinkMenuItemComponent } from './menu-item/link-menu-item.component'; +import { LinkIconMenuItemComponent } from './menu-item/linkicon-menu-item.component'; import { TextMenuItemComponent } from './menu-item/text-menu-item.component'; import { OnClickMenuItemComponent } from './menu-item/onclick-menu-item.component'; import { CommonModule } from '@angular/common'; @@ -17,6 +18,7 @@ const COMPONENTS = [ const ENTRY_COMPONENTS = [ TextMenuItemComponent, LinkMenuItemComponent, + LinkIconMenuItemComponent, OnClickMenuItemComponent, ExternalLinkMenuItemComponent, ]; diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 674a746a513..712eb9228d7 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -585,6 +585,12 @@ export class DefaultAppConfig implements AppConfig { navbar: { // If true, show the "Community and Collections" link in the navbar; otherwise, show it in the admin sidebar showCommunityCollection: true, + // { + // explore_id: 'fundings_and_projects', + // icon: 'fa fa-project-diagram' + // } + showExploreIcons: [ + ] } }; diff --git a/src/config/layout-config.interfaces.ts b/src/config/layout-config.interfaces.ts index b5f293800c1..94b6def79e1 100644 --- a/src/config/layout-config.interfaces.ts +++ b/src/config/layout-config.interfaces.ts @@ -36,6 +36,12 @@ export interface CrisLayoutTypeConfig { export interface NavbarConfig extends Config { showCommunityCollection: boolean; + showExploreIcons?: NavbarItemIconConfig[] +} + +export interface NavbarItemIconConfig extends Config { + explore_id: string; + icon: string; } export interface CrisItemPageConfig extends Config {