Skip to content

Commit 0000da2

Browse files
[DURACOM-501] implement dso public menu
1 parent a2c5e0c commit 0000da2

18 files changed

Lines changed: 800 additions & 1 deletion

src/app/app.menus.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { WorkflowMenuProvider } from './shared/menu/providers/workflow.menu';
5050
* - `MenuID.PUBLIC`: Defines menus accessible by the public in the navigation bar.
5151
* - `MenuID.ADMIN`: Defines menus for administrative users in the sidebar.
5252
* - `MenuID.DSO_EDIT`: Defines dynamic menu options for DSpace Objects that will be present on the DSpace Object's page.
53+
* - `MenuID.DSO_PUBLIC`: Defines dynamic menu options for DSpace Objects available to unauthenticated users.
5354
*
5455
* To add more menu sections to a menu (public navbar, admin sidebar or the dso edit menus),
5556
* a new provider can be added to the list with the corresponding menu ID.
@@ -118,4 +119,6 @@ export const MENUS = buildMenuStructure({
118119
),
119120
]),
120121
],
122+
[MenuID.DSO_PUBLIC]: [
123+
],
121124
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
@if (hasSubSections$ | async) {
2+
<div class="dso-button-menu mb-1" ngbDropdown container="body" placement="bottom-right">
3+
<div class="d-flex flex-row flex-nowrap"
4+
[ngbTooltip]="itemModel.text | translate" container="body">
5+
<button [attr.aria-label]="itemModel.text | translate" [title]="itemModel.text | translate" class="btn btn-dark btn-sm" ngbDropdownToggle [dsBtnDisabled]="section.model?.disabled" role="menuitem" tabindex="0">
6+
<i class="fas fa-{{section.icon}} fa-fw"></i>
7+
</button>
8+
<ul ngbDropdownMenu class="dso-public-menu-dropdown p-1" role="menu">
9+
@for (subSection of (subSections$ | async); track subSection) {
10+
<li class="nav-item nav-link d-flex flex-row p-2" role="presentation">
11+
@if (renderIcons$ | async) {
12+
<div class="me-2">
13+
@if (subSection.icon) {
14+
<i class="fas fa-{{subSection.icon}} fa-fw" aria-hidden="true"></i>
15+
} @else {
16+
<i class="fas fa-fw" aria-hidden="true"></i>
17+
}
18+
</div>
19+
}
20+
<ng-container
21+
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;">
22+
</ng-container>
23+
</li>
24+
}
25+
</ul>
26+
</div>
27+
</div>
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
.btn-dark {
2+
background-color: var(--ds-admin-sidebar-bg);
3+
}
4+
5+
.dso-button-menu {
6+
.dropdown-toggle::after {
7+
content: '';
8+
width: 0;
9+
height: 0;
10+
border-style: solid;
11+
border-width: 12px 12px 0 0;
12+
border-color: transparent #627a91 transparent transparent;
13+
border-bottom-right-radius: var(--bs-btn-border-radius-sm);
14+
right: 0;
15+
bottom: 0;
16+
position: absolute;
17+
overflow: hidden;
18+
}
19+
overflow: hidden;
20+
}
21+
22+
ul.dropdown-menu {
23+
background-color: var(--ds-admin-sidebar-bg);
24+
color: white;
25+
26+
::ng-deep a {
27+
color: white;
28+
29+
&.disabled {
30+
color: var(--bs-btn-link-disabled-color);
31+
}
32+
}
33+
34+
.disabled {
35+
color: var(--bs-btn-link-disabled-color);
36+
}
37+
}
38+
39+
.dso-public-menu-dropdown {
40+
max-width: calc(min(600px, 75vw));
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { Component } from '@angular/core';
2+
import {
3+
ComponentFixture,
4+
TestBed,
5+
waitForAsync,
6+
} from '@angular/core/testing';
7+
import { By } from '@angular/platform-browser';
8+
import { Router } from '@angular/router';
9+
import { CSSVariableServiceStub } from '@dspace/core/testing/css-variable-service.stub';
10+
import { RouterStub } from '@dspace/core/testing/router.stub';
11+
import { TranslateModule } from '@ngx-translate/core';
12+
import { of } from 'rxjs';
13+
14+
import { MenuService } from '../../../menu/menu.service';
15+
import { MenuItemType } from '../../../menu/menu-item-type.model';
16+
import { MenuItemModels } from '../../../menu/menu-section.model';
17+
import { MenuServiceStub } from '../../../menu/menu-service.stub';
18+
import { CSSVariableService } from '../../../sass-helper/css-variable.service';
19+
import { DsoPublicMenuExpandableSectionComponent } from './dso-public-menu-expandable-section.component';
20+
21+
describe('DsoPublicMenuExpandableSectionComponent', () => {
22+
let component: DsoPublicMenuExpandableSectionComponent;
23+
let fixture: ComponentFixture<DsoPublicMenuExpandableSectionComponent>;
24+
const menuService = new MenuServiceStub();
25+
const iconString = 'test';
26+
27+
const dummySection = {
28+
id: 'dummy',
29+
active: false,
30+
visible: true,
31+
model: {
32+
type: MenuItemType.TEXT,
33+
disabled: false,
34+
text: 'text',
35+
},
36+
icon: iconString,
37+
};
38+
39+
describe('when there are subsections', () => {
40+
beforeEach(waitForAsync(() => {
41+
TestBed.configureTestingModule({
42+
imports: [TranslateModule.forRoot(), DsoPublicMenuExpandableSectionComponent, TestComponent],
43+
providers: [
44+
{ provide: 'sectionDataProvider', useValue: dummySection },
45+
{ provide: MenuService, useValue: menuService },
46+
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
47+
{ provide: Router, useValue: new RouterStub() },
48+
],
49+
}).compileComponents();
50+
}));
51+
52+
beforeEach(() => {
53+
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(of([{
54+
id: 'test',
55+
visible: true,
56+
model: {} as MenuItemModels,
57+
}]));
58+
fixture = TestBed.createComponent(DsoPublicMenuExpandableSectionComponent);
59+
component = fixture.componentInstance;
60+
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
61+
fixture.detectChanges();
62+
});
63+
64+
it('should create', () => {
65+
expect(component).toBeTruthy();
66+
});
67+
68+
it('should show a button with the icon', () => {
69+
const button = fixture.debugElement.query(By.css('.btn-dark'));
70+
expect(button.nativeElement.innerHTML).toContain('fa-' + iconString);
71+
});
72+
});
73+
74+
describe('when there are no subsections', () => {
75+
beforeEach(waitForAsync(() => {
76+
TestBed.configureTestingModule({
77+
imports: [TranslateModule.forRoot(), DsoPublicMenuExpandableSectionComponent, TestComponent],
78+
providers: [
79+
{ provide: 'sectionDataProvider', useValue: dummySection },
80+
{ provide: MenuService, useValue: menuService },
81+
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
82+
{ provide: Router, useValue: new RouterStub() },
83+
],
84+
}).compileComponents();
85+
}));
86+
87+
beforeEach(() => {
88+
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(of([]));
89+
fixture = TestBed.createComponent(DsoPublicMenuExpandableSectionComponent);
90+
component = fixture.componentInstance;
91+
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
92+
fixture.detectChanges();
93+
});
94+
95+
it('should create', () => {
96+
expect(component).toBeTruthy();
97+
});
98+
99+
it('should now show a button', () => {
100+
const button = fixture.debugElement.query(By.css('.btn-dark'));
101+
expect(button).toBeNull();
102+
});
103+
});
104+
});
105+
106+
@Component({
107+
selector: 'ds-test-cmp',
108+
template: ``,
109+
})
110+
class TestComponent {
111+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* The contents of this file are subject to the license and copyright
3+
* detailed in the LICENSE and NOTICE files at the root of the source
4+
* tree and available online at
5+
*
6+
* http://www.dspace.org/license/
7+
*/
8+
import {
9+
AsyncPipe,
10+
NgComponentOutlet,
11+
} from '@angular/common';
12+
import {
13+
Component,
14+
Inject,
15+
Injector,
16+
OnInit,
17+
} from '@angular/core';
18+
import { Router } from '@angular/router';
19+
import {
20+
hasValue,
21+
isNotEmpty,
22+
} from '@dspace/shared/utils/empty.util';
23+
import {
24+
NgbDropdownModule,
25+
NgbTooltip,
26+
} from '@ng-bootstrap/ng-bootstrap';
27+
import { TranslateModule } from '@ngx-translate/core';
28+
import { Observable } from 'rxjs';
29+
import { map } from 'rxjs/operators';
30+
import { MenuID } from 'src/app/shared/menu/menu-id.model';
31+
import { MenuSection } from 'src/app/shared/menu/menu-section.model';
32+
import { AbstractMenuSectionComponent } from 'src/app/shared/menu/menu-section/abstract-menu-section.component';
33+
34+
import { BtnDisabledDirective } from '../../../btn-disabled.directive';
35+
import { MenuService } from '../../../menu/menu.service';
36+
37+
/**
38+
* Represents an expandable section in the dso public menus
39+
*/
40+
@Component({
41+
selector: 'ds-dso-public-menu-expandable-section',
42+
templateUrl: './dso-public-menu-expandable-section.component.html',
43+
styleUrls: ['./dso-public-menu-expandable-section.component.scss'],
44+
imports: [
45+
AsyncPipe,
46+
BtnDisabledDirective,
47+
NgbDropdownModule,
48+
NgbTooltip,
49+
NgComponentOutlet,
50+
TranslateModule,
51+
],
52+
})
53+
export class DsoPublicMenuExpandableSectionComponent extends AbstractMenuSectionComponent implements OnInit {
54+
55+
/**
56+
* This section resides in the DSO public menu
57+
*/
58+
menuID: MenuID = MenuID.DSO_PUBLIC;
59+
60+
/**
61+
* The MenuItemModel of the top section
62+
*/
63+
itemModel;
64+
65+
/**
66+
* Emits whether one of the subsections contains an icon
67+
*/
68+
renderIcons$: Observable<boolean>;
69+
70+
/**
71+
* Emits true when the top section has subsections, else emits false
72+
*/
73+
hasSubSections$: Observable<boolean>;
74+
75+
constructor(
76+
@Inject('sectionDataProvider') protected section: MenuSection,
77+
protected menuService: MenuService,
78+
protected injector: Injector,
79+
protected router: Router,
80+
) {
81+
super(menuService, injector);
82+
this.itemModel = section.model;
83+
}
84+
85+
ngOnInit(): void {
86+
this.menuService.activateSection(this.menuID, this.section.id);
87+
super.ngOnInit();
88+
89+
this.renderIcons$ = this.subSections$.pipe(
90+
map((sections: MenuSection[]) => {
91+
return sections.some(section => hasValue(section.icon));
92+
}),
93+
);
94+
95+
this.hasSubSections$ = this.subSections$.pipe(
96+
map((subSections) => isNotEmpty(subSections)),
97+
);
98+
}
99+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
@if (!canActivate) {
2+
<div class="dso-button-menu mb-1"
3+
[ngbTooltip]="itemModel.text | translate">
4+
@if (!section.model.disabled) {
5+
<a class="btn btn-dark btn-sm"
6+
[routerLink]="itemModel.link">
7+
<i class="fas fa-{{section.icon}} fa-fw" aria-hidden="true"></i>
8+
<span class="sr-only">{{itemModel.text | translate}}</span>
9+
</a>
10+
}
11+
@if (section.model.disabled) {
12+
<button class="btn btn-dark btn-sm" [dsBtnDisabled]="true">
13+
<i class="fas fa-{{section.icon}} fa-fw" aria-hidden="true"></i>
14+
<span class="sr-only">{{itemModel.text | translate}}</span>
15+
</button>
16+
}
17+
</div>
18+
}
19+
20+
@if (canActivate) {
21+
<div class="dso-button-menu mb-1"
22+
[ngbTooltip]="itemModel.text | translate">
23+
<button class="btn btn-dark btn-sm" [dsBtnDisabled]="section.model.disabled"
24+
(click)="activate($event)">
25+
<i class="fas fa-{{section.icon}} fa-fw" aria-hidden="true"></i>
26+
<span class="sr-only">{{itemModel.text | translate}}</span>
27+
</button>
28+
</div>
29+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.btn-dark {
2+
background-color: var(--ds-admin-sidebar-bg);
3+
}

0 commit comments

Comments
 (0)