mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'master' into w2p-55946_Item-mapping-on-item-level
Conflicts: src/app/+community-page/community-page.component.html src/app/app.reducer.ts src/app/core/auth/server-auth.service.ts src/app/core/core.module.ts src/app/core/index/index.effects.ts src/app/core/index/index.reducer.spec.ts src/app/core/index/index.reducer.ts src/app/header/header.component.spec.ts src/app/shared/shared.module.ts
This commit is contained in:
@@ -131,6 +131,7 @@
|
||||
"devDependencies": {
|
||||
"@angular/compiler": "^6.1.4",
|
||||
"@angular/compiler-cli": "^6.1.4",
|
||||
"@fortawesome/fontawesome-free": "^5.5.0",
|
||||
"@ngrx/entity": "^6.1.0",
|
||||
"@ngrx/schematics": "^6.1.0",
|
||||
"@ngrx/store-devtools": "^6.1.0",
|
||||
@@ -183,7 +184,7 @@
|
||||
"karma-webdriver-launcher": "1.0.5",
|
||||
"karma-webpack": "3.0.0",
|
||||
"ngrx-store-freeze": "^0.2.4",
|
||||
"node-sass": "^4.7.2",
|
||||
"node-sass": "^4.11.0",
|
||||
"nodemon": "^1.15.0",
|
||||
"npm-run-all": "4.1.3",
|
||||
"postcss": "^7.0.2",
|
||||
|
@@ -62,6 +62,9 @@
|
||||
},
|
||||
"sub-collection-list": {
|
||||
"head": "Collections of this Community"
|
||||
},
|
||||
"sub-community-list": {
|
||||
"head": "Communities of this Community"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
@@ -183,9 +186,19 @@
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"browse": {
|
||||
"header": "All of DSpace"
|
||||
},
|
||||
"community-browse": {
|
||||
"header": "By Community"
|
||||
},
|
||||
"statistics": {
|
||||
"header": "Statistics"
|
||||
},
|
||||
"login": "Log In",
|
||||
"logout": "Log Out"
|
||||
"logout": "Log Out",
|
||||
"language": "Language switch",
|
||||
"search": "Search"
|
||||
},
|
||||
"pagination": {
|
||||
"results-per-page": "Results Per Page",
|
||||
@@ -341,12 +354,92 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"header": {
|
||||
"admin": "Admin",
|
||||
"image": {
|
||||
"logo": "Repository logo"
|
||||
}
|
||||
},
|
||||
"section": {
|
||||
"pin": "Pin sidebar",
|
||||
"unpin": "Unpin sidebar",
|
||||
"new": "New",
|
||||
"new_community": "Community",
|
||||
"new_collection": "Collection",
|
||||
"new_item": "Item",
|
||||
"new_item_version": "Item Version",
|
||||
"edit": "Edit",
|
||||
"edit_community": "Community",
|
||||
"edit_collection": "Collection",
|
||||
"edit_item": "Item",
|
||||
"import": "Import",
|
||||
"import_metadata": "Metadata",
|
||||
"import_batch": "Batch Import (ZIP)",
|
||||
"export": "Export",
|
||||
"export_community": "Community",
|
||||
"export_collection": "Collection",
|
||||
"export_item": "Item",
|
||||
"export_metadata": "Metadata",
|
||||
"access_control": "Access Control",
|
||||
"access_control_people": "People",
|
||||
"access_control_groups": "Groups",
|
||||
"access_control_authorizations": "Authorizations",
|
||||
"find": "Find",
|
||||
"find_items": "Items",
|
||||
"find_withdrawn_items": "Withdrawn Items",
|
||||
"find_private_items": "Private Items",
|
||||
"registries": "Registries",
|
||||
"registries_metadata": "Metadata",
|
||||
"registries_format": "Format",
|
||||
"curation_task": "Curation Task",
|
||||
"statistics_task": "Statistics Task",
|
||||
"control_panel": "Control Panel",
|
||||
"browse_global": "All of DSpace",
|
||||
"browse_global_communities_and_collections": "Communities & Collections",
|
||||
"browse_global_by_issue_date": "By Issue Date",
|
||||
"browse_global_by_author": "By Author",
|
||||
"browse_global_by_title": "By Title",
|
||||
"statistics": "Statistics",
|
||||
"browse_community": "This Community",
|
||||
"browse_community_by_issue_date": "By Issue Date",
|
||||
"browse_community_by_author": "By Author",
|
||||
"browse_community_by_title": "By Title",
|
||||
"icon": {
|
||||
"pin": "Pin sidebar",
|
||||
"unpin": "Unpin sidebar",
|
||||
"new": "New menu section",
|
||||
"edit": "Edit menu section",
|
||||
"import": "Import menu section",
|
||||
"export": "Export menu section",
|
||||
"access_control": "Access Control menu section",
|
||||
"find": "Find menu section",
|
||||
"registries": "Registries menu section",
|
||||
"curation_task": "Curation Task menu section",
|
||||
"statistics_task": "Statistics Task menu section",
|
||||
"control_panel": "Control Panel menu section"
|
||||
},
|
||||
"toggle": {
|
||||
"new": "Toggle New section",
|
||||
"edit": "Toggle Edit section",
|
||||
"import": "Toggle Import section",
|
||||
"export": "Toggle Export section",
|
||||
"access_control": "Toggle Access Control section",
|
||||
"find": "Toggle Find section",
|
||||
"registries": "Toggle Registries section",
|
||||
"curation_task": "Toggle Curation Task section",
|
||||
"statistics_task": "Toggle Statistics Task section",
|
||||
"control_panel": "Toggle Control Panel section"
|
||||
}
|
||||
}
|
||||
},
|
||||
"loading": {
|
||||
"default": "Loading...",
|
||||
"top-level-communities": "Loading top-level communities...",
|
||||
"community": "Loading community...",
|
||||
"collection": "Loading collection...",
|
||||
"sub-collections": "Loading sub-collections...",
|
||||
"sub-communities": "Loading sub-communities...",
|
||||
"recent-submissions": "Loading recent submissions...",
|
||||
"item": "Loading item...",
|
||||
"objects": "Loading...",
|
||||
@@ -359,6 +452,7 @@
|
||||
"community": "Error fetching community",
|
||||
"collection": "Error fetching collection",
|
||||
"sub-collections": "Error fetching sub-collections",
|
||||
"sub-communities": "Error fetching sub-communities",
|
||||
"recent-submissions": "Error fetching recent submissions",
|
||||
"item": "Error fetching item",
|
||||
"objects": "Error fetching objects",
|
||||
@@ -411,5 +505,8 @@
|
||||
"errors": {
|
||||
"invalid-user": "Invalid email address or password."
|
||||
}
|
||||
},
|
||||
"chips": {
|
||||
"remove": "Remove chip"
|
||||
}
|
||||
}
|
||||
|
23
resources/images/dspace-logo-mini.svg
Normal file
23
resources/images/dspace-logo-mini.svg
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="97.562px" height="91.866px" viewBox="67.165 0 97.562 91.866" enable-background="new 67.165 0 97.562 91.866"
|
||||
xml:space="preserve">
|
||||
<path fill="#92C642" d="M120.726,58.569l0.109-0.006l0.116-0.01l0.106-0.013l0.11-0.01l0.11-0.023l0.109-0.019l0.106-0.023
|
||||
l0.106-0.029l0.105-0.023l0.106-0.033l0.103-0.034l0.097-0.035l0.104-0.04l0.101-0.042l0.1-0.042v-0.001l0.096-0.045l0,0
|
||||
l0.095-0.044l0.097-0.049l0.091-0.056v-0.001l0.094-0.05v-0.002l0.09-0.056v-0.001l0.093-0.06l0.083-0.056v-0.001l0.085-0.063
|
||||
l0.088-0.065v-0.002l0.087-0.062v-0.001c0.816-0.683,1.393-1.646,1.561-2.738l0.013-0.104V54.72l0.014-0.101v-0.011l0.009-0.098
|
||||
v-0.012l0.009-0.101V54.38l0.005-0.095v-0.016l0.002-0.105v-16.46l-0.002-0.105v-0.016l-0.005-0.095v-0.013l-0.009-0.101v-0.012
|
||||
l-0.009-0.098v-0.011l-0.014-0.1v-0.01l-0.013-0.104c-0.167-1.092-0.744-2.057-1.561-2.738V34.3l-0.087-0.063v-0.002l-0.088-0.065
|
||||
l-0.085-0.063v-0.001l-0.083-0.056l-0.093-0.061l0,0l-0.09-0.056V33.93l-0.094-0.05v-0.001l-0.091-0.056l-0.097-0.049l-0.095-0.043
|
||||
V33.73l-0.096-0.045v-0.001l-0.1-0.043l-0.101-0.042l-0.104-0.04l-0.097-0.035l-0.103-0.031l-0.106-0.036l-0.105-0.023l-0.106-0.028
|
||||
l-0.106-0.024l-0.109-0.019l-0.11-0.023l-0.11-0.009l-0.106-0.014l-0.116-0.01l-0.109-0.006l-0.114-0.005h-7.89
|
||||
c-9.716,0-15.858-7.838-15.858-17.15V6.92c0-3.812-3.102-6.915-6.914-6.915H74.085c-3.813,0-6.92,3.106-6.92,6.915v16.682
|
||||
c0,3.806,3.104,6.909,6.92,6.909h8.414c9.169,0,16.906,5.95,17.146,15.403v0.04c-0.24,9.453-7.978,15.402-17.146,15.402h-8.414
|
||||
c-3.815,0-6.92,3.103-6.92,6.909v16.682c0,3.81,3.106,6.915,6.92,6.915H89.95c3.812,0,6.914-3.104,6.914-6.915v-9.223
|
||||
c0-9.312,6.144-17.149,15.858-17.149h7.89L120.726,58.569z M154.772,9.956C148.631,3.814,140.15,0,130.816,0h-15.024v17.424h15.024
|
||||
c4.526,0,8.647,1.858,11.64,4.849c2.99,2.99,4.849,7.112,4.849,11.639v24.042c0,4.538-1.853,8.665-4.832,11.655l-0.017-0.016
|
||||
c-2.991,2.991-7.113,4.849-11.64,4.849h-15.024v17.424h15.024c9.333,0,17.814-3.814,23.956-9.956v-0.033
|
||||
c6.142-6.143,9.955-14.614,9.955-23.923V33.912C164.727,24.578,160.914,16.097,154.772,9.956z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
37
resources/images/dspace-logo.svg
Normal file
37
resources/images/dspace-logo.svg
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="231.892px" height="167.458px" viewBox="0 0 231.892 167.458" enable-background="new 0 0 231.892 167.458"
|
||||
xml:space="preserve">
|
||||
<path fill="#6D6E71" d="M51.733,143.32c0-1.941,1.651-3.203,4.563-3.203c3.302,0,6.798,1.116,9.419,3.543l3.835-5.146
|
||||
c-3.203-2.962-7.476-4.516-12.622-4.516c-7.622,0-12.284,4.468-12.284,9.856c0,12.187,18.644,8.254,18.644,13.886
|
||||
c0,1.894-1.797,3.593-5.632,3.593c-4.466,0-8.011-2.039-10.292-4.419l-3.787,5.39c3.058,3.059,7.525,5.154,13.788,5.154
|
||||
c8.691,0,12.964-4.474,12.964-10.397C70.329,144.971,51.733,148.418,51.733,143.32z M100.682,134.484H85.534v32.385h6.894v-11.556
|
||||
h8.254c6.991,0,10.875-4.759,10.875-10.391C111.558,139.242,107.722,134.484,100.682,134.484z M99.71,149.244h-7.283v-8.69h7.283
|
||||
c2.719,0,4.807,1.651,4.807,4.369C104.518,147.593,102.43,149.244,99.71,149.244z M180.759,140.067c3.301,0,6.214,2.089,7.573,4.71
|
||||
l5.923-2.913c-2.281-4.078-6.408-7.914-13.496-7.914c-9.759,0-17.284,6.75-17.284,16.75c0,9.954,7.525,16.759,17.284,16.759
|
||||
c7.088,0,11.215-3.94,13.496-7.97l-5.923-2.865c-1.359,2.622-4.272,4.71-7.573,4.71c-5.924,0-10.195-4.516-10.195-10.634
|
||||
C170.564,144.583,174.835,140.067,180.759,140.067z M131.958,134.484l-12.486,32.385h7.824l2.04-5.486h13.886l2.039,5.486h7.816
|
||||
l-12.479-32.385H131.958z M131.228,155.313l5.05-13.935l5.05,13.935H131.228z M231.892,140.553v-6.069h-22.916v32.385h22.916v-6.069
|
||||
H215.87v-7.38h15.683v-6.069H215.87v-6.797H231.892z"/>
|
||||
<path fill="#92C642" d="M29.956,150.652c0-9.71-7.04-16.168-17.187-16.168H0v32.385h12.817
|
||||
C22.916,166.869,29.956,160.458,29.956,150.652z M12.769,160.799H6.894v-20.246h5.924c6.603,0,10.098,4.418,10.098,10.099
|
||||
C22.916,156.187,19.177,160.799,12.769,160.799z"/>
|
||||
<path fill="#92C642" d="M120.726,58.569l0.11-0.006l0.116-0.01l0.106-0.013l0.111-0.01l0.11-0.023l0.109-0.019l0.107-0.023
|
||||
l0.106-0.029l0.106-0.023l0.106-0.033l0.103-0.034l0.096-0.035l0.104-0.04l0.101-0.042l0.099-0.042v-0.001l0.096-0.045v0
|
||||
l0.095-0.044l0.096-0.049l0.091-0.056v-0.001l0.094-0.05v-0.002l0.09-0.056v-0.001l0.092-0.06l0.083-0.056v-0.001l0.085-0.063
|
||||
l0.088-0.065v-0.002l0.087-0.063v-0.001c0.817-0.683,1.393-1.646,1.561-2.738l0.012-0.104v-0.009l0.014-0.101v-0.011l0.009-0.098
|
||||
v-0.012l0.009-0.101V54.38l0.005-0.095v-0.016l0.002-0.105v-16.46l-0.002-0.105v-0.016l-0.005-0.095v-0.013l-0.009-0.101v-0.012
|
||||
l-0.009-0.098v-0.011l-0.014-0.1v-0.01l-0.012-0.104c-0.167-1.092-0.744-2.057-1.561-2.738v-0.001l-0.087-0.063v-0.002l-0.088-0.065
|
||||
l-0.085-0.063v-0.001l-0.083-0.056l-0.092-0.061v0l-0.09-0.056v-0.003l-0.094-0.05v-0.001l-0.091-0.056l-0.096-0.049l-0.095-0.043
|
||||
v-0.001l-0.096-0.045v-0.001l-0.099-0.043l-0.101-0.042l-0.104-0.04l-0.096-0.035l-0.103-0.031l-0.106-0.036l-0.106-0.023
|
||||
l-0.106-0.028l-0.107-0.024l-0.109-0.019l-0.11-0.023l-0.111-0.009l-0.106-0.014l-0.116-0.01l-0.11-0.006l-0.114-0.005h-7.89
|
||||
c-9.715,0-15.858-7.838-15.858-17.15V6.92c0-3.812-3.102-6.915-6.914-6.915H74.085c-3.814,0-6.92,3.106-6.92,6.915v16.682
|
||||
c0,3.806,3.104,6.909,6.92,6.909h8.414c9.169,0,16.906,5.95,17.146,15.403v0.04c-0.24,9.453-7.977,15.402-17.146,15.402h-8.414
|
||||
c-3.816,0-6.92,3.103-6.92,6.909v16.682c0,3.809,3.106,6.915,6.92,6.915H89.95c3.812,0,6.914-3.104,6.914-6.915v-9.223
|
||||
c0-9.312,6.144-17.149,15.858-17.149h7.89L120.726,58.569z M154.772,9.956C148.631,3.814,140.15,0,130.816,0h-15.024v17.424h15.024
|
||||
c4.527,0,8.648,1.858,11.64,4.849c2.99,2.99,4.848,7.112,4.848,11.639v24.042c0,4.538-1.852,8.665-4.832,11.655l-0.016-0.016
|
||||
c-2.991,2.991-7.113,4.849-11.64,4.849h-15.024v17.424h15.024c9.333,0,17.815-3.814,23.956-9.956v-0.033
|
||||
c6.142-6.143,9.955-14.614,9.955-23.923V33.912C164.727,24.578,160.914,16.097,154.772,9.956z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.9 KiB |
@@ -7,7 +7,6 @@ import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { MetadataField } from '../../../core/metadata/metadatafield.model';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadataschema.model';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-schema',
|
||||
|
@@ -0,0 +1,11 @@
|
||||
<li class="sidebar-section">
|
||||
<a class="nav-item nav-link shortcut-icon" [routerLink]="itemModel.link">
|
||||
<i class="fas fa-{{section.icon}} fa-fw" [title]="('menu.section.icon.' + section.id) | translate"></i>
|
||||
</a>
|
||||
<div class="sidebar-collapsible">
|
||||
<span class="section-header-text">
|
||||
<ng-container
|
||||
*ngComponentOutlet="itemComponents.get(section.id); injector: itemInjectors.get(section.id);"></ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
@@ -0,0 +1 @@
|
||||
@import '../../../../styles/variables.scss';
|
@@ -0,0 +1,60 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MenuService } from '../../../shared/menu/menu.service';
|
||||
import { MenuServiceStub } from '../../../shared/testing/menu-service-stub';
|
||||
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
|
||||
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service-stub';
|
||||
import { Component } from '@angular/core';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { AdminSidebarSectionComponent } from './admin-sidebar-section.component';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
describe('AdminSidebarSectionComponent', () => {
|
||||
let component: AdminSidebarSectionComponent;
|
||||
let fixture: ComponentFixture<AdminSidebarSectionComponent>;
|
||||
const menuService = new MenuServiceStub();
|
||||
const iconString = 'test';
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule, RouterTestingModule, TranslateModule.forRoot()],
|
||||
declarations: [AdminSidebarSectionComponent, TestComponent],
|
||||
providers: [
|
||||
{ provide: 'sectionDataProvider', useValue: { model: { link: 'google.com' }, icon: iconString } },
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||
]
|
||||
}).overrideComponent(AdminSidebarSectionComponent, {
|
||||
set: {
|
||||
entryComponents: [TestComponent]
|
||||
}
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AdminSidebarSectionComponent);
|
||||
component = fixture.componentInstance;
|
||||
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set the right icon', () => {
|
||||
const icon = fixture.debugElement.query(By.css('.shortcut-icon')).query(By.css('i.fas'));
|
||||
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
|
||||
});
|
||||
});
|
||||
|
||||
// declare a test component
|
||||
@Component({
|
||||
selector: 'ds-test-cmp',
|
||||
template: ``
|
||||
})
|
||||
class TestComponent {
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
import { Component, Inject, Injector, OnInit } from '@angular/core';
|
||||
import { MenuSectionComponent } from '../../../shared/menu/menu-section/menu-section.component';
|
||||
import { MenuID } from '../../../shared/menu/initial-menus-state';
|
||||
import { MenuService } from '../../../shared/menu/menu.service';
|
||||
import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator';
|
||||
import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model';
|
||||
import { MenuSection } from '../../../shared/menu/menu.reducer';
|
||||
|
||||
/**
|
||||
* Represents a non-expandable section in the admin sidebar
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-admin-sidebar-section',
|
||||
templateUrl: './admin-sidebar-section.component.html',
|
||||
styleUrls: ['./admin-sidebar-section.component.scss'],
|
||||
|
||||
})
|
||||
@rendersSectionForMenu(MenuID.ADMIN, false)
|
||||
export class AdminSidebarSectionComponent extends MenuSectionComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* This section resides in the Admin Sidebar
|
||||
*/
|
||||
menuID: MenuID = MenuID.ADMIN;
|
||||
itemModel;
|
||||
constructor(@Inject('sectionDataProvider') menuSection: MenuSection, protected menuService: MenuService, protected injector: Injector,) {
|
||||
super(menuSection, menuService, injector);
|
||||
this.itemModel = menuSection.model as LinkMenuItemModel;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit();
|
||||
}
|
||||
}
|
52
src/app/+admin/admin-sidebar/admin-sidebar.component.html
Normal file
52
src/app/+admin/admin-sidebar/admin-sidebar.component.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<nav @slideHorizontal class="navbar navbar-dark bg-dark p-0"
|
||||
[ngClass]="{'active': sidebarOpen, 'inactive': sidebarClosed}"
|
||||
[@slideSidebar]="{
|
||||
value: (!(sidebarExpanded | async) ? 'collapsed' : 'expanded'),
|
||||
params: {sidebarWidth: (sidebarWidth | async)}
|
||||
}" (@slideSidebar.done)="finishSlide($event)" (@slideSidebar.start)="startSlide($event)"
|
||||
*ngIf="menuVisible | async" (mouseenter)="expandPreview($event)"
|
||||
(mouseleave)="collapsePreview($event)">
|
||||
<div class="sidebar-top-level-items">
|
||||
<ul class="navbar-nav">
|
||||
<li class="admin-menu-header sidebar-section">
|
||||
<a class="shortcut-icon navbar-brand mr-0" href="#">
|
||||
<span class="logo-wrapper">
|
||||
<img class="admin-logo" src="assets/images/dspace-logo-mini.svg"
|
||||
[alt]="('menu.header.image.logo') | translate">
|
||||
</span>
|
||||
</a>
|
||||
<div class="sidebar-collapsible">
|
||||
<a class="navbar-brand mr-0" href="#">
|
||||
<h4 class="section-header-text mb-0">{{'menu.header.admin' |
|
||||
translate}}</h4>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<ng-container *ngFor="let section of (sections | async)">
|
||||
<ng-container
|
||||
*ngComponentOutlet="sectionComponents.get(section.id); injector: sectionInjectors.get(section.id);"></ng-container>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar-nav">
|
||||
<div class="sidebar-section" id="sidebar-collapse-toggle">
|
||||
<a class="nav-item nav-link shortcut-icon"
|
||||
href="#"
|
||||
(click)="toggle($event)">
|
||||
<i *ngIf="(menuCollapsed | async)" class="fas fa-fw fa-angle-double-right"
|
||||
[title]="'menu.section.icon.pin' | translate"></i>
|
||||
<i *ngIf="!(menuCollapsed | async)" class="fas fa-fw fa-angle-double-left"
|
||||
[title]="'menu.section.icon.unpin' | translate"></i>
|
||||
</a>
|
||||
<div class="sidebar-collapsible">
|
||||
<a class="nav-item nav-link sidebar-section"
|
||||
href="#"
|
||||
(click)="toggle($event)">
|
||||
<span *ngIf="menuCollapsed | async" class="section-header-text">{{'menu.section.pin' | translate }}</span>
|
||||
<span *ngIf="!(menuCollapsed | async)" class="section-header-text">{{'menu.section.unpin' | translate }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
77
src/app/+admin/admin-sidebar/admin-sidebar.component.scss
Normal file
77
src/app/+admin/admin-sidebar/admin-sidebar.component.scss
Normal file
@@ -0,0 +1,77 @@
|
||||
@import '../../../styles/variables.scss';
|
||||
@import '../../../styles/mixins.scss';
|
||||
$icon-z-index: 10;
|
||||
|
||||
:host {
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
flex: 1 1 auto;
|
||||
nav {
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
> div {
|
||||
width: 100%;
|
||||
&.sidebar-top-level-items {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
@include dark-scrollbar;
|
||||
}
|
||||
}
|
||||
|
||||
&.inactive ::ng-deep .sidebar-collapsible {
|
||||
margin-left: -#{$sidebar-items-width};
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
.admin-menu-header {
|
||||
background-color: $admin-sidebar-header-bg;
|
||||
.logo-wrapper {
|
||||
img {
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
.section-header-text {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
::ng-deep {
|
||||
.navbar-nav {
|
||||
.sidebar-section {
|
||||
display: flex;
|
||||
align-content: stretch;
|
||||
background-color: $dark;
|
||||
.nav-item {
|
||||
padding-top: $spacer;
|
||||
padding-bottom: $spacer;
|
||||
}
|
||||
.shortcut-icon {
|
||||
padding-left: $icon-padding;
|
||||
padding-right: $icon-padding;
|
||||
}
|
||||
.shortcut-icon, .icon-wrapper {
|
||||
background-color: inherit;
|
||||
z-index: $icon-z-index;
|
||||
}
|
||||
.sidebar-collapsible {
|
||||
width: $sidebar-items-width;
|
||||
position: relative;
|
||||
a {
|
||||
padding-right: $spacer;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
&.active > .sidebar-collapsible > .nav-link {
|
||||
color: $navbar-dark-active-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
142
src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts
Normal file
142
src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { AdminSidebarComponent } from './admin-sidebar.component';
|
||||
import { MenuService } from '../../shared/menu/menu.service';
|
||||
import { MenuServiceStub } from '../../shared/testing/menu-service-stub';
|
||||
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
||||
import { CSSVariableServiceStub } from '../../shared/testing/css-variable-service-stub';
|
||||
import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
describe('AdminSidebarComponent', () => {
|
||||
let comp: AdminSidebarComponent;
|
||||
let fixture: ComponentFixture<AdminSidebarComponent>;
|
||||
const menuService = new MenuServiceStub();
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), NoopAnimationsModule],
|
||||
declarations: [AdminSidebarComponent],
|
||||
providers: [
|
||||
{ provide: Injector, useValue: {} },
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||
{ provide: AuthService, useClass: AuthServiceStub }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(AdminSidebarComponent, {
|
||||
set: {
|
||||
changeDetection: ChangeDetectionStrategy.Default,
|
||||
}
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'getMenuTopSections').and.returnValue(observableOf([]));
|
||||
fixture = TestBed.createComponent(AdminSidebarComponent);
|
||||
comp = fixture.componentInstance; // SearchPageComponent test instance
|
||||
comp.sections = observableOf([]);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('startSlide', () => {
|
||||
describe('when expanding', () => {
|
||||
beforeEach(() => {
|
||||
comp.sidebarClosed = true;
|
||||
comp.startSlide({ toState: 'expanded' } as any);
|
||||
});
|
||||
|
||||
it('should set the sidebarClosed to false', () => {
|
||||
expect(comp.sidebarClosed).toBeFalsy();
|
||||
})
|
||||
});
|
||||
|
||||
describe('when collapsing', () => {
|
||||
beforeEach(() => {
|
||||
comp.sidebarClosed = false;
|
||||
comp.startSlide({ toState: 'collapsed' } as any);
|
||||
});
|
||||
|
||||
it('should set the sidebarOpen to false', () => {
|
||||
expect(comp.sidebarOpen).toBeFalsy();
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
describe('finishSlide', () => {
|
||||
describe('when expanding', () => {
|
||||
beforeEach(() => {
|
||||
comp.sidebarClosed = true;
|
||||
comp.startSlide({ fromState: 'expanded' } as any);
|
||||
});
|
||||
|
||||
it('should set the sidebarClosed to true', () => {
|
||||
expect(comp.sidebarClosed).toBeTruthy();
|
||||
})
|
||||
});
|
||||
|
||||
describe('when collapsing', () => {
|
||||
beforeEach(() => {
|
||||
comp.sidebarClosed = false;
|
||||
comp.startSlide({ fromState: 'collapsed' } as any);
|
||||
});
|
||||
|
||||
it('should set the sidebarOpen to true', () => {
|
||||
expect(comp.sidebarOpen).toBeTruthy();
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
describe('when the collapse icon is clicked', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'toggleMenu');
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('#sidebar-collapse-toggle')).query(By.css('a.shortcut-icon'));
|
||||
sidebarToggler.triggerEventHandler('click', {preventDefault: () => {/**/}});
|
||||
});
|
||||
|
||||
it('should call toggleMenu on the menuService', () => {
|
||||
expect(menuService.toggleMenu).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the collapse link is clicked', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'toggleMenu');
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('#sidebar-collapse-toggle')).query(By.css('.sidebar-collapsible')).query(By.css('a'));
|
||||
sidebarToggler.triggerEventHandler('click', {preventDefault: () => {/**/}});
|
||||
});
|
||||
|
||||
it('should call toggleMenu on the menuService', () => {
|
||||
expect(menuService.toggleMenu).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the the mouse enters the nav tag', () => {
|
||||
it('should call expandPreview on the menuService after 100ms', fakeAsync(() => {
|
||||
spyOn(menuService, 'expandMenuPreview');
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('nav.navbar'));
|
||||
sidebarToggler.triggerEventHandler('mouseenter', {preventDefault: () => {/**/}});
|
||||
tick(99);
|
||||
expect(menuService.expandMenuPreview).not.toHaveBeenCalled();
|
||||
tick(1);
|
||||
expect(menuService.expandMenuPreview).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when the the mouse leaves the nav tag', () => {
|
||||
it('should call collapseMenuPreview on the menuService after 400ms', fakeAsync(() => {
|
||||
spyOn(menuService, 'collapseMenuPreview');
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('nav.navbar'));
|
||||
sidebarToggler.triggerEventHandler('mouseleave', {preventDefault: () => {/**/}});
|
||||
tick(399);
|
||||
expect(menuService.collapseMenuPreview).not.toHaveBeenCalled();
|
||||
tick(1);
|
||||
expect(menuService.collapseMenuPreview).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
});
|
479
src/app/+admin/admin-sidebar/admin-sidebar.component.ts
Normal file
479
src/app/+admin/admin-sidebar/admin-sidebar.component.ts
Normal file
@@ -0,0 +1,479 @@
|
||||
import { Component, Injector, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { slide, slideHorizontal, slideSidebar } from '../../shared/animations/slide';
|
||||
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
||||
import { MenuService } from '../../shared/menu/menu.service';
|
||||
import { MenuID, MenuItemType } from '../../shared/menu/initial-menus-state';
|
||||
import { MenuComponent } from '../../shared/menu/menu.component';
|
||||
import { TextMenuItemModel } from '../../shared/menu/menu-item/models/text.model';
|
||||
import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
import { combineLatest as combineLatestObservable } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Component representing the admin sidebar
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-admin-sidebar',
|
||||
templateUrl: './admin-sidebar.component.html',
|
||||
styleUrls: ['./admin-sidebar.component.scss'],
|
||||
animations: [slideHorizontal, slideSidebar]
|
||||
})
|
||||
export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
/**
|
||||
* The menu ID of the Navbar is PUBLIC
|
||||
* @type {MenuID.ADMIN}
|
||||
*/
|
||||
menuID = MenuID.ADMIN;
|
||||
|
||||
/**
|
||||
* Observable that emits the width of the collapsible menu sections
|
||||
*/
|
||||
sidebarWidth: Observable<string>;
|
||||
|
||||
/**
|
||||
* Is true when the sidebar is open, is false when the sidebar is animating or closed
|
||||
* @type {boolean}
|
||||
*/
|
||||
sidebarOpen = true; // Open in UI, animation finished
|
||||
|
||||
/**
|
||||
* Is true when the sidebar is closed, is false when the sidebar is animating or open
|
||||
* @type {boolean}
|
||||
*/
|
||||
sidebarClosed = !this.sidebarOpen; // Closed in UI, animation finished
|
||||
|
||||
/**
|
||||
* Emits true when either the menu OR the menu's preview is expanded, else emits false
|
||||
*/
|
||||
sidebarExpanded: Observable<boolean>;
|
||||
|
||||
constructor(protected menuService: MenuService,
|
||||
protected injector: Injector,
|
||||
private variableService: CSSVariableService,
|
||||
private authService: AuthService
|
||||
) {
|
||||
super(menuService, injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set and calculate all initial values of the instance variables
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.createMenu();
|
||||
super.ngOnInit();
|
||||
this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
|
||||
this.authService.isAuthenticated()
|
||||
.subscribe((loggedIn: boolean) => {
|
||||
if (loggedIn) {
|
||||
this.menuService.showMenu(this.menuID);
|
||||
}
|
||||
});
|
||||
this.menuCollapsed.pipe(first())
|
||||
.subscribe((collapsed: boolean) => {
|
||||
this.sidebarOpen = !collapsed;
|
||||
this.sidebarClosed = collapsed;
|
||||
});
|
||||
this.sidebarExpanded = combineLatestObservable(this.menuCollapsed, this.menuPreviewCollapsed)
|
||||
.pipe(
|
||||
map(([collapsed, previewCollapsed]) => (!collapsed || !previewCollapsed))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all menu sections and items for this menu
|
||||
*/
|
||||
private createMenu() {
|
||||
const menuList = [
|
||||
/* News */
|
||||
{
|
||||
id: 'new',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.new'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'plus-circle',
|
||||
index: 0
|
||||
},
|
||||
{
|
||||
id: 'new_community',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.new_community',
|
||||
link: '/communities/submission'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_collection',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.new_collection',
|
||||
link: '/collections/submission'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_item',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.new_item',
|
||||
link: '/items/submission'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_item_version',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.new_item_version',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
|
||||
/* Edit */
|
||||
{
|
||||
id: 'edit',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.edit'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'pencil-alt',
|
||||
index: 1
|
||||
},
|
||||
{
|
||||
id: 'edit_community',
|
||||
parentID: 'edit',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.edit_community',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'edit_collection',
|
||||
parentID: 'edit',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.edit_collection',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'edit_item',
|
||||
parentID: 'edit',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.edit_item',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
|
||||
/* Import */
|
||||
{
|
||||
id: 'import',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.import'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'sign-in-alt',
|
||||
index: 2
|
||||
},
|
||||
{
|
||||
id: 'import_metadata',
|
||||
parentID: 'import',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.import_metadata',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'import_batch',
|
||||
parentID: 'import',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.import_batch',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
|
||||
/* Export */
|
||||
{
|
||||
id: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.export'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'sign-out-alt',
|
||||
index: 3
|
||||
},
|
||||
{
|
||||
id: 'export_community',
|
||||
parentID: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.export_community',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'export_collection',
|
||||
parentID: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.export_collection',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'export_item',
|
||||
parentID: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.export_item',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
}, {
|
||||
id: 'export_metadata',
|
||||
parentID: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.export_metadata',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
|
||||
/* Access Control */
|
||||
{
|
||||
id: 'access_control',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.access_control'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'key',
|
||||
index: 4
|
||||
},
|
||||
{
|
||||
id: 'access_control_people',
|
||||
parentID: 'access_control',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_people',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'access_control_groups',
|
||||
parentID: 'access_control',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_groups',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'access_control_authorizations',
|
||||
parentID: 'access_control',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_authorizations',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
|
||||
/* Search */
|
||||
{
|
||||
id: 'find',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.find'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'search',
|
||||
index: 5
|
||||
},
|
||||
{
|
||||
id: 'find_items',
|
||||
parentID: 'find',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.find_items',
|
||||
link: '/search'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'find_withdrawn_items',
|
||||
parentID: 'find',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.find_withdrawn_items',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'find_private_items',
|
||||
parentID: 'find',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.find_private_items',
|
||||
link: '/admin/items'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
|
||||
/* Registries */
|
||||
{
|
||||
id: 'registries',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.registries'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'list',
|
||||
index: 6
|
||||
},
|
||||
{
|
||||
id: 'registries_metadata',
|
||||
parentID: 'registries',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.registries_metadata',
|
||||
link: 'admin/registries/metadata'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'registries_format',
|
||||
parentID: 'registries',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.registries_format',
|
||||
link: 'admin/registries/bitstream-formats'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
|
||||
/* Curation tasks */
|
||||
{
|
||||
id: 'curation_tasks',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.curation_task',
|
||||
link: '/curation'
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'filter',
|
||||
index: 7
|
||||
},
|
||||
|
||||
/* Statistics */
|
||||
{
|
||||
id: 'statistics_task',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.statistics_task',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'chart-bar',
|
||||
index: 8
|
||||
},
|
||||
|
||||
/* Control Panel */
|
||||
{
|
||||
id: 'control_panel',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.control_panel',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'cogs',
|
||||
index: 9
|
||||
},
|
||||
];
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to change this.collapsed to false when the slide animation ends and is sliding open
|
||||
* @param event The animation event
|
||||
*/
|
||||
startSlide(event: any): void {
|
||||
if (event.toState === 'expanded') {
|
||||
this.sidebarClosed = false;
|
||||
} else if (event.toState === 'collapsed') {
|
||||
this.sidebarOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to change this.collapsed to false when the slide animation ends and is sliding open
|
||||
* @param event The animation event
|
||||
*/
|
||||
finishSlide(event: any): void {
|
||||
if (event.fromState === 'expanded') {
|
||||
this.sidebarClosed = true;
|
||||
} else if (event.fromState === 'collapsed') {
|
||||
this.sidebarOpen = true;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
<li class="sidebar-section" [ngClass]="{'expanded': (expanded | async)}"
|
||||
[@bgColor]="{
|
||||
value: ((expanded | async) ? 'endBackground' : 'startBackground'),
|
||||
params: {endColor: (sidebarActiveBg | async)}}">
|
||||
<div class="icon-wrapper">
|
||||
<a class="nav-item nav-link shortcut-icon" (click)="toggleSection($event)" href="#">
|
||||
<i class="fas fa-{{section.icon}} fa-fw" [title]="('menu.section.icon.' + section.id) | translate"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="sidebar-collapsible">
|
||||
<a class="nav-item nav-link" href="#"
|
||||
(click)="toggleSection($event)">
|
||||
<span class="section-header-text">
|
||||
<ng-container
|
||||
*ngComponentOutlet="itemComponents.get(section.id); injector: itemInjectors.get(section.id);"></ng-container>
|
||||
</span>
|
||||
<i class="fas fa-chevron-right fa-pull-right"
|
||||
[@rotate]="(expanded | async) ? 'expanded' : 'collapsed'" [title]="('menu.section.toggle.' + section.id) | translate"></i>
|
||||
</a>
|
||||
<ul class="sidebar-sub-level-items list-unstyled" @slide *ngIf="(expanded | async)">
|
||||
<li *ngFor="let subSection of (subSections | async)">
|
||||
<ng-container
|
||||
*ngComponentOutlet="itemComponents.get(subSection.id); injector: itemInjectors.get(subSection.id);"></ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
@@ -0,0 +1,21 @@
|
||||
@import '../../../../styles/variables.scss';
|
||||
|
||||
::ng-deep {
|
||||
.fa-chevron-right {
|
||||
padding-left: $spacer/2;
|
||||
font-size: 0.5rem;
|
||||
line-height: 3;
|
||||
}
|
||||
|
||||
.sidebar-sub-level-items {
|
||||
list-style: disc;
|
||||
color: $navbar-dark-color;
|
||||
overflow: hidden;
|
||||
|
||||
}
|
||||
|
||||
.sidebar-collapsible {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
@@ -0,0 +1,84 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ExpandableAdminSidebarSectionComponent } from './expandable-admin-sidebar-section.component';
|
||||
import { MenuService } from '../../../shared/menu/menu.service';
|
||||
import { MenuServiceStub } from '../../../shared/testing/menu-service-stub';
|
||||
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
|
||||
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service-stub';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { Component } from '@angular/core';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
describe('ExpandableAdminSidebarSectionComponent', () => {
|
||||
let component: ExpandableAdminSidebarSectionComponent;
|
||||
let fixture: ComponentFixture<ExpandableAdminSidebarSectionComponent>;
|
||||
const menuService = new MenuServiceStub();
|
||||
const iconString = 'test';
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule, TranslateModule.forRoot()],
|
||||
declarations: [ExpandableAdminSidebarSectionComponent, TestComponent],
|
||||
providers: [
|
||||
{ provide: 'sectionDataProvider', useValue: {icon: iconString} },
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||
]
|
||||
}).overrideComponent(ExpandableAdminSidebarSectionComponent, {
|
||||
set: {
|
||||
entryComponents: [TestComponent]
|
||||
}
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([]));
|
||||
fixture = TestBed.createComponent(ExpandableAdminSidebarSectionComponent);
|
||||
component = fixture.componentInstance;
|
||||
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set the right icon', () => {
|
||||
const icon = fixture.debugElement.query(By.css('.icon-wrapper')).query(By.css('i.fas'));
|
||||
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
|
||||
});
|
||||
|
||||
describe('when the icon is clicked', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'toggleActiveSection');
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('a.shortcut-icon'));
|
||||
sidebarToggler.triggerEventHandler('click', {preventDefault: () => {/**/}});
|
||||
});
|
||||
|
||||
it('should call toggleActiveSection on the menuService', () => {
|
||||
expect(menuService.toggleActiveSection).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the header text is clicked', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'toggleActiveSection');
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('.sidebar-collapsible')).query(By.css('a'));
|
||||
sidebarToggler.triggerEventHandler('click', {preventDefault: () => {/**/}});
|
||||
});
|
||||
|
||||
it('should call toggleActiveSection on the menuService', () => {
|
||||
expect(menuService.toggleActiveSection).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// declare a test component
|
||||
@Component({
|
||||
selector: 'ds-test-cmp',
|
||||
template: ``
|
||||
})
|
||||
class TestComponent {
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
import { Component, Inject, Injector, OnInit } from '@angular/core';
|
||||
import { rotate } from '../../../shared/animations/rotate';
|
||||
import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sidebar-section.component';
|
||||
import { slide } from '../../../shared/animations/slide';
|
||||
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
|
||||
import { bgColor } from '../../../shared/animations/bgColor';
|
||||
import { MenuID } from '../../../shared/menu/initial-menus-state';
|
||||
import { MenuService } from '../../../shared/menu/menu.service';
|
||||
import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator';
|
||||
|
||||
/**
|
||||
* Represents a expandable section in the sidebar
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-expandable-admin-sidebar-section',
|
||||
templateUrl: './expandable-admin-sidebar-section.component.html',
|
||||
styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
|
||||
animations: [rotate, slide, bgColor]
|
||||
|
||||
})
|
||||
@rendersSectionForMenu(MenuID.ADMIN, true)
|
||||
export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionComponent implements OnInit {
|
||||
/**
|
||||
* This section resides in the Admin Sidebar
|
||||
*/
|
||||
menuID = MenuID.ADMIN;
|
||||
|
||||
/**
|
||||
* The background color of the section when it's active
|
||||
*/
|
||||
sidebarActiveBg;
|
||||
|
||||
/**
|
||||
* Emits true when the sidebar is currently collapsed, true when it's expanded
|
||||
*/
|
||||
sidebarCollapsed: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Emits true when the sidebar's preview is currently collapsed, true when it's expanded
|
||||
*/
|
||||
sidebarPreviewCollapsed: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Emits true when the menu section is expanded, else emits false
|
||||
* This is true when the section is active AND either the sidebar or it's preview is open
|
||||
*/
|
||||
expanded: Observable<boolean>;
|
||||
|
||||
constructor(@Inject('sectionDataProvider') menuSection, protected menuService: MenuService,
|
||||
private variableService: CSSVariableService, protected injector: Injector) {
|
||||
super(menuSection, menuService, injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set initial values for instance variables
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit();
|
||||
this.sidebarActiveBg = this.variableService.getVariable('adminSidebarActiveBg');
|
||||
this.sidebarCollapsed = this.menuService.isMenuCollapsed(this.menuID);
|
||||
this.sidebarPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID);
|
||||
this.expanded = combineLatestObservable(this.active, this.sidebarCollapsed, this.sidebarPreviewCollapsed)
|
||||
.pipe(
|
||||
map(([active, sidebarCollapsed, sidebarPreviewCollapsed]) => (active && (!sidebarCollapsed || !sidebarPreviewCollapsed)))
|
||||
);
|
||||
}
|
||||
}
|
@@ -5,7 +5,7 @@ import { AdminRoutingModule } from './admin-routing.module';
|
||||
@NgModule({
|
||||
imports: [
|
||||
AdminRegistriesModule,
|
||||
AdminRoutingModule
|
||||
AdminRoutingModule,
|
||||
]
|
||||
})
|
||||
export class AdminModule {
|
||||
|
@@ -24,8 +24,8 @@
|
||||
[content]="communityPayload.copyrightText"
|
||||
[hasInnerHtml]="true">
|
||||
</ds-comcol-page-content>
|
||||
<ds-community-page-sub-collection-list
|
||||
[community]="communityPayload"></ds-community-page-sub-collection-list>
|
||||
<ds-community-page-sub-community-list [community]="communityPayload"></ds-community-page-sub-community-list>
|
||||
<ds-community-page-sub-collection-list [community]="communityPayload"></ds-community-page-sub-collection-list>
|
||||
</div>
|
||||
</div>
|
||||
<ds-error *ngIf="communityRD?.hasFailed" message="{{'error.community' | translate}}"></ds-error>
|
||||
|
@@ -6,6 +6,7 @@ import { SharedModule } from '../shared/shared.module';
|
||||
import { CommunityPageComponent } from './community-page.component';
|
||||
import { CommunityPageSubCollectionListComponent } from './sub-collection-list/community-page-sub-collection-list.component';
|
||||
import { CommunityPageRoutingModule } from './community-page-routing.module';
|
||||
import {CommunityPageSubCommunityListComponent} from './sub-community-list/community-page-sub-community-list.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -16,6 +17,7 @@ import { CommunityPageRoutingModule } from './community-page-routing.module';
|
||||
declarations: [
|
||||
CommunityPageComponent,
|
||||
CommunityPageSubCollectionListComponent,
|
||||
CommunityPageSubCommunityListComponent,
|
||||
]
|
||||
})
|
||||
export class CommunityPageModule {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<ng-container *ngVar="(subCollectionsRDObs | async) as subCollectionsRD">
|
||||
<div *ngIf="subCollectionsRD?.hasSucceeded" @fadeIn>
|
||||
<div *ngIf="subCollectionsRD?.hasSucceeded && subCollectionsRD?.payload.totalElements > 0" @fadeIn>
|
||||
<h2>{{'community.sub-collection-list.head' | translate}}</h2>
|
||||
<ul>
|
||||
<li *ngFor="let collection of subCollectionsRD?.payload.page">
|
||||
|
@@ -0,0 +1,15 @@
|
||||
<ng-container *ngVar="(subCommunitiesRDObs | async) as subCommunitiesRD">
|
||||
<div *ngIf="subCommunitiesRD?.hasSucceeded && subCommunitiesRD?.payload.totalElements > 0" @fadeIn>
|
||||
<h2>{{'community.sub-community-list.head' | translate}}</h2>
|
||||
<ul>
|
||||
<li *ngFor="let community of subCommunitiesRD?.payload.page">
|
||||
<p>
|
||||
<span class="lead"><a [routerLink]="['/communities', community.id]">{{community.name}}</a></span><br>
|
||||
<span class="text-muted">{{community.shortDescription}}</span>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<ds-error *ngIf="subCommunitiesRD?.hasFailed" message="{{'error.sub-communities' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="subCommunitiesRD?.isLoading" message="{{'loading.sub-communities' | translate}}"></ds-loading>
|
||||
</ng-container>
|
@@ -0,0 +1 @@
|
||||
@import '../../../styles/variables.scss';
|
@@ -0,0 +1,96 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {NO_ERRORS_SCHEMA} from '@angular/core';
|
||||
import {CommunityPageSubCommunityListComponent} from './community-page-sub-community-list.component';
|
||||
import {Community} from '../../core/shared/community.model';
|
||||
import {RemoteData} from '../../core/data/remote-data';
|
||||
import {PaginatedList} from '../../core/data/paginated-list';
|
||||
import {PageInfo} from '../../core/shared/page-info.model';
|
||||
import {SharedModule} from '../../shared/shared.module';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {of as observableOf, Observable } from 'rxjs';
|
||||
|
||||
describe('SubCommunityList Component', () => {
|
||||
let comp: CommunityPageSubCommunityListComponent;
|
||||
let fixture: ComponentFixture<CommunityPageSubCommunityListComponent>;
|
||||
|
||||
const subcommunities = [Object.assign(new Community(), {
|
||||
name: 'SubCommunity 1',
|
||||
id: '123456789-1',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'SubCommunity 1'
|
||||
}]
|
||||
}),
|
||||
Object.assign(new Community(), {
|
||||
name: 'SubCommunity 2',
|
||||
id: '123456789-2',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'SubCommunity 2'
|
||||
}]
|
||||
})
|
||||
];
|
||||
|
||||
const emptySubCommunitiesCommunity = Object.assign(new Community(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
}],
|
||||
subcommunities: observableOf(new RemoteData(true, true, true,
|
||||
undefined, new PaginatedList(new PageInfo(), [])))
|
||||
});
|
||||
|
||||
const mockCommunity = Object.assign(new Community(), {
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Test title'
|
||||
}],
|
||||
subcommunities: observableOf(new RemoteData(true, true, true,
|
||||
undefined, new PaginatedList(new PageInfo(), subcommunities)))
|
||||
})
|
||||
;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), SharedModule,
|
||||
RouterTestingModule.withRoutes([]),
|
||||
NoopAnimationsModule],
|
||||
declarations: [CommunityPageSubCommunityListComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CommunityPageSubCommunityListComponent);
|
||||
comp = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should display a list of subCommunities', () => {
|
||||
comp.community = mockCommunity;
|
||||
fixture.detectChanges();
|
||||
|
||||
const subComList = fixture.debugElement.queryAll(By.css('li'));
|
||||
expect(subComList.length).toEqual(2);
|
||||
expect(subComList[0].nativeElement.textContent).toContain('SubCommunity 1');
|
||||
expect(subComList[1].nativeElement.textContent).toContain('SubCommunity 2');
|
||||
});
|
||||
|
||||
it('should not display the header when subCommunities are empty', () => {
|
||||
comp.community = emptySubCommunitiesCommunity;
|
||||
fixture.detectChanges();
|
||||
|
||||
const subComHead = fixture.debugElement.queryAll(By.css('h2'));
|
||||
expect(subComHead.length).toEqual(0);
|
||||
});
|
||||
});
|
@@ -0,0 +1,26 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
|
||||
import { fadeIn } from '../../shared/animations/fade';
|
||||
import { PaginatedList } from '../../core/data/paginated-list';
|
||||
import {Observable} from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-community-page-sub-community-list',
|
||||
styleUrls: ['./community-page-sub-community-list.component.scss'],
|
||||
templateUrl: './community-page-sub-community-list.component.html',
|
||||
animations:[fadeIn]
|
||||
})
|
||||
/**
|
||||
* Component to render the sub-communities of a Community
|
||||
*/
|
||||
export class CommunityPageSubCommunityListComponent implements OnInit {
|
||||
@Input() community: Community;
|
||||
subCommunitiesRDObs: Observable<RemoteData<PaginatedList<Community>>>;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subCommunitiesRDObs = this.community.subcommunities;
|
||||
}
|
||||
}
|
@@ -8,6 +8,9 @@
|
||||
|
||||
.dspace-logo-container {
|
||||
margin: 10px 20px 0px 20px;
|
||||
.display-3 {
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.dspace-logo-container img {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<div class="container w-100 h-100">
|
||||
<div class="text-center mt-5 row justify-content-md-center">
|
||||
<div>
|
||||
<div class="mx-auto">
|
||||
<img class="mb-4 login-logo" src="assets/images/dspace-logo.png">
|
||||
<h1 class="h3 mb-0 font-weight-normal">{{"logout.form.header" | translate}}</h1>
|
||||
<ds-log-out></ds-log-out>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div>
|
||||
<div (click)="toggle()" class="filter-name"><h5 class="d-inline-block mb-0">{{'search.filters.filter.' + filter.name + '.head'| translate}}</h5> <span class="filter-toggle fa float-right"
|
||||
<div (click)="toggle()" class="filter-name"><h5 class="d-inline-block mb-0">{{'search.filters.filter.' + filter.name + '.head'| translate}}</h5> <span class="filter-toggle fas float-right"
|
||||
[ngClass]="(isCollapsed() | async) ? 'fa-plus' : 'fa-minus'"></span></div>
|
||||
<div [@slide]="(isCollapsed() | async) ? 'collapsed' : 'expanded'" (@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)" class="search-filter-wrapper" [ngClass]="{'closed' : collapsed}">
|
||||
<ds-search-facet-filter-wrapper [filterConfig]="filter"></ds-search-facet-filter-wrapper>
|
||||
|
@@ -2,11 +2,12 @@
|
||||
@import '../../../../styles/mixins.scss';
|
||||
|
||||
:host {
|
||||
border: 1px solid map-get($theme-colors, light);
|
||||
.search-filter-wrapper.closed {
|
||||
overflow: hidden;
|
||||
}
|
||||
.filter-toggle {
|
||||
line-height: $line-height-base;
|
||||
}
|
||||
border: 1px solid map-get($theme-colors, light);
|
||||
cursor: pointer;
|
||||
.search-filter-wrapper.closed {
|
||||
overflow: hidden;
|
||||
}
|
||||
.filter-toggle {
|
||||
line-height: $line-height-base;
|
||||
}
|
||||
}
|
@@ -26,7 +26,7 @@
|
||||
<ds-view-mode-switch></ds-view-mode-switch>
|
||||
<button (click)="openSidebar()" aria-controls="#search-body"
|
||||
class="btn btn-outline-primary float-right open-sidebar"><i
|
||||
class="fa fa-sliders"></i> {{"search.sidebar.open"
|
||||
class="fas fa-sliders"></i> {{"search.sidebar.open"
|
||||
| translate}}
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<button (click)="toggleSidebar.emit()"
|
||||
aria-controls="#search-body"
|
||||
class="btn btn-outline-primary float-right close-sidebar"><i
|
||||
class="fa fa-arrow-right"></i> {{"search.sidebar.close" | translate}}
|
||||
class="fas fa-arrow-right" [title]="'search.sidebar.close' | translate"></i> {{"search.sidebar.close" | translate}}
|
||||
</button>
|
||||
</div>
|
||||
<div id="search-sidebar-content">
|
||||
|
@@ -1,20 +1,22 @@
|
||||
<div class="outer-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<ds-header></ds-header>
|
||||
<ds-admin-sidebar></ds-admin-sidebar>
|
||||
<div class="inner-wrapper" [@slideSidebarPadding]="{
|
||||
value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'),
|
||||
params: {collapsedSidebarWidth: (collapsedSidebarWidth | async), totalSidebarWidth: (totalSidebarWidth | async)}
|
||||
}">
|
||||
<ds-header-navbar-wrapper></ds-header-navbar-wrapper>
|
||||
|
||||
<ds-notifications-board
|
||||
[options]="config.notifications">
|
||||
</ds-notifications-board>
|
||||
<ds-notifications-board
|
||||
[options]="config.notifications">
|
||||
</ds-notifications-board>
|
||||
|
||||
<main class="main-content">
|
||||
<div class="container" *ngIf="isLoading">
|
||||
<ds-loading message="{{'loading.default' | translate}}"></ds-loading>
|
||||
</div>
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
<main class="main-content">
|
||||
<div class="container" *ngIf="isLoading">
|
||||
<ds-loading message="{{'loading.default' | translate}}"></ds-loading>
|
||||
</div>
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
<ds-footer></ds-footer>
|
||||
</div>
|
||||
<ds-footer></ds-footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
@import '../styles/variables.scss';
|
||||
@import '../styles/helpers/font_awesome_imports.scss';
|
||||
@import '../../node_modules/bootstrap/scss/bootstrap.scss';
|
||||
@import '../../node_modules/nouislider/distribute/nouislider.min.css';
|
||||
@import "../../node_modules/font-awesome/scss/font-awesome.scss";
|
||||
@import '../../node_modules/nouislider/distribute/nouislider.min';
|
||||
|
||||
html {
|
||||
position: relative;
|
||||
@@ -11,8 +11,8 @@ html {
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
// Sticky Footer
|
||||
|
||||
// Sticky Footer
|
||||
.outer-wrapper {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
@@ -25,10 +25,22 @@ body {
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
z-index: $main-z-index;
|
||||
flex: 1 0 auto;
|
||||
margin-top: $content-spacing;
|
||||
margin-bottom: $content-spacing;
|
||||
}
|
||||
|
||||
ds-header-navbar-wrapper {
|
||||
z-index: $nav-z-index;
|
||||
}
|
||||
|
||||
ds-admin-sidebar {
|
||||
position: fixed;
|
||||
z-index: $sidebar-z-index;
|
||||
}
|
||||
|
||||
|
@@ -35,11 +35,18 @@ import { AngularticsMock } from './shared/mocks/mock-angulartics.service';
|
||||
import { AuthServiceMock } from './shared/mocks/mock-auth.service';
|
||||
import { AuthService } from './core/auth/auth.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { MenuService } from './shared/menu/menu.service';
|
||||
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
||||
import { CSSVariableServiceStub } from './shared/testing/css-variable-service-stub';
|
||||
import { MenuServiceStub } from './shared/testing/menu-service-stub';
|
||||
import { HostWindowService } from './shared/host-window.service';
|
||||
import { HostWindowServiceStub } from './shared/testing/host-window-service-stub';
|
||||
|
||||
let comp: AppComponent;
|
||||
let fixture: ComponentFixture<AppComponent>;
|
||||
let de: DebugElement;
|
||||
let el: HTMLElement;
|
||||
const menuService = new MenuServiceStub();
|
||||
|
||||
describe('App component', () => {
|
||||
|
||||
@@ -64,6 +71,9 @@ describe('App component', () => {
|
||||
{ provide: Angulartics2GoogleAnalytics, useValue: new AngularticsMock() },
|
||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||
{ provide: Router, useValue: {} },
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||
AppComponent
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
@@ -75,7 +85,6 @@ describe('App component', () => {
|
||||
fixture = TestBed.createComponent(AppComponent);
|
||||
|
||||
comp = fixture.componentInstance; // component test instance
|
||||
|
||||
// query for the <div class='outer-wrapper'> by CSS element selector
|
||||
de = fixture.debugElement.query(By.css('div.outer-wrapper'));
|
||||
el = de.nativeElement;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { filter, first, take } from 'rxjs/operators';
|
||||
import { filter, first, map, take } from 'rxjs/operators';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
@@ -23,16 +23,29 @@ import { NativeWindowRef, NativeWindowService } from './shared/services/window.s
|
||||
import { isAuthenticated } from './core/auth/selectors';
|
||||
import { AuthService } from './core/auth/auth.service';
|
||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||
import variables from '../styles/_exposed_variables.scss';
|
||||
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
||||
import { MenuService } from './shared/menu/menu.service';
|
||||
import { MenuID } from './shared/menu/initial-menus-state';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { slideSidebarPadding } from './shared/animations/slide';
|
||||
import { combineLatest as combineLatestObservable } from 'rxjs';
|
||||
import { HostWindowService } from './shared/host-window.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
animations: [slideSidebarPadding]
|
||||
})
|
||||
export class AppComponent implements OnInit, AfterViewInit {
|
||||
isLoading = true;
|
||||
sidebarVisible: Observable<boolean>;
|
||||
slideSidebarOver: Observable<boolean>;
|
||||
collapsedSidebarWidth: Observable<string>;
|
||||
totalSidebarWidth: Observable<string>;
|
||||
|
||||
constructor(
|
||||
@Inject(GLOBAL_CONFIG) public config: GlobalConfig,
|
||||
@@ -42,7 +55,10 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
private metadata: MetadataService,
|
||||
private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics,
|
||||
private authService: AuthService,
|
||||
private router: Router
|
||||
private router: Router,
|
||||
private cssService: CSSVariableService,
|
||||
private menuService: MenuService,
|
||||
private windowService: HostWindowService
|
||||
) {
|
||||
// this language will be used as a fallback when a translation isn't found in the current language
|
||||
translate.setDefaultLang('en');
|
||||
@@ -54,9 +70,12 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
if (config.debug) {
|
||||
console.info(config);
|
||||
}
|
||||
this.storeCSSVariables();
|
||||
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
const env: string = this.config.production ? 'Production' : 'Development';
|
||||
const color: string = this.config.production ? 'red' : 'green';
|
||||
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
|
||||
@@ -67,7 +86,23 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
take(1),
|
||||
filter((authenticated) => !authenticated)
|
||||
).subscribe((authenticated) => this.authService.checkAuthenticationToken());
|
||||
this.sidebarVisible = this.menuService.isMenuVisible(MenuID.ADMIN);
|
||||
|
||||
this.collapsedSidebarWidth = this.cssService.getVariable('collapsedSidebarWidth');
|
||||
this.totalSidebarWidth = this.cssService.getVariable('totalSidebarWidth');
|
||||
|
||||
const sidebarCollapsed = this.menuService.isMenuCollapsed(MenuID.ADMIN);
|
||||
this.slideSidebarOver = combineLatestObservable(sidebarCollapsed, this.windowService.isXsOrSm())
|
||||
.pipe(
|
||||
map(([collapsed, mobile]) => collapsed || mobile)
|
||||
);
|
||||
}
|
||||
|
||||
private storeCSSVariables() {
|
||||
const vars = variables.locals || {};
|
||||
Object.keys(vars).forEach((name: string) => {
|
||||
this.cssService.addCSSVariable(name, vars[name]);
|
||||
})
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
|
@@ -1,10 +1,9 @@
|
||||
|
||||
import { HeaderEffects } from './header/header.effects';
|
||||
import { StoreEffects } from './store.effects';
|
||||
import { NotificationsEffects } from './shared/notifications/notifications.effects';
|
||||
import { NavbarEffects } from './navbar/navbar.effects';
|
||||
|
||||
export const appEffects = [
|
||||
StoreEffects,
|
||||
HeaderEffects,
|
||||
NotificationsEffects
|
||||
NavbarEffects,
|
||||
NotificationsEffects,
|
||||
];
|
||||
|
@@ -31,6 +31,11 @@ import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-s
|
||||
import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component';
|
||||
import { NotificationComponent } from './shared/notifications/notification/notification.component';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
import { HeaderNavbarWrapperComponent } from './header-nav-wrapper/header-navbar-wrapper.component';
|
||||
import { AdminSidebarComponent } from './+admin/admin-sidebar/admin-sidebar.component';
|
||||
import { AdminSidebarSectionComponent } from './+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
|
||||
import { ExpandableAdminSidebarSectionComponent } from './+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
|
||||
import { NavbarModule } from './navbar/navbar.module';
|
||||
|
||||
export function getConfig() {
|
||||
return ENV_CONFIG;
|
||||
@@ -48,6 +53,7 @@ export function getMetaReducers(config: GlobalConfig): Array<MetaReducer<AppStat
|
||||
const IMPORTS = [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
NavbarModule,
|
||||
HttpClientModule,
|
||||
AppRoutingModule,
|
||||
CoreModule.forRoot(),
|
||||
@@ -88,6 +94,10 @@ const PROVIDERS = [
|
||||
const DECLARATIONS = [
|
||||
AppComponent,
|
||||
HeaderComponent,
|
||||
HeaderNavbarWrapperComponent,
|
||||
AdminSidebarComponent,
|
||||
AdminSidebarSectionComponent,
|
||||
ExpandableAdminSidebarSectionComponent,
|
||||
FooterComponent,
|
||||
PageNotFoundComponent,
|
||||
NotificationComponent,
|
||||
@@ -106,10 +116,14 @@ const EXPORTS = [
|
||||
...PROVIDERS
|
||||
],
|
||||
declarations: [
|
||||
...DECLARATIONS
|
||||
...DECLARATIONS,
|
||||
],
|
||||
exports: [
|
||||
...EXPORTS
|
||||
],
|
||||
entryComponents: [
|
||||
AdminSidebarSectionComponent,
|
||||
ExpandableAdminSidebarSectionComponent
|
||||
]
|
||||
})
|
||||
export class AppModule {
|
||||
|
@@ -1,7 +1,5 @@
|
||||
import { ActionReducerMap } from '@ngrx/store';
|
||||
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
|
||||
import * as fromRouter from '@ngrx/router-store';
|
||||
|
||||
import { headerReducer, HeaderState } from './header/header.reducer';
|
||||
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
|
||||
import { formReducer, FormState } from './shared/form/form.reducer';
|
||||
import {
|
||||
@@ -12,36 +10,50 @@ import {
|
||||
filterReducer,
|
||||
SearchFiltersState
|
||||
} from './+search-page/search-filters/search-filter/search-filter.reducer';
|
||||
import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers';
|
||||
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
||||
import {
|
||||
ObjectSelectionListState,
|
||||
objectSelectionReducer,
|
||||
ObjectSelectionsState
|
||||
} from './shared/object-select/object-select.reducer';
|
||||
notificationsReducer,
|
||||
NotificationsState
|
||||
} from './shared/notifications/notifications.reducers';
|
||||
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
||||
import { hasValue } from './shared/empty.util';
|
||||
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
|
||||
import { menusReducer, MenusState } from './shared/menu/menu.reducer';
|
||||
import { ObjectSelectionListState, objectSelectionReducer } from './shared/object-select/object-select.reducer';
|
||||
|
||||
export interface AppState {
|
||||
router: fromRouter.RouterReducerState;
|
||||
hostWindow: HostWindowState;
|
||||
header: HeaderState;
|
||||
forms: FormState;
|
||||
notifications: NotificationsState;
|
||||
searchSidebar: SearchSidebarState;
|
||||
searchFilter: SearchFiltersState;
|
||||
truncatable: TruncatablesState,
|
||||
objectSelection: ObjectSelectionListState
|
||||
truncatable: TruncatablesState;
|
||||
cssVariables: CSSVariablesState;
|
||||
menus: MenusState;
|
||||
objectSelection: ObjectSelectionListState;
|
||||
}
|
||||
|
||||
export const appReducers: ActionReducerMap<AppState> = {
|
||||
router: fromRouter.routerReducer,
|
||||
hostWindow: hostWindowReducer,
|
||||
header: headerReducer,
|
||||
forms: formReducer,
|
||||
notifications: notificationsReducer,
|
||||
searchSidebar: sidebarReducer,
|
||||
searchFilter: filterReducer,
|
||||
truncatable: truncatableReducer,
|
||||
cssVariables: cssVariablesReducer,
|
||||
menus: menusReducer,
|
||||
objectSelection: objectSelectionReducer
|
||||
};
|
||||
|
||||
export const routerStateSelector = (state: AppState) => state.router;
|
||||
|
||||
export function keySelector<T>(key: string, selector): MemoizedSelector<AppState, T> {
|
||||
return createSelector(selector, (state) => {
|
||||
if (hasValue(state)) {
|
||||
return state[key];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -63,6 +63,8 @@ import { NotificationsService } from '../shared/notifications/notifications.serv
|
||||
import { UploaderService } from '../shared/uploader/uploader.service';
|
||||
import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service';
|
||||
import { DSpaceObjectDataService } from './data/dspace-object-data.service';
|
||||
import { CSSVariableService } from '../shared/sass-helper/sass-helper.service';
|
||||
import { MenuService } from '../shared/menu/menu.service';
|
||||
import { MappingCollectionsReponseParsingService } from './data/mapping-collections-reponse-parsing.service';
|
||||
import { ObjectSelectService } from '../shared/object-select/object-select.service';
|
||||
|
||||
@@ -130,6 +132,8 @@ const PROVIDERS = [
|
||||
UUIDService,
|
||||
DSpaceObjectDataService,
|
||||
ObjectSelectService,
|
||||
CSSVariableService,
|
||||
MenuService,
|
||||
// register AuthInterceptor as HttpInterceptor
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { filter, map, tap } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Effect, Actions, ofType } from '@ngrx/effects';
|
||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||
|
||||
import {
|
||||
ObjectCacheActionTypes, AddToObjectCacheAction,
|
||||
AddToObjectCacheAction,
|
||||
ObjectCacheActionTypes,
|
||||
RemoveFromObjectCacheAction
|
||||
} from '../cache/object-cache.actions';
|
||||
import { RequestActionTypes, RequestConfigureAction } from '../data/request.actions';
|
||||
@@ -11,6 +12,11 @@ import { AddToIndexAction, RemoveFromIndexByValueAction } from './index.actions'
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { IndexName } from './index.reducer';
|
||||
import { RestRequestMethod } from '../data/rest-request-method';
|
||||
import {
|
||||
AddMenuSectionAction,
|
||||
MenuActionTypes,
|
||||
RemoveMenuSectionAction
|
||||
} from '../../shared/menu/menu.actions';
|
||||
|
||||
@Injectable()
|
||||
export class UUIDIndexEffects {
|
||||
@@ -52,17 +58,6 @@ export class UUIDIndexEffects {
|
||||
})
|
||||
);
|
||||
|
||||
// @Effect() removeRequest$ = this.actions$
|
||||
// .pipe(
|
||||
// ofType(ObjectCacheActionTypes.REMOVE),
|
||||
// map((action: RemoveFromObjectCacheAction) => {
|
||||
// return new RemoveFromIndexByValueAction(
|
||||
// IndexName.OBJECT,
|
||||
// action.payload
|
||||
// );
|
||||
// })
|
||||
// )
|
||||
|
||||
constructor(private actions$: Actions) {
|
||||
|
||||
}
|
||||
|
@@ -61,6 +61,6 @@ export class Community extends DSpaceObject {
|
||||
|
||||
collections: Observable<RemoteData<PaginatedList<Collection>>>;
|
||||
|
||||
subcommunities: Observable<RemoteData<PaginatedList<Collection>>>;
|
||||
subcommunities: Observable<RemoteData<PaginatedList<Community>>>;
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,4 @@
|
||||
<div [ngClass]="{'open': !(isNavBarCollapsed | async)}">
|
||||
<ds-header></ds-header>
|
||||
<ds-navbar></ds-navbar>
|
||||
</div>
|
@@ -0,0 +1,9 @@
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
@media screen and (max-width: map-get($grid-breakpoints, md)) {
|
||||
:host.open {
|
||||
background-color: $white;
|
||||
top: 0;
|
||||
position: sticky;
|
||||
}
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
import { Component, HostBinding, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '../app.reducer';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { Subscription } from 'rxjs/internal/Subscription';
|
||||
import { MenuService } from '../shared/menu/menu.service';
|
||||
import { MenuID } from '../shared/menu/initial-menus-state';
|
||||
|
||||
/**
|
||||
* This component represents a wrapper for the horizontal navbar and the header
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-header-navbar-wrapper',
|
||||
styleUrls: ['header-navbar-wrapper.component.scss'],
|
||||
templateUrl: 'header-navbar-wrapper.component.html',
|
||||
})
|
||||
export class HeaderNavbarWrapperComponent implements OnInit, OnDestroy {
|
||||
@HostBinding('class.open') isOpen = false;
|
||||
private sub: Subscription;
|
||||
public isNavBarCollapsed: Observable<boolean>;
|
||||
menuID = MenuID.PUBLIC;
|
||||
|
||||
constructor(
|
||||
private store: Store<AppState>,
|
||||
private menuService: MenuService
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isNavBarCollapsed = this.menuService.isMenuCollapsed(this.menuID);
|
||||
this.sub = this.isNavBarCollapsed.subscribe((isCollapsed) => this.isOpen = !isCollapsed)
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (hasValue(this.sub)) {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,40 +0,0 @@
|
||||
import { Action } from '@ngrx/store';
|
||||
|
||||
import { type } from '../shared/ngrx/type';
|
||||
|
||||
/**
|
||||
* For each action type in an action group, make a simple
|
||||
* enum object for all of this group's action types.
|
||||
*
|
||||
* The 'type' utility function coerces strings into string
|
||||
* literal types and runs a simple check to guarantee all
|
||||
* action types in the application are unique.
|
||||
*/
|
||||
export const HeaderActionTypes = {
|
||||
COLLAPSE: type('dspace/header/COLLAPSE'),
|
||||
EXPAND: type('dspace/header/EXPAND'),
|
||||
TOGGLE: type('dspace/header/TOGGLE')
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
export class HeaderCollapseAction implements Action {
|
||||
type = HeaderActionTypes.COLLAPSE;
|
||||
}
|
||||
|
||||
export class HeaderExpandAction implements Action {
|
||||
type = HeaderActionTypes.EXPAND;
|
||||
}
|
||||
|
||||
export class HeaderToggleAction implements Action {
|
||||
type = HeaderActionTypes.TOGGLE;
|
||||
}
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
* Export a type alias of all actions in this action group
|
||||
* so that reducers can easily compose action types
|
||||
*/
|
||||
export type HeaderAction
|
||||
= HeaderCollapseAction
|
||||
| HeaderExpandAction
|
||||
| HeaderToggleAction
|
@@ -1,18 +1,20 @@
|
||||
<header>
|
||||
<nav class="navbar navbar-dark bg-primary navbar-expand-md">
|
||||
<div [ngClass]="{'clearfix': !(isNavBarCollapsed | async)}">
|
||||
<a class="navbar-brand" routerLink="/home">{{ 'title' | translate }}</a>
|
||||
<div class="container">
|
||||
<a class="navbar-brand my-2" routerLink="/home">
|
||||
<img src="assets/images/dspace-logo.svg"/>
|
||||
</a>
|
||||
|
||||
<nav class="navbar navbar-light navbar-expand-md float-right px-0">
|
||||
<a href="#" class="px-1"><i class="fas fa-search fa-lg fa-fw" [title]="'nav.search' | translate"></i></a>
|
||||
<a href="#" class="px-1"><i class="fas fa-globe-asia fa-lg fa-fw" [title]="'nav.language' | translate"></i></a>
|
||||
<ds-auth-nav-menu></ds-auth-nav-menu>
|
||||
<div class="pl-2">
|
||||
<button class="navbar-toggler" type="button" (click)="toggleNavbar()"
|
||||
aria-controls="collapsingNav"
|
||||
aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon fas fa-bars fa-fw" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<button class="navbar-toggler" type="button" (click)="toggle()" aria-controls="collapsingNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon fa fa-bars fa-fw" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div [ngbCollapse]="(isNavBarCollapsed | async)" class="collapse navbar-collapse" id="collapsingNav">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="/home" routerLinkActive="active"><i class="fa fa-home fa-fw" aria-hidden="true"></i> {{ 'nav.home' | translate }}<span class="sr-only">(current)</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
<ds-auth-nav-menu></ds-auth-nav-menu>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
@@ -1,14 +1,12 @@
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
header nav.navbar {
|
||||
border-radius: 0;
|
||||
.navbar-brand img {
|
||||
height: $header-logo-height;
|
||||
@media screen and (max-width: map-get($grid-breakpoints, sm)) {
|
||||
height: $header-logo-height-xs;
|
||||
}
|
||||
}
|
||||
|
||||
header nav.navbar .navbar-toggler:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
header nav.navbar .navbar-toggler .navbar-toggler-icon {
|
||||
background-image: none !important;
|
||||
line-height: 1.5;
|
||||
.navbar-toggler .navbar-toggler-icon {
|
||||
background-image: none !important;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
@@ -1,42 +1,31 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { HeaderComponent } from './header.component';
|
||||
import { HeaderState } from './header.reducer';
|
||||
import { HeaderToggleAction } from './header.actions';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { HostWindowServiceStub } from '../shared/testing/host-window-service-stub';
|
||||
import { RouterStub } from '../shared/testing/router-stub';
|
||||
import { Router } from '@angular/router';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import * as ngrx from '@ngrx/store';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { MenuService } from '../shared/menu/menu.service';
|
||||
import { MenuServiceStub } from '../shared/testing/menu-service-stub';
|
||||
import { HeaderComponent } from './header.component';
|
||||
|
||||
let comp: HeaderComponent;
|
||||
let fixture: ComponentFixture<HeaderComponent>;
|
||||
let store: Store<HeaderState>;
|
||||
|
||||
describe('HeaderComponent', () => {
|
||||
const menuService = new MenuServiceStub();
|
||||
|
||||
// async beforeEach
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
StoreModule.forRoot({}),
|
||||
TranslateModule.forRoot(),
|
||||
NgbCollapseModule.forRoot(),
|
||||
NoopAnimationsModule,
|
||||
ReactiveFormsModule],
|
||||
declarations: [HeaderComponent],
|
||||
providers: [
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||
{ provide: Router, useClass: RouterStub },
|
||||
{ provide: MenuService, useValue: menuService }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
@@ -45,63 +34,25 @@ describe('HeaderComponent', () => {
|
||||
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'getMenuTopSections').and.returnValue(observableOf([]));
|
||||
|
||||
fixture = TestBed.createComponent(HeaderComponent);
|
||||
|
||||
comp = fixture.componentInstance;
|
||||
|
||||
store = fixture.debugElement.injector.get(Store) as Store<HeaderState>;
|
||||
spyOn(store, 'dispatch');
|
||||
});
|
||||
|
||||
describe('when the toggle button is clicked', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'toggleMenu');
|
||||
const navbarToggler = fixture.debugElement.query(By.css('.navbar-toggler'));
|
||||
navbarToggler.triggerEventHandler('click', null);
|
||||
});
|
||||
|
||||
it('should dispatch a HeaderToggleAction', () => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new HeaderToggleAction());
|
||||
it('should call toggleMenu on the menuService', () => {
|
||||
expect(menuService.toggleMenu).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when navCollapsed in the store is true', () => {
|
||||
let menu: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
menu = fixture.debugElement.query(By.css('#collapsingNav')).nativeElement;
|
||||
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
||||
return () => {
|
||||
return () => observableOf({ navCollapsed: true })
|
||||
};
|
||||
});
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should close the menu', () => {
|
||||
expect(menu.classList).not.toContain('show');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when navCollapsed in the store is false', () => {
|
||||
let menu: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
menu = fixture.debugElement.query(By.css('#collapsingNav')).nativeElement;
|
||||
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
||||
return () => {
|
||||
return () => observableOf(false)
|
||||
};
|
||||
});
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should open the menu', () => {
|
||||
expect(menu.classList).toContain('show');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -1,43 +1,31 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { createSelector, select, Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs';
|
||||
import { RouterReducerState } from '@ngrx/router-store';
|
||||
|
||||
import { HeaderState } from './header.reducer';
|
||||
import { HeaderToggleAction } from './header.actions';
|
||||
import { AppState } from '../app.reducer';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
|
||||
const headerStateSelector = (state: AppState) => state.header;
|
||||
const navCollapsedSelector = createSelector(headerStateSelector, (header: HeaderState) => header.navCollapsed);
|
||||
import { Component } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { MenuService } from '../shared/menu/menu.service';
|
||||
import { MenuID } from '../shared/menu/initial-menus-state';
|
||||
|
||||
/**
|
||||
* Represents the header with the logo and simple navigation
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-header',
|
||||
styleUrls: ['header.component.scss'],
|
||||
templateUrl: 'header.component.html',
|
||||
})
|
||||
export class HeaderComponent implements OnInit {
|
||||
export class HeaderComponent {
|
||||
/**
|
||||
* Whether user is authenticated.
|
||||
* @type {Observable<string>}
|
||||
*/
|
||||
public isAuthenticated: Observable<boolean>;
|
||||
public isNavBarCollapsed: Observable<boolean>;
|
||||
public showAuth = false;
|
||||
menuID = MenuID.PUBLIC;
|
||||
|
||||
constructor(
|
||||
private store: Store<AppState>,
|
||||
private windowService: HostWindowService
|
||||
private menuService: MenuService
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// set loading
|
||||
this.isNavBarCollapsed = this.store.pipe(select(navCollapsedSelector));
|
||||
public toggleNavbar(): void {
|
||||
this.menuService.toggleMenu(this.menuID);
|
||||
}
|
||||
|
||||
public toggle(): void {
|
||||
this.store.dispatch(new HeaderToggleAction());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,28 +0,0 @@
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Effect, Actions, ofType } from '@ngrx/effects'
|
||||
import * as fromRouter from '@ngrx/router-store';
|
||||
|
||||
import { HostWindowActionTypes } from '../shared/host-window.actions';
|
||||
import { HeaderCollapseAction } from './header.actions';
|
||||
|
||||
@Injectable()
|
||||
export class HeaderEffects {
|
||||
|
||||
@Effect() resize$ = this.actions$
|
||||
.pipe(
|
||||
ofType(HostWindowActionTypes.RESIZE),
|
||||
map(() => new HeaderCollapseAction())
|
||||
);
|
||||
|
||||
@Effect() routeChange$ = this.actions$
|
||||
.pipe(
|
||||
ofType(fromRouter.ROUTER_NAVIGATION),
|
||||
map(() => new HeaderCollapseAction())
|
||||
);
|
||||
|
||||
constructor(private actions$: Actions) {
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -1,89 +0,0 @@
|
||||
import * as deepFreeze from 'deep-freeze';
|
||||
|
||||
import { headerReducer } from './header.reducer';
|
||||
import {
|
||||
HeaderCollapseAction,
|
||||
HeaderExpandAction,
|
||||
HeaderToggleAction
|
||||
} from './header.actions';
|
||||
|
||||
class NullAction extends HeaderCollapseAction {
|
||||
type = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
describe('headerReducer', () => {
|
||||
it('should return the current state when no valid actions have been made', () => {
|
||||
const state = { navCollapsed: false };
|
||||
const action = new NullAction();
|
||||
const newState = headerReducer(state, action);
|
||||
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should start with navCollapsed = true', () => {
|
||||
const action = new NullAction();
|
||||
const initialState = headerReducer(undefined, action);
|
||||
|
||||
// The navigation starts collapsed
|
||||
expect(initialState.navCollapsed).toEqual(true);
|
||||
});
|
||||
|
||||
it('should set navCollapsed to true in response to the COLLAPSE action', () => {
|
||||
const state = { navCollapsed: false };
|
||||
const action = new HeaderCollapseAction();
|
||||
const newState = headerReducer(state, action);
|
||||
|
||||
expect(newState.navCollapsed).toEqual(true);
|
||||
});
|
||||
|
||||
it('should perform the COLLAPSE action without affecting the previous state', () => {
|
||||
const state = { navCollapsed: false };
|
||||
deepFreeze(state);
|
||||
|
||||
const action = new HeaderCollapseAction();
|
||||
headerReducer(state, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
});
|
||||
|
||||
it('should set navCollapsed to false in response to the EXPAND action', () => {
|
||||
const state = { navCollapsed: true };
|
||||
const action = new HeaderExpandAction();
|
||||
const newState = headerReducer(state, action);
|
||||
|
||||
expect(newState.navCollapsed).toEqual(false);
|
||||
});
|
||||
|
||||
it('should perform the EXPAND action without affecting the previous state', () => {
|
||||
const state = { navCollapsed: true };
|
||||
deepFreeze(state);
|
||||
|
||||
const action = new HeaderExpandAction();
|
||||
headerReducer(state, action);
|
||||
});
|
||||
|
||||
it('should flip the value of navCollapsed in response to the TOGGLE action', () => {
|
||||
const state1 = { navCollapsed: true };
|
||||
const action = new HeaderToggleAction();
|
||||
|
||||
const state2 = headerReducer(state1, action);
|
||||
const state3 = headerReducer(state2, action);
|
||||
|
||||
expect(state2.navCollapsed).toEqual(false);
|
||||
expect(state3.navCollapsed).toEqual(true);
|
||||
});
|
||||
|
||||
it('should perform the TOGGLE action without affecting the previous state', () => {
|
||||
const state = { navCollapsed: true };
|
||||
deepFreeze(state);
|
||||
|
||||
const action = new HeaderToggleAction();
|
||||
headerReducer(state, action);
|
||||
});
|
||||
|
||||
});
|
@@ -1,38 +0,0 @@
|
||||
import { HeaderAction, HeaderActionTypes } from './header.actions';
|
||||
|
||||
export interface HeaderState {
|
||||
navCollapsed: boolean;
|
||||
}
|
||||
|
||||
const initialState: HeaderState = {
|
||||
navCollapsed: true
|
||||
};
|
||||
|
||||
export function headerReducer(state = initialState, action: HeaderAction): HeaderState {
|
||||
switch (action.type) {
|
||||
|
||||
case HeaderActionTypes.COLLAPSE: {
|
||||
return Object.assign({}, state, {
|
||||
navCollapsed: true
|
||||
});
|
||||
}
|
||||
|
||||
case HeaderActionTypes.EXPAND: {
|
||||
return Object.assign({}, state, {
|
||||
navCollapsed: false
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
case HeaderActionTypes.TOGGLE: {
|
||||
return Object.assign({}, state, {
|
||||
navCollapsed: !state.navCollapsed
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
<li class="nav-item dropdown"
|
||||
(mouseenter)="activateSection($event)"
|
||||
(mouseleave)="deactivateSection($event)">
|
||||
<a href="#" class="nav-link dropdown-toggle" routerLinkActive="active"
|
||||
id="browseDropdown" (click)="toggleSection($event)"
|
||||
data-toggle="dropdown">
|
||||
<ng-container
|
||||
*ngComponentOutlet="itemComponents.get(section.id); injector: itemInjectors.get(section.id);"></ng-container>
|
||||
</a>
|
||||
<ul @slide *ngIf="(active | async)" (click)="deactivateSection($event)"
|
||||
class="m-0 shadow-none border-top-0 dropdown-menu show">
|
||||
<ng-container *ngFor="let subSection of (subSections | async)">
|
||||
<ng-container
|
||||
*ngComponentOutlet="itemComponents.get(subSection.id); injector: itemInjectors.get(subSection.id);"></ng-container>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</li>
|
@@ -0,0 +1,27 @@
|
||||
@import '../../../styles/variables.scss';
|
||||
|
||||
.dropdown-menu {
|
||||
overflow: hidden;
|
||||
min-width: 100%;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
::ng-deep a.nav-link {
|
||||
padding-right: $spacer;
|
||||
padding-left: $spacer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/** Mobile menu styling **/
|
||||
@media screen and (max-width: map-get($grid-breakpoints, md)) {
|
||||
.dropdown-toggle {
|
||||
&:after {
|
||||
float: right;
|
||||
margin-top: $spacer/2;
|
||||
}
|
||||
}
|
||||
.dropdown-menu {
|
||||
border: 0;
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,176 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ExpandableNavbarSectionComponent } from './expandable-navbar-section.component';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { MenuServiceStub } from '../../shared/testing/menu-service-stub';
|
||||
import { Component } from '@angular/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { HostWindowService } from '../../shared/host-window.service';
|
||||
import { MenuService } from '../../shared/menu/menu.service';
|
||||
import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
describe('ExpandableNavbarSectionComponent', () => {
|
||||
let component: ExpandableNavbarSectionComponent;
|
||||
let fixture: ComponentFixture<ExpandableNavbarSectionComponent>;
|
||||
const menuService = new MenuServiceStub();
|
||||
|
||||
describe('on larger screens', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule],
|
||||
declarations: [ExpandableNavbarSectionComponent, TestComponent],
|
||||
providers: [
|
||||
{ provide: 'sectionDataProvider', useValue: {} },
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) }
|
||||
]
|
||||
}).overrideComponent(ExpandableNavbarSectionComponent, {
|
||||
set: {
|
||||
entryComponents: [TestComponent]
|
||||
}
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([]));
|
||||
|
||||
fixture = TestBed.createComponent(ExpandableNavbarSectionComponent);
|
||||
component = fixture.componentInstance;
|
||||
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('when the mouse enters the section header', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'activateSection');
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('li.nav-item.dropdown'));
|
||||
sidebarToggler.triggerEventHandler('mouseenter', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should call activateSection on the menuService', () => {
|
||||
expect(menuService.activateSection).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the mouse leaves the section header', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'deactivateSection');
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('li.nav-item.dropdown'));
|
||||
sidebarToggler.triggerEventHandler('mouseleave', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should call deactivateSection on the menuService', () => {
|
||||
expect(menuService.deactivateSection).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a click occurs on the section header', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'toggleActiveSection');
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('li.nav-item.dropdown')).query(By.css('a'));
|
||||
sidebarToggler.triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should not call toggleActiveSection on the menuService', () => {
|
||||
expect(menuService.toggleActiveSection).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('on smaller, mobile screens', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule],
|
||||
declarations: [ExpandableNavbarSectionComponent, TestComponent],
|
||||
providers: [
|
||||
{ provide: 'sectionDataProvider', useValue: {} },
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(300) }
|
||||
]
|
||||
}).overrideComponent(ExpandableNavbarSectionComponent, {
|
||||
set: {
|
||||
entryComponents: [TestComponent]
|
||||
}
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([]));
|
||||
|
||||
fixture = TestBed.createComponent(ExpandableNavbarSectionComponent);
|
||||
component = fixture.componentInstance;
|
||||
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('when the mouse enters the section header', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'activateSection');
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('li.nav-item.dropdown'));
|
||||
sidebarToggler.triggerEventHandler('mouseenter', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should not call activateSection on the menuService', () => {
|
||||
expect(menuService.activateSection).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the mouse leaves the section header', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'deactivateSection');
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('li.nav-item.dropdown'));
|
||||
sidebarToggler.triggerEventHandler('mouseleave', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should not call deactivateSection on the menuService', () => {
|
||||
expect(menuService.deactivateSection).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a click occurs on the section header link', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'toggleActiveSection');
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('li.nav-item.dropdown')).query(By.css('a'));
|
||||
sidebarToggler.triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should call toggleActiveSection on the menuService', () => {
|
||||
expect(menuService.toggleActiveSection).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// declare a test component
|
||||
@Component({
|
||||
selector: 'ds-test-cmp',
|
||||
template: ``
|
||||
})
|
||||
class TestComponent {
|
||||
}
|
@@ -0,0 +1,83 @@
|
||||
import { Component, Inject, Injector, OnInit } from '@angular/core';
|
||||
import { NavbarSectionComponent } from '../navbar-section/navbar-section.component';
|
||||
import { MenuService } from '../../shared/menu/menu.service';
|
||||
import { MenuID } from '../../shared/menu/initial-menus-state';
|
||||
import { slide } from '../../shared/animations/slide';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { HostWindowService } from '../../shared/host-window.service';
|
||||
import { rendersSectionForMenu } from '../../shared/menu/menu-section.decorator';
|
||||
|
||||
/**
|
||||
* Represents an expandable section in the navbar
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-expandable-navbar-section',
|
||||
templateUrl: './expandable-navbar-section.component.html',
|
||||
styleUrls: ['./expandable-navbar-section.component.scss'],
|
||||
animations: [slide]
|
||||
})
|
||||
@rendersSectionForMenu(MenuID.PUBLIC, true)
|
||||
export class ExpandableNavbarSectionComponent extends NavbarSectionComponent implements OnInit {
|
||||
/**
|
||||
* This section resides in the Public Navbar
|
||||
*/
|
||||
menuID = MenuID.PUBLIC;
|
||||
|
||||
constructor(@Inject('sectionDataProvider') menuSection,
|
||||
protected menuService: MenuService,
|
||||
protected injector: Injector,
|
||||
private windowService: HostWindowService
|
||||
) {
|
||||
super(menuSection, menuService, injector);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the super function that activates this section (triggered on hover)
|
||||
* Has an extra check to make sure the section can only be activated on non-mobile devices
|
||||
* @param {Event} event The user event that triggered this function
|
||||
*/
|
||||
activateSection(event): void {
|
||||
this.windowService.isXsOrSm().pipe(
|
||||
first()
|
||||
).subscribe((isMobile) => {
|
||||
if (!isMobile) {
|
||||
super.activateSection(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the super function that deactivates this section (triggered on hover)
|
||||
* Has an extra check to make sure the section can only be deactivated on non-mobile devices
|
||||
* @param {Event} event The user event that triggered this function
|
||||
*/
|
||||
deactivateSection(event): void {
|
||||
this.windowService.isXsOrSm().pipe(
|
||||
first()
|
||||
).subscribe((isMobile) => {
|
||||
if (!isMobile) {
|
||||
super.deactivateSection(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the super function that toggles this section (triggered on click)
|
||||
* Has an extra check to make sure the section can only be toggled on mobile devices
|
||||
* @param {Event} event The user event that triggered this function
|
||||
*/
|
||||
toggleSection(event): void {
|
||||
event.preventDefault();
|
||||
this.windowService.isXsOrSm().pipe(
|
||||
first()
|
||||
).subscribe((isMobile) => {
|
||||
if (isMobile) {
|
||||
super.toggleSection(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
<li class="nav-item">
|
||||
<ng-container
|
||||
*ngComponentOutlet="itemComponents.get(section.id); injector: itemInjectors.get(section.id);"></ng-container>
|
||||
</li>
|
@@ -0,0 +1,53 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NavbarSectionComponent } from './navbar-section.component';
|
||||
import { HostWindowService } from '../../shared/host-window.service';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MenuService } from '../../shared/menu/menu.service';
|
||||
import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub';
|
||||
import { Component } from '@angular/core';
|
||||
import { MenuServiceStub } from '../../shared/testing/menu-service-stub';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
describe('NavbarSectionComponent', () => {
|
||||
let component: NavbarSectionComponent;
|
||||
let fixture: ComponentFixture<NavbarSectionComponent>;
|
||||
const menuService = new MenuServiceStub();
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule],
|
||||
declarations: [NavbarSectionComponent, TestComponent],
|
||||
providers: [
|
||||
{ provide: 'sectionDataProvider', useValue: {} },
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) }
|
||||
]
|
||||
}).overrideComponent(NavbarSectionComponent, {
|
||||
set: {
|
||||
entryComponents: [TestComponent]
|
||||
}
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([]));
|
||||
|
||||
fixture = TestBed.createComponent(NavbarSectionComponent);
|
||||
component = fixture.componentInstance;
|
||||
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
// declare a test component
|
||||
@Component({
|
||||
selector: 'ds-test-cmp',
|
||||
template: ``
|
||||
})
|
||||
class TestComponent {
|
||||
}
|
32
src/app/navbar/navbar-section/navbar-section.component.ts
Normal file
32
src/app/navbar/navbar-section/navbar-section.component.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Component, Inject, Injector, OnInit } from '@angular/core';
|
||||
import { MenuSectionComponent } from '../../shared/menu/menu-section/menu-section.component';
|
||||
import { MenuService } from '../../shared/menu/menu.service';
|
||||
import { MenuID } from '../../shared/menu/initial-menus-state';
|
||||
import { rendersSectionForMenu } from '../../shared/menu/menu-section.decorator';
|
||||
|
||||
/**
|
||||
* Represents a non-expandable section in the navbar
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-navbar-section',
|
||||
templateUrl: './navbar-section.component.html',
|
||||
styleUrls: ['./navbar-section.component.scss']
|
||||
})
|
||||
@rendersSectionForMenu(MenuID.PUBLIC, false)
|
||||
export class NavbarSectionComponent extends MenuSectionComponent implements OnInit {
|
||||
/**
|
||||
* This section resides in the Public Navbar
|
||||
*/
|
||||
menuID = MenuID.PUBLIC;
|
||||
|
||||
constructor(@Inject('sectionDataProvider') menuSection,
|
||||
protected menuService: MenuService,
|
||||
protected injector: Injector
|
||||
) {
|
||||
super(menuSection, menuService, injector);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
}
|
||||
}
|
17
src/app/navbar/navbar.component.html
Normal file
17
src/app/navbar/navbar.component.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<nav [ngClass]="{'open': !(menuCollapsed | async)}"
|
||||
[@slideMobileNav]="!(windowService.isXsOrSm() | async) ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')"
|
||||
class="navbar navbar-light navbar-expand-md p-md-0 navbar-container"> <!-- TODO remove navbar-container class when https://github.com/twbs/bootstrap/issues/24726 is fixed -->
|
||||
<div class="container">
|
||||
<div class="reset-padding-md w-100">
|
||||
<div id="collapsingNav">
|
||||
<ul class="navbar-nav mr-auto shadow-none">
|
||||
<ng-container *ngFor="let section of (sections | async)">
|
||||
<ng-container
|
||||
*ngComponentOutlet="sectionComponents.get(section.id); injector: sectionInjectors.get(section.id);"></ng-container>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
39
src/app/navbar/navbar.component.scss
Normal file
39
src/app/navbar/navbar.component.scss
Normal file
@@ -0,0 +1,39 @@
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
nav.navbar {
|
||||
border-bottom: 1px $gray-400 solid;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
/** Mobile menu styling **/
|
||||
@media screen and (max-width: map-get($grid-breakpoints, md)) {
|
||||
.navbar {
|
||||
width: 100%;
|
||||
background-color: $white;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
height: 0;
|
||||
&.open {
|
||||
height: 100vh; //doesn't matter because wrapper is sticky
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: map-get($grid-breakpoints, md)) {
|
||||
.reset-padding-md {
|
||||
margin-left: -$spacer/2;
|
||||
margin-right: -$spacer/2;
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO remove when https://github.com/twbs/bootstrap/issues/24726 is fixed */
|
||||
.navbar-expand-md.navbar-container {
|
||||
@media screen and (max-width: map-get($grid-breakpoints, md)) {
|
||||
> .container {
|
||||
padding: 0 $spacer;
|
||||
}
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
52
src/app/navbar/navbar.component.spec.ts
Normal file
52
src/app/navbar/navbar.component.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { NavbarComponent } from './navbar.component';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { HostWindowServiceStub } from '../shared/testing/host-window-service-stub';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { Injector, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { MenuService } from '../shared/menu/menu.service';
|
||||
import { MenuServiceStub } from '../shared/testing/menu-service-stub';
|
||||
|
||||
let comp: NavbarComponent;
|
||||
let fixture: ComponentFixture<NavbarComponent>;
|
||||
|
||||
describe('NavbarComponent', () => {
|
||||
const menuService = new MenuServiceStub();
|
||||
|
||||
// async beforeEach
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
NoopAnimationsModule,
|
||||
ReactiveFormsModule],
|
||||
declarations: [NavbarComponent],
|
||||
providers: [
|
||||
{ provide: Injector, useValue: {} },
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents(); // compile template and css
|
||||
}));
|
||||
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'getMenuTopSections').and.returnValue(observableOf([]));
|
||||
|
||||
fixture = TestBed.createComponent(NavbarComponent);
|
||||
|
||||
comp = fixture.componentInstance;
|
||||
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(comp).toBeTruthy();
|
||||
});
|
||||
});
|
115
src/app/navbar/navbar.component.ts
Normal file
115
src/app/navbar/navbar.component.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { Component, Injector, OnInit } from '@angular/core';
|
||||
import { slideMobileNav } from '../shared/animations/slide';
|
||||
import { MenuComponent } from '../shared/menu/menu.component';
|
||||
import { MenuService } from '../shared/menu/menu.service';
|
||||
import { MenuID, MenuItemType } from '../shared/menu/initial-menus-state';
|
||||
import { TextMenuItemModel } from '../shared/menu/menu-item/models/text.model';
|
||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
|
||||
/**
|
||||
* Component representing the public navbar
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-navbar',
|
||||
styleUrls: ['navbar.component.scss'],
|
||||
templateUrl: 'navbar.component.html',
|
||||
animations: [slideMobileNav]
|
||||
})
|
||||
export class NavbarComponent extends MenuComponent implements OnInit {
|
||||
/**
|
||||
* The menu ID of the Navbar is PUBLIC
|
||||
* @type {MenuID.PUBLIC}
|
||||
*/
|
||||
menuID = MenuID.PUBLIC;
|
||||
|
||||
constructor(protected menuService: MenuService,
|
||||
protected injector: Injector,
|
||||
public windowService: HostWindowService
|
||||
) {
|
||||
super(menuService, injector);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.createMenu();
|
||||
super.ngOnInit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all menu sections and items for this menu
|
||||
*/
|
||||
createMenu() {
|
||||
const menuList = [
|
||||
/* News */
|
||||
{
|
||||
id: 'browse_global',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.browse_global'
|
||||
} as TextMenuItemModel,
|
||||
index: 0
|
||||
},
|
||||
{
|
||||
id: 'browse_global_communities_and_collections',
|
||||
parentID: 'browse_global',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.browse_global_communities_and_collections',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'browse_global_global_by_issue_date',
|
||||
parentID: 'browse_global',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.browse_global_by_issue_date',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
}, {
|
||||
id: 'browse_global_global_by_title',
|
||||
parentID: 'browse_global',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.browse_global_by_title',
|
||||
link: '/browse/title'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'browse_global_by_author',
|
||||
parentID: 'browse_global',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.browse_global_by_author',
|
||||
link: '/browse/author'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
|
||||
/* Statistics */
|
||||
{
|
||||
id: 'statistics',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.statistics',
|
||||
link: '#'
|
||||
} as LinkMenuItemModel,
|
||||
index: 2
|
||||
},
|
||||
];
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -1,26 +1,31 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { HeaderEffects } from './header.effects';
|
||||
import { HeaderCollapseAction } from './header.actions';
|
||||
import { NavbarEffects } from './navbar.effects';
|
||||
import { HostWindowResizeAction } from '../shared/host-window.actions';
|
||||
import { Observable } from 'rxjs';
|
||||
import { provideMockActions } from '@ngrx/effects/testing';
|
||||
import { cold, hot } from 'jasmine-marbles';
|
||||
import * as fromRouter from '@ngrx/router-store';
|
||||
import { CollapseMenuAction } from '../shared/menu/menu.actions';
|
||||
import { MenuID } from '../shared/menu/initial-menus-state';
|
||||
import { MenuService } from '../shared/menu/menu.service';
|
||||
import { MenuServiceStub } from '../shared/testing/menu-service-stub';
|
||||
|
||||
describe('HeaderEffects', () => {
|
||||
let headerEffects: HeaderEffects;
|
||||
describe('NavbarEffects', () => {
|
||||
let navbarEffects: NavbarEffects;
|
||||
let actions: Observable<any>;
|
||||
const menuService = new MenuServiceStub();
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
HeaderEffects,
|
||||
NavbarEffects,
|
||||
provideMockActions(() => actions),
|
||||
{ provide: MenuService, useValue: menuService },
|
||||
// other providers
|
||||
],
|
||||
});
|
||||
|
||||
headerEffects = TestBed.get(HeaderEffects);
|
||||
navbarEffects = TestBed.get(NavbarEffects);
|
||||
});
|
||||
|
||||
describe('resize$', () => {
|
||||
@@ -28,9 +33,9 @@ describe('HeaderEffects', () => {
|
||||
it('should return a COLLAPSE action in response to a RESIZE action', () => {
|
||||
actions = hot('--a-', { a: new HostWindowResizeAction(800, 600) });
|
||||
|
||||
const expected = cold('--b-', { b: new HeaderCollapseAction() });
|
||||
const expected = cold('--b-', { b: new CollapseMenuAction(MenuID.PUBLIC) });
|
||||
|
||||
expect(headerEffects.resize$).toBeObservable(expected);
|
||||
expect(navbarEffects.resize$).toBeObservable(expected);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -40,9 +45,9 @@ describe('HeaderEffects', () => {
|
||||
it('should return a COLLAPSE action in response to an UPDATE_LOCATION action', () => {
|
||||
actions = hot('--a-', { a: { type: fromRouter.ROUTER_NAVIGATION } });
|
||||
|
||||
const expected = cold('--b-', { b: new HeaderCollapseAction() });
|
||||
const expected = cold('--b-', { b: new CollapseMenuAction(MenuID.PUBLIC) });
|
||||
|
||||
expect(headerEffects.routeChange$).toBeObservable(expected);
|
||||
expect(navbarEffects.routeChange$).toBeObservable(expected);
|
||||
});
|
||||
|
||||
});
|
62
src/app/navbar/navbar.effects.ts
Normal file
62
src/app/navbar/navbar.effects.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { first, map, switchMap } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Actions, Effect, ofType } from '@ngrx/effects'
|
||||
import * as fromRouter from '@ngrx/router-store';
|
||||
|
||||
import { HostWindowActionTypes } from '../shared/host-window.actions';
|
||||
import {
|
||||
CollapseMenuAction,
|
||||
ExpandMenuPreviewAction,
|
||||
MenuActionTypes
|
||||
} from '../shared/menu/menu.actions';
|
||||
import { MenuID } from '../shared/menu/initial-menus-state';
|
||||
import { MenuService } from '../shared/menu/menu.service';
|
||||
import { MenuState } from '../shared/menu/menu.reducer';
|
||||
|
||||
@Injectable()
|
||||
export class NavbarEffects {
|
||||
menuID = MenuID.PUBLIC;
|
||||
/**
|
||||
* Effect that collapses the public menu on window resize
|
||||
* @type {Observable<CollapseMenuAction>}
|
||||
*/
|
||||
@Effect() resize$ = this.actions$
|
||||
.pipe(
|
||||
ofType(HostWindowActionTypes.RESIZE),
|
||||
map(() => new CollapseMenuAction(this.menuID))
|
||||
);
|
||||
|
||||
/**
|
||||
* Effect that collapses the public menu on reroute
|
||||
* @type {Observable<CollapseMenuAction>}
|
||||
*/
|
||||
@Effect() routeChange$ = this.actions$
|
||||
.pipe(
|
||||
ofType(fromRouter.ROUTER_NAVIGATION),
|
||||
map(() => new CollapseMenuAction(this.menuID))
|
||||
);
|
||||
/**
|
||||
* Effect that collapses the public menu when the admin sidebar opens
|
||||
* @type {Observable<CollapseMenuAction>}
|
||||
*/
|
||||
@Effect() openAdminSidebar$ = this.actions$
|
||||
.pipe(
|
||||
ofType(MenuActionTypes.EXPAND_MENU_PREVIEW),
|
||||
switchMap((action: ExpandMenuPreviewAction) => {
|
||||
return this.menuService.getMenu(action.menuID).pipe(
|
||||
first(),
|
||||
map((menu: MenuState) => {
|
||||
if (menu.id === MenuID.ADMIN) {
|
||||
if (!menu.previewCollapsed && menu.collapsed) {
|
||||
return new CollapseMenuAction(MenuID.PUBLIC)
|
||||
}
|
||||
}
|
||||
return { type: 'NO_ACTION' };
|
||||
}));
|
||||
})
|
||||
);
|
||||
constructor(private actions$: Actions, private menuService: MenuService) {
|
||||
|
||||
}
|
||||
|
||||
}
|
45
src/app/navbar/navbar.module.ts
Normal file
45
src/app/navbar/navbar.module.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { CoreModule } from '../core/core.module';
|
||||
import { NavbarEffects } from './navbar.effects';
|
||||
import { NavbarSectionComponent } from './navbar-section/navbar-section.component';
|
||||
import { ExpandableNavbarSectionComponent } from './expandable-navbar-section/expandable-navbar-section.component';
|
||||
import { NavbarComponent } from './navbar.component';
|
||||
|
||||
const effects = [
|
||||
NavbarEffects
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
EffectsModule.forFeature(effects),
|
||||
CoreModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
NavbarComponent,
|
||||
NavbarSectionComponent,
|
||||
ExpandableNavbarSectionComponent
|
||||
],
|
||||
providers: [
|
||||
|
||||
],
|
||||
entryComponents: [
|
||||
NavbarSectionComponent,
|
||||
ExpandableNavbarSectionComponent
|
||||
],
|
||||
exports: [
|
||||
NavbarComponent,
|
||||
NavbarSectionComponent,
|
||||
ExpandableNavbarSectionComponent
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
* This module handles all components and pipes that are necessary for the horizontal navigation bar
|
||||
*/
|
||||
export class NavbarModule {
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
<div class="page-not-found">
|
||||
<div class="page-not-found container">
|
||||
<h1>404</h1>
|
||||
<h2><small>{{"404.page-not-found" | translate}}</small></h2>
|
||||
<br/>
|
||||
|
26
src/app/shared/animations/bgColor.ts
Normal file
26
src/app/shared/animations/bgColor.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
animate,
|
||||
animateChild,
|
||||
group, query,
|
||||
state,
|
||||
style,
|
||||
transition,
|
||||
trigger
|
||||
} from '@angular/animations';
|
||||
|
||||
const startStyle = style({ backgroundColor: '{{ startColor }}' });
|
||||
const endStyle = style({ backgroundColor: '{{ endColor }}' });
|
||||
|
||||
export const bgColor = trigger('bgColor',
|
||||
[
|
||||
state('startBackground', startStyle, { params: { startColor: '*' } }),
|
||||
state('endBackground', endStyle, { params: { endColor: '*' } }),
|
||||
transition('startBackground <=> endBackground',
|
||||
group(
|
||||
[
|
||||
query('@*', animateChild()),
|
||||
animate('200ms'),
|
||||
|
||||
]
|
||||
))
|
||||
]);
|
@@ -2,18 +2,18 @@ import { animate, state, transition, trigger, style } from '@angular/animations'
|
||||
|
||||
export const focusShadow = trigger('focusShadow', [
|
||||
|
||||
state('focus', style({ 'box-shadow': 'rgba(119, 119, 119, 0.6) 0px 0px 6px' })),
|
||||
state('focus', style({ boxShadow: 'rgba(119, 119, 119, 0.6) 0px 0px 6px' })),
|
||||
|
||||
state('blur', style({ 'box-shadow': 'none' })),
|
||||
state('blur', style({ boxShadow: 'none' })),
|
||||
|
||||
transition('focus <=> blur', animate(250))
|
||||
transition('focus <=> blur', [animate('250ms')])
|
||||
]);
|
||||
|
||||
export const focusBackground = trigger('focusBackground', [
|
||||
|
||||
state('focus', style({ 'background-color': 'rgba(119, 119, 119, 0.1)' })),
|
||||
state('focus', style({ backgroundColor: 'rgba(119, 119, 119, 0.1)' })),
|
||||
|
||||
state('blur', style({ 'background-color': 'transparent' })),
|
||||
state('blur', style({ backgroundColor: 'transparent' })),
|
||||
|
||||
transition('focus <=> blur', animate(250))
|
||||
transition('focus <=> blur', [animate('250ms')])
|
||||
]);
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
|
||||
export const rotateInState = state('rotateIn', style({opacity: 1, transform: 'rotate(0deg)'}));
|
||||
export const rotateEnter = transition('* => rotateIn', [
|
||||
style({opacity: 0, transform: 'rotate(5deg)'}),
|
||||
export const rotateInState = state('rotateIn', style({ opacity: 1, transform: 'rotate(0deg)' }));
|
||||
export const rotateEnter = transition('* => rotateIn', [
|
||||
style({ opacity: 0, transform: 'rotate(5deg)' }),
|
||||
animate('400ms ease-in-out')
|
||||
]);
|
||||
|
||||
export const rotateOutState = state('rotateOut', style({opacity: 0, transform: 'rotate(5deg)'}));
|
||||
export const rotateOutState = state('rotateOut', style({ opacity: 0, transform: 'rotate(5deg)' }));
|
||||
export const rotateLeave = transition('rotateIn => rotateOut', [
|
||||
style({opacity: 1, transform: 'rotate(0deg)'}),
|
||||
style({ opacity: 1, transform: 'rotate(0deg)' }),
|
||||
animate('400ms ease-in-out')
|
||||
]);
|
||||
|
||||
@@ -24,3 +24,16 @@ export const rotateInOut = trigger('rotateInOut', [
|
||||
rotateEnter,
|
||||
rotateLeave
|
||||
]);
|
||||
|
||||
const expandedStyle = { transform: 'rotate(90deg)' };
|
||||
const collapsedStyle = { transform: 'rotate(0deg)' };
|
||||
|
||||
export const rotate = trigger('rotate',
|
||||
[
|
||||
state('expanded', style(expandedStyle)),
|
||||
state('collapsed', style(collapsedStyle)),
|
||||
transition('expanded <=> collapsed', [
|
||||
animate('200ms')
|
||||
])
|
||||
|
||||
]);
|
||||
|
@@ -1,10 +1,72 @@
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
import {
|
||||
animate,
|
||||
animateChild,
|
||||
group,
|
||||
query,
|
||||
state,
|
||||
style,
|
||||
transition,
|
||||
trigger
|
||||
} from '@angular/animations';
|
||||
|
||||
export const slide = trigger('slide', [
|
||||
|
||||
state('expanded', style({ height: '*' })),
|
||||
state('collapsed', style({ height: 0 })),
|
||||
state('void', style({ height: 0 })),
|
||||
state('*', style({ height: '*' })),
|
||||
transition(':enter', [animate('200ms')]),
|
||||
transition(':leave', [animate('200ms')]),
|
||||
transition('expanded <=> collapsed', animate(250))
|
||||
]);
|
||||
|
||||
export const slideHorizontal = trigger('slideHorizontal', [
|
||||
state('void', style({ width: 0 })),
|
||||
state('*', style({ width: '*' })),
|
||||
transition(':enter', [animate('200ms')]),
|
||||
transition(':leave', [animate('200ms')])
|
||||
]);
|
||||
|
||||
export const slideMobileNav = trigger('slideMobileNav', [
|
||||
|
||||
state('expanded', style({ height: '100vh' })),
|
||||
|
||||
state('collapsed', style({ height: 0 })),
|
||||
|
||||
transition('expanded <=> collapsed', animate(250))
|
||||
transition('expanded <=> collapsed', animate('300ms'))
|
||||
]);
|
||||
|
||||
const collapsedStyle = style({ marginLeft: '-{{ sidebarWidth }}' });
|
||||
const expandedStyle = style({ marginLeft: '0' });
|
||||
const options = { params: { sidebarWidth: '*' } };
|
||||
|
||||
export const slideSidebar = trigger('slideSidebar', [
|
||||
|
||||
transition('expanded => collapsed',
|
||||
group
|
||||
(
|
||||
[
|
||||
query('@*', animateChild()),
|
||||
query('.sidebar-collapsible', expandedStyle, options),
|
||||
query('.sidebar-collapsible', animate('300ms ease-in-out', collapsedStyle))
|
||||
],
|
||||
)),
|
||||
|
||||
transition('collapsed => expanded',
|
||||
group
|
||||
(
|
||||
[
|
||||
query('@*', animateChild()),
|
||||
query('.sidebar-collapsible', collapsedStyle),
|
||||
query('.sidebar-collapsible', animate('300ms ease-in-out', expandedStyle), options)
|
||||
]
|
||||
))
|
||||
]);
|
||||
|
||||
export const slideSidebarPadding = trigger('slideSidebarPadding', [
|
||||
state('hidden', style({ paddingLeft: 0 })),
|
||||
state('shown', style({ paddingLeft: '{{ collapsedSidebarWidth }}' }), { params: { collapsedSidebarWidth: '*' } }),
|
||||
state('expanded', style({ paddingLeft: '{{ totalSidebarWidth }}' }), { params: { totalSidebarWidth: '*' } }),
|
||||
transition('hidden <=> shown', [animate('200ms')]),
|
||||
transition('hidden <=> expanded', [animate('200ms')]),
|
||||
transition('shown <=> expanded', [animate('200ms')]),
|
||||
]);
|
||||
|
@@ -1,25 +1,26 @@
|
||||
<ul class="navbar-nav" [ngClass]="{'mr-auto': (isXsOrSm$ | async)}">
|
||||
<li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item dropdown" (click)="$event.stopPropagation();">
|
||||
<div ngbDropdown placement="bottom-right" class="d-inline-block float-right" @fadeInOut>
|
||||
<a href="#" id="dropdownLogin" class="nav-link" (click)="$event.preventDefault()" ngbDropdownToggle><i class="fa fa-sign-in fa-fw" aria-hidden="true"></i> {{ 'nav.login' | translate }}<span class="caret"></span></a>
|
||||
<li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item" (click)="$event.stopPropagation();">
|
||||
<div ngbDropdown placement="bottom-right" class="d-inline-block" @fadeInOut>
|
||||
<a href="#" id="dropdownLogin" (click)="$event.preventDefault()" ngbDropdownToggle class="px-1">{{ 'nav.login' | translate }}</a>
|
||||
<div id="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu aria-labelledby="dropdownLogin">
|
||||
<ds-log-in></ds-log-in>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li *ngIf="!(isAuthenticated | async) && (isXsOrSm$ | async)" class="nav-item">
|
||||
<a id="loginLink" class="nav-link" routerLink="/login" routerLinkActive="active"><i class="fa fa-sign-in fa-fw" aria-hidden="true"></i> {{ 'nav.login' | translate }}<span class="sr-only">(current)</span></a>
|
||||
<a id="loginLink" routerLink="/login" routerLinkActive="active" class="px-1" >{{ 'nav.login' | translate }}<span class="sr-only">(current)</span></a>
|
||||
</li>
|
||||
<li *ngIf="(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item">
|
||||
<div ngbDropdown placement="bottom-right" class="d-inline-block" [ngClass]="{'float-right': !(isXsOrSm$ | async)}" @fadeInOut>
|
||||
<a href="#" id="dropdownUser" class="nav-link" (click)="$event.preventDefault()" ngbDropdownToggle><i class="fa fa-user fa-fw" aria-hidden="true"></i>Hello {{(user | async).name}}<span class="caret"></span></a>
|
||||
<div id="logoutDropdownMenu" ngbDropdownMenu aria-labelledby="dropdownUser">
|
||||
<ds-log-out></ds-log-out>
|
||||
</div>
|
||||
<div ngbDropdown placement="bottom-right" class="d-inline-block" @fadeInOut>
|
||||
<a href="#" id="dropdownUser" (click)="$event.preventDefault()" class="px-1" ngbDropdownToggle><i class="fas fa-user-circle fa-lg fa-fw" [title]="'nav.logout' | translate"></i></a>
|
||||
<ul id="logoutDropdownMenu" ngbDropdownMenu aria-labelledby="dropdownUser">
|
||||
<li class="dropdown-item">{{(user | async).name}}</li>
|
||||
<li class="dropdown-item"><ds-log-out></ds-log-out></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li *ngIf="(isAuthenticated | async) && (isXsOrSm$ | async)" class="nav-item">
|
||||
<a id="logoutLink" class="nav-link" routerLink="/logout" routerLinkActive="active"><i class="fa fa-sign-out fa-fw" aria-hidden="true"></i> {{ 'nav.logout' | translate }}<span class="sr-only">(current)</span></a>
|
||||
<a id="logoutLink" routerLink="/logout" routerLinkActive="active" class="px-1"><i class="fas fa-user-circle fa-lg fa-fw" [title]="'nav.logout' | translate"></i><span class="sr-only">(current)</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
@@ -229,7 +229,7 @@ describe('AuthNavMenuComponent', () => {
|
||||
component = null;
|
||||
});
|
||||
it('should render logout dropdown menu', () => {
|
||||
const logoutDropdownMenu = deNavMenuItem.query(By.css('div[id=logoutDropdownMenu]'));
|
||||
const logoutDropdownMenu = deNavMenuItem.query(By.css('ul[id=logoutDropdownMenu]'));
|
||||
expect(logoutDropdownMenu.nativeElement).toBeDefined();
|
||||
});
|
||||
})
|
||||
|
@@ -15,7 +15,7 @@
|
||||
[ngbTooltip]="tipContent"
|
||||
triggers="manual"
|
||||
#t="ngbTooltip"
|
||||
class="fa {{icon.style}}"
|
||||
class="fas {{icon.style}}"
|
||||
[class.mr-1]="!l"
|
||||
[class.mr-2]="l"
|
||||
aria-hidden="true"
|
||||
@@ -23,7 +23,7 @@
|
||||
(mouseover)="showTooltip(t, i, icon.metadata)"
|
||||
(mouseout)="t.close()"></i>
|
||||
</ng-container>
|
||||
<p class="chip-label text-truncate d-table-cell">{{c.display}}</p><i class="fa fa-times ml-2" (click)="removeChips($event, i)"></i>
|
||||
<p class="chip-label text-truncate d-table-cell">{{c.display}}</p><i class="fas fa-times ml-2" (click)="removeChips($event, i)" [title]="'chips.remove' | translate"></i>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
@@ -10,6 +10,7 @@ import { SortablejsModule } from 'angular-sortablejs';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model';
|
||||
import { createTestComponent, hasClass } from '../testing/utils';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
describe('ChipsComponent test suite', () => {
|
||||
|
||||
@@ -27,6 +28,7 @@ describe('ChipsComponent test suite', () => {
|
||||
imports: [
|
||||
NgbModule.forRoot(),
|
||||
SortablejsModule.forRoot({animation: 150}),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
ChipsComponent,
|
||||
@@ -148,7 +150,7 @@ describe('ChipsComponent test suite', () => {
|
||||
name: 'mainField',
|
||||
config: {
|
||||
withAuthority:{
|
||||
style: 'fa-user'
|
||||
style: 'fas-user'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -156,10 +158,10 @@ describe('ChipsComponent test suite', () => {
|
||||
name: 'relatedField',
|
||||
config: {
|
||||
withAuthority:{
|
||||
style: 'fa-user-alt'
|
||||
style: 'fas-user-alt'
|
||||
},
|
||||
withoutAuthority:{
|
||||
style: 'fa-user-alt text-muted'
|
||||
style: 'fas-user-alt text-muted'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -167,10 +169,10 @@ describe('ChipsComponent test suite', () => {
|
||||
name: 'otherRelatedField',
|
||||
config: {
|
||||
withAuthority:{
|
||||
style: 'fa-user-alt'
|
||||
style: 'fas-user-alt'
|
||||
},
|
||||
withoutAuthority:{
|
||||
style: 'fa-user-alt text-muted'
|
||||
style: 'fas-user-alt text-muted'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -190,7 +192,7 @@ describe('ChipsComponent test suite', () => {
|
||||
|
||||
it('should show icon for every field that has a configured icon', () => {
|
||||
const de = chipsFixture.debugElement.query(By.css('li.nav-item'));
|
||||
const icons = de.queryAll(By.css('i.fa'));
|
||||
const icons = de.queryAll(By.css('i.fas'));
|
||||
|
||||
expect(icons.length).toBe(4);
|
||||
|
||||
@@ -198,14 +200,14 @@ describe('ChipsComponent test suite', () => {
|
||||
|
||||
it('should has text-muted on icon style when field value had not authority', () => {
|
||||
const de = chipsFixture.debugElement.query(By.css('li.nav-item'));
|
||||
const icons = de.queryAll(By.css('i.fa'));
|
||||
const icons = de.queryAll(By.css('i.fas'));
|
||||
|
||||
expect(hasClass(icons[2].nativeElement, 'text-muted')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should show tooltip on mouse over an icon', () => {
|
||||
const de = chipsFixture.debugElement.query(By.css('li.nav-item'));
|
||||
const icons = de.queryAll(By.css('i.fa'));
|
||||
const icons = de.queryAll(By.css('i.fas'));
|
||||
|
||||
icons[0].triggerEventHandler('mouseover', null);
|
||||
|
||||
|
@@ -34,14 +34,14 @@ describe('ChipsItem model test suite', () => {
|
||||
});
|
||||
|
||||
it('should update icons', () => {
|
||||
const icons: ChipsItemIcon[] = [{metadata: 'test', hasAuthority: false, style: 'fa fa-plus'}];
|
||||
const icons: ChipsItemIcon[] = [{metadata: 'test', hasAuthority: false, style: 'fas fa-plus'}];
|
||||
item.updateIcons(icons);
|
||||
|
||||
expect(item.icons).toEqual(icons);
|
||||
});
|
||||
|
||||
it('should return true if has icons', () => {
|
||||
const icons: ChipsItemIcon[] = [{metadata: 'test', hasAuthority: false, style: 'fa fa-plus'}];
|
||||
const icons: ChipsItemIcon[] = [{metadata: 'test', hasAuthority: false, style: 'fas fa-plus'}];
|
||||
item.updateIcons(icons);
|
||||
const hasIcons = item.hasIcons();
|
||||
|
||||
|
@@ -1,3 +1,3 @@
|
||||
<div *ngIf="logo" class="dso-logo">
|
||||
<div *ngIf="logo" class="dso-logo mb-3">
|
||||
<img [src]="logo.content" class="img-fluid" [attr.alt]="alternateText ? alternateText : null" (error)="errorHandler($event)"/>
|
||||
</div>
|
||||
|
@@ -27,7 +27,7 @@ export const DATE_TEST_MODEL_CONFIG = {
|
||||
placeholder: 'Date',
|
||||
readOnly: false,
|
||||
required: true,
|
||||
toggleIcon: 'fa fa-calendar'
|
||||
toggleIcon: 'fas fa-calendar'
|
||||
};
|
||||
|
||||
describe('DsDatePickerComponent test suite', () => {
|
||||
|
@@ -2,7 +2,7 @@
|
||||
class="close position-relative"
|
||||
ngbTooltip="{{'form.group-collapse-help' | translate}}"
|
||||
placement="left">
|
||||
<span class="fa fa-angle-up fa-fw fa-2x"
|
||||
<span class="fas fa-angle-up fa-fw fa-2x"
|
||||
aria-hidden="true"
|
||||
(click)="collapseForm()"></span>
|
||||
</a>
|
||||
@@ -10,7 +10,7 @@
|
||||
class="close position-relative"
|
||||
ngbTooltip="{{'form.group-expand-help' | translate}}"
|
||||
placement="left">
|
||||
<span class="fa fa-angle-down fa-fw fa-2x"
|
||||
<span class="fas fa-angle-down fa-fw fa-2x"
|
||||
aria-hidden="true"
|
||||
(click)="expandForm()"></span>
|
||||
</a>
|
||||
@@ -33,21 +33,21 @@
|
||||
class="btn btn-link"
|
||||
[disabled]="isMandatoryFieldEmpty()"
|
||||
(click)="save()">
|
||||
<i class="fa fa-save text-primary fa-2x"
|
||||
<i class="fas fa-save text-primary fa-2x"
|
||||
aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-link"
|
||||
[disabled]="!editMode"
|
||||
(click)="delete()">
|
||||
<i class="fa fa-trash text-danger fa-2x"
|
||||
<i class="fas fa-trash text-danger fa-2x"
|
||||
aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-link"
|
||||
[disabled]="isMandatoryFieldEmpty()"
|
||||
(click)="clear()">
|
||||
<i class="fa fa-undo fa-2x"
|
||||
<i class="fas fa-undo fa-2x"
|
||||
aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
|
@@ -41,6 +41,6 @@
|
||||
(keypress)="preventEventsPropagation($event)"
|
||||
(keydown)="preventEventsPropagation($event)"
|
||||
(keyup)="onKeyUp($event)"/>
|
||||
<i *ngIf="searching" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw text-primary position-absolute mt-1 p-0" aria-hidden="true"></i>
|
||||
<i *ngIf="searching" class="fas fa-circle-o-notch fa-spin fa-2x fa-fw text-primary position-absolute mt-1 p-0" aria-hidden="true"></i>
|
||||
</ds-chips>
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
{{ r.display}}
|
||||
</ng-template>
|
||||
<div class="position-relative right-addon">
|
||||
<i *ngIf="searching" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw text-primary position-absolute mt-1 p-0" aria-hidden="true"></i>
|
||||
<i *ngIf="searching" class="fas fa-circle-o-notch fa-spin fa-2x fa-fw text-primary position-absolute mt-1 p-0" aria-hidden="true"></i>
|
||||
<input class="form-control"
|
||||
[attr.autoComplete]="model.autoComplete"
|
||||
[class.is-invalid]="showErrorMessages"
|
||||
|
@@ -11,7 +11,7 @@ export class DateFieldParser extends FieldParser {
|
||||
let malformedDate = false;
|
||||
const inputDateModelConfig: DynamicDatePickerModelConfig = this.initModel(null, label);
|
||||
|
||||
inputDateModelConfig.toggleIcon = 'fa fa-calendar';
|
||||
inputDateModelConfig.toggleIcon = 'fas fa-calendar';
|
||||
this.setValues(inputDateModelConfig as any, fieldValue);
|
||||
// Init Data and validity check
|
||||
if (isNotEmpty(inputDateModelConfig.value)) {
|
||||
|
@@ -20,12 +20,12 @@
|
||||
<button type="button" class="btn btn-secondary"
|
||||
[disabled]="isItemReadOnly(context, index)"
|
||||
(click)="insertItem($event, group.context, group.index + 1)">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary"
|
||||
(click)="removeItem($event, context, index)"
|
||||
[disabled]="group.context.groups.length === 1 || isItemReadOnly(context, index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,7 +37,7 @@
|
||||
<button type="button" class="btn btn-secondary"
|
||||
(click)="removeItem($event, context, index)"
|
||||
[disabled]="group.context.groups.length === 1 || isItemReadOnly(context, index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -3,17 +3,23 @@ import { cold, hot } from 'jasmine-marbles';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { AppState } from '../app.reducer';
|
||||
|
||||
import { GridBreakpoint, HostWindowService, WidthCategory } from './host-window.service';
|
||||
|
||||
import { HostWindowService, WidthCategory } from './host-window.service';
|
||||
import { CSSVariableServiceStub } from './testing/css-variable-service-stub';
|
||||
describe('HostWindowService', () => {
|
||||
let service: HostWindowService;
|
||||
let store: Store<AppState>;
|
||||
enum GridBreakpoint {
|
||||
SM_MIN = 576,
|
||||
MD_MIN = 768,
|
||||
LG_MIN = 992,
|
||||
XL_MIN = 1200
|
||||
};
|
||||
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
const _initialState = { hostWindow: { width: 1600, height: 770 } };
|
||||
store = new Store<AppState>(observableOf(_initialState), undefined, undefined);
|
||||
service = new HostWindowService(store);
|
||||
service = new HostWindowService(store, new CSSVariableServiceStub() as any);
|
||||
});
|
||||
|
||||
it('isXs() should return false with width = 1600', () => {
|
||||
@@ -49,7 +55,7 @@ describe('HostWindowService', () => {
|
||||
beforeEach(() => {
|
||||
const _initialState = { hostWindow: { width: 1100, height: 770 } };
|
||||
store = new Store<AppState>(observableOf(_initialState), undefined, undefined);
|
||||
service = new HostWindowService(store);
|
||||
service = new HostWindowService(store, new CSSVariableServiceStub() as any);
|
||||
});
|
||||
|
||||
it('isXs() should return false with width = 1100', () => {
|
||||
@@ -85,7 +91,7 @@ describe('HostWindowService', () => {
|
||||
beforeEach(() => {
|
||||
const _initialState = { hostWindow: { width: 800, height: 770 } };
|
||||
store = new Store<AppState>(observableOf(_initialState), undefined, undefined);
|
||||
service = new HostWindowService(store);
|
||||
service = new HostWindowService(store, new CSSVariableServiceStub() as any);
|
||||
});
|
||||
|
||||
it('isXs() should return false with width = 800', () => {
|
||||
@@ -121,7 +127,7 @@ describe('HostWindowService', () => {
|
||||
beforeEach(() => {
|
||||
const _initialState = { hostWindow: { width: 600, height: 770 } };
|
||||
store = new Store<AppState>(observableOf(_initialState), undefined, undefined);
|
||||
service = new HostWindowService(store);
|
||||
service = new HostWindowService(store, new CSSVariableServiceStub() as any);
|
||||
});
|
||||
|
||||
it('isXs() should return false with width = 600', () => {
|
||||
@@ -157,7 +163,7 @@ describe('HostWindowService', () => {
|
||||
beforeEach(() => {
|
||||
const _initialState = { hostWindow: { width: 400, height: 770 } };
|
||||
store = new Store<AppState>(observableOf(_initialState), undefined, undefined);
|
||||
service = new HostWindowService(store);
|
||||
service = new HostWindowService(store, new CSSVariableServiceStub() as any);
|
||||
});
|
||||
|
||||
it('isXs() should return true with width = 400', () => {
|
||||
@@ -191,7 +197,7 @@ describe('HostWindowService', () => {
|
||||
|
||||
describe('widthCategory', () => {
|
||||
beforeEach(() => {
|
||||
service = new HostWindowService({} as Store<AppState>);
|
||||
service = new HostWindowService({} as Store<AppState>, new CSSVariableServiceStub() as any);
|
||||
});
|
||||
|
||||
it('should call getWithObs to get the current width', () => {
|
||||
|
@@ -1,20 +1,13 @@
|
||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
|
||||
import { filter, distinctUntilChanged, map } from 'rxjs/operators';
|
||||
import { filter, distinctUntilChanged, map, first } from 'rxjs/operators';
|
||||
import { HostWindowState } from './host-window.reducer';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { createSelector, select, Store } from '@ngrx/store';
|
||||
|
||||
import { hasValue } from './empty.util';
|
||||
import { AppState } from '../app.reducer';
|
||||
|
||||
// TODO: ideally we should get these from sass somehow
|
||||
export enum GridBreakpoint {
|
||||
SM_MIN = 576,
|
||||
MD_MIN = 768,
|
||||
LG_MIN = 992,
|
||||
XL_MIN = 1200
|
||||
}
|
||||
import { CSSVariableService } from './sass-helper/sass-helper.service';
|
||||
|
||||
export enum WidthCategory {
|
||||
XS,
|
||||
@@ -29,10 +22,20 @@ const widthSelector = createSelector(hostWindowStateSelector, (hostWindow: HostW
|
||||
|
||||
@Injectable()
|
||||
export class HostWindowService {
|
||||
private breakPoints: { XS_MIN, SM_MIN, MD_MIN, LG_MIN, XL_MIN } = {} as any;
|
||||
|
||||
constructor(
|
||||
private store: Store<AppState>
|
||||
private store: Store<AppState>,
|
||||
private variableService: CSSVariableService
|
||||
) {
|
||||
/* See _exposed_variables.scss */
|
||||
variableService.getAllVariables()
|
||||
.subscribe((variables) => {
|
||||
this.breakPoints.XL_MIN = parseInt(variables.xlMin, 10);
|
||||
this.breakPoints.LG_MIN = parseInt(variables.lgMin, 10);
|
||||
this.breakPoints.MD_MIN = parseInt(variables.mdMin, 10);
|
||||
this.breakPoints.SM_MIN = parseInt(variables.smMin, 10);
|
||||
});
|
||||
}
|
||||
|
||||
private getWidthObs(): Observable<number> {
|
||||
@@ -45,13 +48,13 @@ export class HostWindowService {
|
||||
get widthCategory(): Observable<WidthCategory> {
|
||||
return this.getWidthObs().pipe(
|
||||
map((width: number) => {
|
||||
if (width < GridBreakpoint.SM_MIN) {
|
||||
if (width < this.breakPoints.SM_MIN) {
|
||||
return WidthCategory.XS
|
||||
} else if (width >= GridBreakpoint.SM_MIN && width < GridBreakpoint.MD_MIN) {
|
||||
} else if (width >= this.breakPoints.SM_MIN && width < this.breakPoints.MD_MIN) {
|
||||
return WidthCategory.SM
|
||||
} else if (width >= GridBreakpoint.MD_MIN && width < GridBreakpoint.LG_MIN) {
|
||||
} else if (width >= this.breakPoints.MD_MIN && width < this.breakPoints.LG_MIN) {
|
||||
return WidthCategory.MD
|
||||
} else if (width >= GridBreakpoint.LG_MIN && width < GridBreakpoint.XL_MIN) {
|
||||
} else if (width >= this.breakPoints.LG_MIN && width < this.breakPoints.XL_MIN) {
|
||||
return WidthCategory.LG
|
||||
} else {
|
||||
return WidthCategory.XL
|
||||
|
40
src/app/shared/menu/initial-menus-state.ts
Normal file
40
src/app/shared/menu/initial-menus-state.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { MenusState } from './menu.reducer';
|
||||
|
||||
/**
|
||||
* Availavle Menu IDs
|
||||
*/
|
||||
export enum MenuID {
|
||||
ADMIN = 'admin-sidebar',
|
||||
PUBLIC = 'public'
|
||||
}
|
||||
|
||||
/**
|
||||
* List of possible MenuItemTypes
|
||||
*/
|
||||
export enum MenuItemType {
|
||||
TEXT, LINK, ALTMETRIC, SEARCH
|
||||
}
|
||||
|
||||
/**
|
||||
* The initial state of the menus
|
||||
*/
|
||||
export const initialMenusState: MenusState = {
|
||||
[MenuID.ADMIN]:
|
||||
{
|
||||
id: MenuID.ADMIN,
|
||||
collapsed: true,
|
||||
previewCollapsed: true,
|
||||
visible: false,
|
||||
sections: {},
|
||||
sectionToSubsectionIndex: {}
|
||||
},
|
||||
[MenuID.PUBLIC]:
|
||||
{
|
||||
id: MenuID.PUBLIC,
|
||||
collapsed: true,
|
||||
previewCollapsed: true,
|
||||
visible: true,
|
||||
sections: {},
|
||||
sectionToSubsectionIndex: {}
|
||||
}
|
||||
};
|
26
src/app/shared/menu/menu-item.decorator.ts
Normal file
26
src/app/shared/menu/menu-item.decorator.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { MenuItemType } from './initial-menus-state';
|
||||
|
||||
const menuMenuItemComponentMap = new Map();
|
||||
|
||||
/**
|
||||
* Decorator function to link a MenuItemType to a Component
|
||||
* @param {MenuItemType} type The MenuItemType of the MenuSection's model
|
||||
* @returns {(sectionComponent: GenericContructor) => void}
|
||||
*/
|
||||
export function rendersMenuItemForType(type: MenuItemType) {
|
||||
return function decorator(sectionComponent: any) {
|
||||
if (!sectionComponent) {
|
||||
return;
|
||||
}
|
||||
menuMenuItemComponentMap.set(type, sectionComponent);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Component matching a given MenuItemType
|
||||
* @param {MenuItemType} type The given MenuItemType
|
||||
* @returns {GenericConstructor} The constructor of the Component that matches the MenuItemType
|
||||
*/
|
||||
export function getComponentForMenuItemType(type: MenuItemType) {
|
||||
return menuMenuItemComponentMap.get(type);
|
||||
}
|
@@ -0,0 +1 @@
|
||||
<a class="nav-item nav-link" [routerLink]="getRouterLink()">{{item.text | translate}}</a>
|
@@ -0,0 +1,57 @@
|
||||
import { async, ComponentFixture, TestBed } 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 { LinkMenuItemComponent } from './link-menu-item.component';
|
||||
import { RouterLinkDirectiveStub } from '../../testing/router-link-directive-stub';
|
||||
import { GLOBAL_CONFIG } from '../../../../config';
|
||||
|
||||
describe('LinkMenuItemComponent', () => {
|
||||
let component: LinkMenuItemComponent;
|
||||
let fixture: ComponentFixture<LinkMenuItemComponent>;
|
||||
let debugElement: DebugElement;
|
||||
const text = 'HELLO';
|
||||
const link = 'http://google.com';
|
||||
const nameSpace = 'dspace.com/';
|
||||
const globalConfig = {
|
||||
ui: {
|
||||
nameSpace: nameSpace
|
||||
}
|
||||
} as any;
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [LinkMenuItemComponent, RouterLinkDirectiveStub],
|
||||
providers: [
|
||||
{ provide: 'itemModelProvider', useValue: { text: text, link: link } },
|
||||
{ provide: GLOBAL_CONFIG, useValue: globalConfig },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LinkMenuItemComponent);
|
||||
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).toBe(nameSpace + link);
|
||||
});
|
||||
});
|
24
src/app/shared/menu/menu-item/link-menu-item.component.ts
Normal file
24
src/app/shared/menu/menu-item/link-menu-item.component.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Component, Inject, Input } from '@angular/core';
|
||||
import { LinkMenuItemModel } from './models/link.model';
|
||||
import { MenuItemType } from '../initial-menus-state';
|
||||
import { rendersMenuItemForType } from '../menu-item.decorator';
|
||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config';
|
||||
|
||||
/**
|
||||
* Component that renders a menu section of type LINK
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-link-menu-item',
|
||||
templateUrl: './link-menu-item.component.html'
|
||||
})
|
||||
@rendersMenuItemForType(MenuItemType.LINK)
|
||||
export class LinkMenuItemComponent {
|
||||
item: LinkMenuItemModel;
|
||||
constructor(@Inject('itemModelProvider') item: LinkMenuItemModel, @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) {
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
getRouterLink() {
|
||||
return this.EnvConfig.ui.nameSpace + this.item.link;
|
||||
}
|
||||
}
|
10
src/app/shared/menu/menu-item/models/altmetric.model.ts
Normal file
10
src/app/shared/menu/menu-item/models/altmetric.model.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { MenuItemType } from '../../initial-menus-state';
|
||||
import { MenuItemModel } from './menu-item.model';
|
||||
|
||||
/**
|
||||
* Model representing an Altmetric Menu Section
|
||||
*/
|
||||
export class AltmetricMenuItemModel implements MenuItemModel {
|
||||
type = MenuItemType.ALTMETRIC;
|
||||
url: string;
|
||||
}
|
11
src/app/shared/menu/menu-item/models/link.model.ts
Normal file
11
src/app/shared/menu/menu-item/models/link.model.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { MenuItemModel } from './menu-item.model';
|
||||
import { MenuItemType } from '../../initial-menus-state';
|
||||
|
||||
/**
|
||||
* Model representing an Link Menu Section
|
||||
*/
|
||||
export class LinkMenuItemModel implements MenuItemModel {
|
||||
type = MenuItemType.LINK;
|
||||
text: string;
|
||||
link: string;
|
||||
}
|
8
src/app/shared/menu/menu-item/models/menu-item.model.ts
Normal file
8
src/app/shared/menu/menu-item/models/menu-item.model.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { MenuItemType } from '../../initial-menus-state';
|
||||
|
||||
/**
|
||||
* Interface for models representing a Menu Section
|
||||
*/
|
||||
export interface MenuItemModel {
|
||||
type: MenuItemType;
|
||||
}
|
11
src/app/shared/menu/menu-item/models/search.model.ts
Normal file
11
src/app/shared/menu/menu-item/models/search.model.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { MenuItemType } from '../../initial-menus-state';
|
||||
import { MenuItemModel } from './menu-item.model';
|
||||
|
||||
/**
|
||||
* Model representing an Search Bar Menu Section
|
||||
*/
|
||||
export class SearchMenuItemModel implements MenuItemModel {
|
||||
type = MenuItemType.SEARCH;
|
||||
placeholder: string;
|
||||
action: string;
|
||||
}
|
10
src/app/shared/menu/menu-item/models/text.model.ts
Normal file
10
src/app/shared/menu/menu-item/models/text.model.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { MenuItemType } from '../../initial-menus-state';
|
||||
import { MenuItemModel } from './menu-item.model';
|
||||
|
||||
/**
|
||||
* Model representing an Text Menu Section
|
||||
*/
|
||||
export class TextMenuItemModel implements MenuItemModel {
|
||||
type = MenuItemType.TEXT;
|
||||
text: string;
|
||||
}
|
@@ -0,0 +1 @@
|
||||
<span>{{item.text | translate}}</span>
|
@@ -0,0 +1,39 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TextMenuItemComponent } from './text-menu-item.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
describe('TextMenuItemComponent', () => {
|
||||
let component: TextMenuItemComponent;
|
||||
let fixture: ComponentFixture<TextMenuItemComponent>;
|
||||
let debugElement: DebugElement;
|
||||
const text = 'HELLO';
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [TextMenuItemComponent],
|
||||
providers: [
|
||||
{ provide: 'itemModelProvider', useValue: { text: text } },
|
||||
],
|
||||
schemas: [ NO_ERRORS_SCHEMA ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TextMenuItemComponent);
|
||||
component = fixture.componentInstance;
|
||||
debugElement = fixture.debugElement;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should contain the correct text', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should contain the text element', () => {
|
||||
const textContent = debugElement.query(By.css('span')).nativeElement.textContent;
|
||||
expect(textContent).toEqual(text);
|
||||
});
|
||||
});
|
19
src/app/shared/menu/menu-item/text-menu-item.component.ts
Normal file
19
src/app/shared/menu/menu-item/text-menu-item.component.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Component, Inject, Input } from '@angular/core';
|
||||
import { TextMenuItemModel } from './models/text.model';
|
||||
import { MenuItemType } from '../initial-menus-state';
|
||||
import { rendersMenuItemForType } from '../menu-item.decorator';
|
||||
|
||||
/**
|
||||
* Component that renders a menu section of type TEXT
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-text-menu-item',
|
||||
templateUrl: './text-menu-item.component.html',
|
||||
})
|
||||
@rendersMenuItemForType(MenuItemType.TEXT)
|
||||
export class TextMenuItemComponent {
|
||||
item: TextMenuItemModel;
|
||||
constructor(@Inject('itemModelProvider') item: TextMenuItemModel) {
|
||||
this.item = item;
|
||||
}
|
||||
}
|
31
src/app/shared/menu/menu-section.decorator.ts
Normal file
31
src/app/shared/menu/menu-section.decorator.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { MenuID } from './initial-menus-state';
|
||||
|
||||
const menuComponentMap = new Map();
|
||||
|
||||
/**
|
||||
* Decorator function to render a MenuSection for a menu
|
||||
* @param {MenuID} menuID The ID of the Menu in which the section is rendered
|
||||
* @param {boolean} expandable True when the section should be expandable, false when if should not
|
||||
* @returns {(menuSectionWrapperComponent: GenericConstructor) => void}
|
||||
*/
|
||||
export function rendersSectionForMenu(menuID: MenuID, expandable: boolean) {
|
||||
return function decorator(menuSectionWrapperComponent: any) {
|
||||
if (!menuSectionWrapperComponent) {
|
||||
return;
|
||||
}
|
||||
if (!menuComponentMap.get(menuID)) {
|
||||
menuComponentMap.set(menuID, new Map());
|
||||
}
|
||||
menuComponentMap.get(menuID).set(expandable, menuSectionWrapperComponent);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the component matching the given MenuID and whether or not it should be expandable
|
||||
* @param {MenuID} menuID The ID of the Menu in which the section is rendered
|
||||
* @param {boolean} expandable True when the section should be expandable, false when if should not
|
||||
* @returns {GenericConstructor} The constructor of the matching Component
|
||||
*/
|
||||
export function getComponentForMenu(menuID: MenuID, expandable: boolean) {
|
||||
return menuComponentMap.get(menuID).get(expandable);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user