Merge branch 'main' into w2p-96062_theme-collection-dropdown-component

This commit is contained in:
jensvannerum
2022-11-02 13:00:19 +01:00
committed by GitHub
50 changed files with 1104 additions and 1492 deletions

View File

@@ -6,6 +6,9 @@ name: Build
# Run this Build for all pushes / PRs to current branch
on: [push, pull_request]
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
tests:
runs-on: ubuntu-latest

View File

@@ -12,6 +12,9 @@ on:
- 'dspace-**'
pull_request:
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
docker:
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'

View File

@@ -5,6 +5,7 @@ on:
issues:
types: [opened]
permissions: {}
jobs:
automation:
runs-on: ubuntu-latest

View File

@@ -11,13 +11,14 @@ on:
pull_request_target:
types: [ synchronize ]
permissions: {}
jobs:
triage:
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
if: github.repository == 'dspace/dspace-angular'
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
# See: https://github.com/prince-chrismc/label-merge-conflicts-action

View File

@@ -13,13 +13,14 @@
[paginationOptions]="pageConfig"
[pageInfoState]="(bitstreamFormats | async)?.payload"
[collectionSize]="(bitstreamFormats | async)?.payload?.totalElements"
[hideGear]="true"
[hideGear]="false"
[hidePagerWhenSinglePage]="true">
<div class="table-responsive">
<table id="formats" class="table table-striped table-hover">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">{{'admin.registries.bitstream-formats.table.id' | translate}}</th>
<th scope="col">{{'admin.registries.bitstream-formats.table.name' | translate}}</th>
<th scope="col">{{'admin.registries.bitstream-formats.table.mimetype' | translate}}</th>
<th scope="col">{{'admin.registries.bitstream-formats.table.supportLevel.head' | translate}}</th>
@@ -35,6 +36,7 @@
>
</label>
</td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.id}}</a></td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.shortDescription}}</a></td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.mimetype}} <span *ngIf="bitstreamFormat.internal">({{'admin.registries.bitstream-formats.table.internal' | translate}})</span></a></td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{'admin.registries.bitstream-formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}</a></td>

View File

@@ -129,16 +129,19 @@ describe('BitstreamFormatsComponent', () => {
});
it('should contain the correct formats', () => {
const unknownName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(2)')).nativeElement;
const unknownName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(3)')).nativeElement;
expect(unknownName.textContent).toBe('Unknown');
const licenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(2) td:nth-child(2)')).nativeElement;
const UUID: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(2)')).nativeElement;
expect(UUID.textContent).toBe('test-uuid-1');
const licenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(2) td:nth-child(3)')).nativeElement;
expect(licenseName.textContent).toBe('License');
const ccLicenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(3) td:nth-child(2)')).nativeElement;
const ccLicenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(3) td:nth-child(3)')).nativeElement;
expect(ccLicenseName.textContent).toBe('CC License');
const adobeName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(4) td:nth-child(2)')).nativeElement;
const adobeName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(4) td:nth-child(3)')).nativeElement;
expect(adobeName.textContent).toBe('Adobe PDF');
});
});

View File

@@ -1,5 +1,5 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { combineLatest as observableCombineLatest, Observable, zip } from 'rxjs';
import {combineLatest, combineLatest as observableCombineLatest, Observable, of as observableOf, zip} from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
@@ -29,21 +29,14 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
*/
bitstreamFormats: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
/**
* The current pagination configuration for the page used by the FindAll method
* Currently simply renders all bitstream formats
*/
config: FindListOptions = Object.assign(new FindListOptions(), {
elementsPerPage: 20
});
/**
* The current pagination configuration for the page
* Currently simply renders all bitstream formats
*/
pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'rbp',
pageSize: 20
pageSize: 20,
pageSizeOptions: [20, 40, 60, 80, 100]
});
constructor(private notificationsService: NotificationsService,
@@ -149,7 +142,7 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
ngOnInit(): void {
this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe(
this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe(
switchMap((findListOptions: FindListOptions) => {
return this.bitstreamFormatService.findAll(findListOptions);
})

View File

@@ -11,11 +11,11 @@
</div>
<div>
<span class="font-weight-bold">{{'collection.source.controls.harvest.last' | translate}}</span>
<span>{{contentSource?.message ? contentSource?.message : 'collection.source.controls.harvest.no-information'|translate }}</span>
<span>{{contentSource?.lastHarvested ? contentSource?.lastHarvested : 'collection.source.controls.harvest.no-information'|translate }}</span>
</div>
<div>
<span class="font-weight-bold">{{'collection.source.controls.harvest.message' | translate}}</span>
<span>{{contentSource?.lastHarvested ? contentSource?.lastHarvested : 'collection.source.controls.harvest.no-information'|translate }}</span>
<span>{{contentSource?.message ? contentSource?.message: 'collection.source.controls.harvest.no-information'|translate }}</span>
</div>
<button *ngIf="!(testConfigRunning$ |async)" class="btn btn-secondary"

View File

@@ -26,6 +26,8 @@ import { SearchConfigurationService } from './search-configuration.service';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { RequestEntry } from '../../data/request-entry.model';
import { Angulartics2 } from 'angulartics2';
import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model';
import anything = jasmine.anything;
@Component({ template: '' })
class DummyComponent {
@@ -36,7 +38,7 @@ describe('SearchService', () => {
let searchService: SearchService;
const router = new RouterStub();
const route = new ActivatedRouteStub();
const searchConfigService = {paginationID: 'page-id'};
const searchConfigService = { paginationID: 'page-id' };
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
@@ -103,7 +105,8 @@ describe('SearchService', () => {
};
const paginationService = new PaginationServiceStub();
const searchConfigService = {paginationID: 'page-id'};
const searchConfigService = { paginationID: 'page-id' };
const requestService = getMockRequestService();
beforeEach(() => {
TestBed.configureTestingModule({
@@ -119,7 +122,7 @@ describe('SearchService', () => {
providers: [
{ provide: Router, useValue: router },
{ provide: RouteService, useValue: routeServiceStub },
{ provide: RequestService, useValue: getMockRequestService() },
{ provide: RequestService, useValue: requestService },
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService },
{ provide: HALEndpointService, useValue: halService },
{ provide: CommunityDataService, useValue: {} },
@@ -138,13 +141,13 @@ describe('SearchService', () => {
it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => {
searchService.setViewMode(ViewMode.ListElement);
expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], {page: 1}, { view: ViewMode.ListElement }
expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], { page: 1 }, { view: ViewMode.ListElement }
);
});
it('should call the navigate method on the Router with view mode grid parameter as a parameter when setViewMode is called', () => {
searchService.setViewMode(ViewMode.GridElement);
expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], {page: 1}, { view: ViewMode.GridElement }
expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], { page: 1 }, { view: ViewMode.GridElement }
);
});
@@ -191,5 +194,23 @@ describe('SearchService', () => {
expect((searchService as any).rdb.buildFromHref).toHaveBeenCalledWith(endPoint);
});
});
describe('when getFacetValuesFor is called with a filterQuery', () => {
it('should add the encoded filterQuery to the args list', () => {
jasmine.getEnv().allowRespy(true);
const spyRequest = spyOn((searchService as any), 'request').and.stub();
spyOn(requestService, 'send').and.returnValue(true);
const searchFilterConfig = new SearchFilterConfig();
searchFilterConfig._links = {
self: {
href: 'https://demo.dspace.org/',
},
};
searchService.getFacetValuesFor(searchFilterConfig, 1, undefined, 'filter&Query');
expect(spyRequest).toHaveBeenCalledWith(anything(), 'https://demo.dspace.org?page=0&size=5&prefix=filter%26Query');
});
});
});
});

View File

@@ -271,7 +271,7 @@ export class SearchService implements OnDestroy {
let href;
let args: string[] = [];
if (hasValue(filterQuery)) {
args.push(`prefix=${filterQuery}`);
args.push(`prefix=${encodeURIComponent(filterQuery)}`);
}
if (hasValue(searchOptions)) {
searchOptions = Object.assign(new PaginatedSearchOptions({}), searchOptions, {

View File

@@ -1,4 +1,4 @@
<div class="nav-item dropdown expandable-navbar-section"
<div class="nav-item dropdown expandable-navbar-section text-md-center"
*ngVar="(active | async) as isActive"
(keyup.enter)="isActive ? deactivateSection($event) : activateSection($event)"
(keyup.space)="isActive ? deactivateSection($event) : activateSection($event)"

View File

@@ -1,3 +1,10 @@
.expandable-navbar-section {
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
}
.dropdown-menu {
overflow: hidden;
min-width: 100%;

View File

@@ -1,3 +1,3 @@
<div class="nav-item navbar-section">
<div class="nav-item navbar-section text-md-center">
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
</div>

View File

@@ -0,0 +1,5 @@
.navbar-section {
display: flex;
align-items: center;
height: 100%;
}

View File

@@ -1,10 +1,13 @@
<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" role="navigation" [attr.aria-label]="'nav.main.description' | translate">
<!-- TODO remove navbar-container class when https://github.com/twbs/bootstrap/issues/24726 is fixed -->
<div class="container">
<div class="navbar-inner-container w-100" [class.container]="!(isXsOrSm$ | async)">
<div class="reset-padding-md w-100">
<div id="collapsingNav">
<ul class="navbar-nav navbar-navigation mr-auto shadow-none">
<li *ngIf="(isXsOrSm$ | async) && (isAuthenticated$ | async)">
<ds-user-menu [inExpandableNavbar]="true"></ds-user-menu>
</li>
<ng-container *ngFor="let section of (sections | async)">
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
</ng-container>

View File

@@ -6,13 +6,14 @@ nav.navbar {
/** Mobile menu styling **/
@media screen and (max-width: map-get($grid-breakpoints, md)-0.02) {
.navbar {
width: 100vw;
width: 100%;
background-color: var(--bs-white);
position: absolute;
overflow: hidden;
height: 0;
&.open {
height: 100vh; //doesn't matter because wrapper is sticky
height: auto;
min-height: 100vh; //doesn't matter because wrapper is sticky
}
}
}
@@ -27,7 +28,7 @@ nav.navbar {
/* 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)-0.02) {
> .container {
> .navbar-inner-container {
padding: 0 var(--bs-spacer);
}
padding: 0;

View File

@@ -22,9 +22,17 @@ import { Item } from '../core/shared/item.model';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { ThemeService } from '../shared/theme-support/theme.service';
import { getMockThemeService } from '../shared/mocks/theme-service.mock';
import { Store, StoreModule } from '@ngrx/store';
import { AppState, storeModuleConfig } from '../app.reducer';
import { authReducer } from '../core/auth/auth.reducer';
import { provideMockStore } from '@ngrx/store/testing';
import { AuthTokenInfo } from '../core/auth/models/auth-token-info.model';
import { EPersonMock } from '../shared/testing/eperson.mock';
let comp: NavbarComponent;
let fixture: ComponentFixture<NavbarComponent>;
let store: Store<AppState>;
let initialState: any;
const authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true)
@@ -83,10 +91,24 @@ describe('NavbarComponent', () => {
}
),
];
initialState = {
core: {
auth: {
authenticated: true,
loaded: true,
blocking: false,
loading: false,
authToken: new AuthTokenInfo('test_token'),
userId: EPersonMock.id,
authMethods: []
}
}
};
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
StoreModule.forRoot({ auth: authReducer }, storeModuleConfig),
NoopAnimationsModule,
ReactiveFormsModule,
RouterTestingModule],
@@ -99,6 +121,7 @@ describe('NavbarComponent', () => {
{ provide: ActivatedRoute, useValue: routeStub },
{ provide: BrowseService, useValue: { getBrowseDefinitions: createSuccessfulRemoteDataObject$(buildPaginatedList(undefined, browseDefinitions)) } },
{ provide: AuthorizationDataService, useValue: authorizationService },
provideMockStore({ initialState }),
],
schemas: [NO_ERRORS_SCHEMA]
})
@@ -107,7 +130,7 @@ describe('NavbarComponent', () => {
// synchronous beforeEach
beforeEach(() => {
store = TestBed.inject(Store);
fixture = TestBed.createComponent(NavbarComponent);
comp = fixture.componentInstance;

View File

@@ -8,6 +8,10 @@ import { ActivatedRoute } from '@angular/router';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { MenuID } from '../shared/menu/menu-id.model';
import { ThemeService } from '../shared/theme-support/theme.service';
import { Observable } from 'rxjs';
import { select, Store } from '@ngrx/store';
import { AppState } from '../app.reducer';
import { isAuthenticated } from '../core/auth/selectors';
/**
* Component representing the public navbar
@@ -25,18 +29,29 @@ export class NavbarComponent extends MenuComponent {
*/
menuID = MenuID.PUBLIC;
/**
* Whether user is authenticated.
* @type {Observable<string>}
*/
public isAuthenticated$: Observable<boolean>;
public isXsOrSm$: Observable<boolean>;
constructor(protected menuService: MenuService,
protected injector: Injector,
public windowService: HostWindowService,
public browseService: BrowseService,
public authorizationService: AuthorizationDataService,
public route: ActivatedRoute,
protected themeService: ThemeService
protected themeService: ThemeService,
private store: Store<AppState>,
) {
super(menuService, injector, authorizationService, route, themeService);
}
ngOnInit(): void {
super.ngOnInit();
this.isXsOrSm$ = this.windowService.isXsOrSm();
this.isAuthenticated$ = this.store.pipe(select(isAuthenticated));
}
}

View File

@@ -1,9 +1,9 @@
<div class="outer-wrapper" [class.d-none]="shouldShowFullscreenLoader">
<ds-themed-admin-sidebar></ds-themed-admin-sidebar>
<div class="inner-wrapper" [@slideSidebarPadding]="{
<div class="outer-wrapper" [class.d-none]="shouldShowFullscreenLoader" [@slideSidebarPadding]="{
value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'),
params: {collapsedSidebarWidth: (collapsedSidebarWidth | async), totalSidebarWidth: (totalSidebarWidth | async)}
}">
<ds-themed-admin-sidebar></ds-themed-admin-sidebar>
<div class="inner-wrapper">
<ds-themed-header-navbar-wrapper></ds-themed-header-navbar-wrapper>
<main class="main-content">
<ds-themed-breadcrumbs></ds-themed-breadcrumbs>

View File

@@ -12,7 +12,7 @@ export const slide = trigger('slide', [
export const slideMobileNav = trigger('slideMobileNav', [
state('expanded', style({ height: '100vh' })),
state('expanded', style({ height: 'auto', 'min-height': '100vh' })),
state('collapsed', style({ height: 0 })),

View File

@@ -2,11 +2,11 @@
<li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item"
(click)="$event.stopPropagation();">
<div ngbDropdown #loginDrop display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
<a href="javascript:void(0);" class="dropdownLogin px-1 " [attr.aria-label]="'nav.login' |translate" (click)="$event.preventDefault()" [attr.data-test]="'login-menu' | dsBrowserOnly" ngbDropdownToggle>
{{ 'nav.login' | translate }}
</a>
<a href="javascript:void(0);" class="dropdownLogin px-1" [attr.aria-label]="'nav.login' |translate"
(click)="$event.preventDefault()" [attr.data-test]="'login-menu' | dsBrowserOnly"
ngbDropdownToggle>{{ 'nav.login' | translate }}</a>
<div class="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu
[attr.aria-label]="'nav.login' |translate">
[attr.aria-label]="'nav.login' | translate">
<ds-log-in
[isStandalonePage]="false"></ds-log-in>
</div>
@@ -19,16 +19,16 @@
</li>
<li *ngIf="(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item">
<div ngbDropdown display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
<a href="javascript:void(0);" role="button" [attr.aria-label]="'nav.logout' |translate" (click)="$event.preventDefault()" [title]="'nav.logout' | translate" class="px-1" [attr.data-test]="'user-menu' | dsBrowserOnly" ngbDropdownToggle>
<a href="javascript:void(0);" role="button" [attr.aria-label]="'nav.user-profile-menu-and-logout' |translate" (click)="$event.preventDefault()" [title]="'nav.user-profile-menu-and-logout' | translate" class="px-1" [attr.data-test]="'user-menu' | dsBrowserOnly" ngbDropdownToggle>
<i class="fas fa-user-circle fa-lg fa-fw"></i></a>
<div class="logoutDropdownMenu" ngbDropdownMenu [attr.aria-label]="'nav.logout' |translate">
<div class="logoutDropdownMenu" ngbDropdownMenu [attr.aria-label]="'nav.user-profile-menu-and-logout' |translate">
<ds-user-menu></ds-user-menu>
</div>
</div>
</li>
<li *ngIf="(isAuthenticated | async) && (isXsOrSm$ | async)" class="nav-item">
<a id="logoutLink" role="button" [attr.aria-label]="'nav.logout' |translate" [title]="'nav.logout' | translate" routerLink="/logout" routerLinkActive="active" class="px-1">
<i class="fas fa-user-circle fa-lg fa-fw"></i>
<i class="fas fa-sign-out-alt fa-lg fa-fw"></i>
<span class="sr-only">(current)</span>
</a>
</li>

View File

@@ -1,10 +1,13 @@
<ds-themed-loading *ngIf="(loading$ | async)"></ds-themed-loading>
<div *ngIf="!(loading$ | async)">
<span class="dropdown-item-text">{{(user$ | async)?.name}} ({{(user$ | async)?.email}})</span>
<a class="dropdown-item" [routerLink]="[profileRoute]" routerLinkActive="active">{{'nav.profile' | translate}}</a>
<a class="dropdown-item" [routerLink]="[mydspaceRoute]" routerLinkActive="active">{{'nav.mydspace' | translate}}</a>
<span class="dropdown-item-text" [class.pl-0]="inExpandableNavbar">
{{(user$ | async)?.name}}<br>
<span class="text-muted">{{(user$ | async)?.email}}</span>
</span>
<a [ngClass]="inExpandableNavbar ? 'nav-item nav-link' : 'dropdown-item'" [routerLink]="[profileRoute]" routerLinkActive="active">{{'nav.profile' | translate}}</a>
<a [ngClass]="inExpandableNavbar ? 'nav-item nav-link' : 'dropdown-item'" [routerLink]="[mydspaceRoute]" routerLinkActive="active">{{'nav.mydspace' | translate}}</a>
<div class="dropdown-divider"></div>
<ds-log-out></ds-log-out>
<ds-log-out *ngIf="!inExpandableNavbar" data-test="log-out-component"></ds-log-out>
</div>

View File

@@ -162,10 +162,24 @@ describe('UserMenuComponent', () => {
});
it('should display user name and email', () => {
const user = 'User Test (test@test.com)';
const username = 'User Test';
const email = 'test@test.com';
const span = deUserMenu.query(By.css('.dropdown-item-text'));
expect(span).toBeDefined();
expect(span.nativeElement.innerHTML).toBe(user);
expect(span.nativeElement.innerHTML).toContain(username);
expect(span.nativeElement.innerHTML).toContain(email);
});
it('should create logout component', () => {
const components = fixture.debugElement.query(By.css('[data-test="log-out-component"]'));
expect(components).toBeTruthy();
});
it('should not create logout component', () => {
component.inExpandableNavbar = true;
fixture.detectChanges();
const components = fixture.debugElement.query(By.css('[data-test="log-out-component"]'));
expect(components).toBeFalsy();
});
});

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { select, Store } from '@ngrx/store';
@@ -20,6 +20,11 @@ import { getProfileModuleRoute } from '../../../app-routing-paths';
})
export class UserMenuComponent implements OnInit {
/**
* The input flag to show user details in navbar expandable menu
*/
@Input() inExpandableNavbar = false;
/**
* True if the authentication is loading.
* @type {Observable<boolean>}

View File

@@ -1,7 +1,7 @@
/* eslint-disable max-classes-per-file */
import { Item } from '../../../../core/shared/item.model';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { getListableObjectComponent, listableObjectComponent } from './listable-object.decorator';
import { DEFAULT_VIEW_MODE, getListableObjectComponent, listableObjectComponent } from './listable-object.decorator';
import { Context } from '../../../../core/shared/context.model';
import { environment } from '../../../../../environments/environment';
@@ -13,6 +13,10 @@ describe('ListableObject decorator function', () => {
const type3 = 'TestType3';
const typeAncestor = 'TestTypeAncestor';
const typeUnthemed = 'TestTypeUnthemed';
const typeLowPriority = 'TypeLowPriority';
const typeLowPriority2 = 'TypeLowPriority2';
const typeMidPriority = 'TypeMidPriority';
const typeHighPriority = 'TypeHighPriority';
class Test1List {
}
@@ -38,6 +42,21 @@ describe('ListableObject decorator function', () => {
class TestUnthemedComponent {
}
class TestDefaultLowPriorityComponent {
}
class TestLowPriorityComponent {
}
class TestDefaultMidPriorityComponent {
}
class TestMidPriorityComponent {
}
class TestHighPriorityComponent {
}
/* eslint-enable max-classes-per-file */
beforeEach(() => {
@@ -54,6 +73,15 @@ describe('ListableObject decorator function', () => {
listableObjectComponent(typeAncestor, ViewMode.ListElement, Context.Any, 'ancestor')(TestAncestorComponent);
listableObjectComponent(typeUnthemed, ViewMode.ListElement, Context.Any)(TestUnthemedComponent);
// Register component with different priorities for expected parameters:
// ViewMode.DetailedListElement, Context.Search, 'custom'
listableObjectComponent(typeLowPriority, DEFAULT_VIEW_MODE, undefined, undefined)(TestDefaultLowPriorityComponent);
listableObjectComponent(typeLowPriority, DEFAULT_VIEW_MODE, Context.Search, 'custom')(TestLowPriorityComponent);
listableObjectComponent(typeLowPriority2, DEFAULT_VIEW_MODE, undefined, undefined)(TestDefaultLowPriorityComponent);
listableObjectComponent(typeMidPriority, ViewMode.DetailedListElement, undefined, undefined)(TestDefaultMidPriorityComponent);
listableObjectComponent(typeMidPriority, ViewMode.DetailedListElement, undefined, 'custom')(TestMidPriorityComponent);
listableObjectComponent(typeHighPriority, ViewMode.DetailedListElement, Context.Search, undefined)(TestHighPriorityComponent);
ogEnvironmentThemes = environment.themes;
});
@@ -81,7 +109,7 @@ describe('ListableObject decorator function', () => {
});
});
describe('If there isn\'nt an exact match', () => {
describe('If there isn\'t an exact match', () => {
describe('If there is a match for one of the entity types and the view mode', () => {
it('should return the class with the matching entity type and view mode and default context', () => {
const component = getListableObjectComponent([type3], ViewMode.ListElement, Context.Workspace);
@@ -152,4 +180,45 @@ describe('ListableObject decorator function', () => {
});
});
});
describe('priorities', () => {
beforeEach(() => {
environment.themes = [
{
name: 'custom',
}
];
});
describe('If a component with default ViewMode contains specific context and/or theme', () => {
it('requesting a specific ViewMode should return the one with the requested context and/or theme', () => {
const component = getListableObjectComponent([typeLowPriority], ViewMode.DetailedListElement, Context.Search, 'custom');
expect(component).toEqual(TestLowPriorityComponent);
});
});
describe('If a component with default Context contains specific ViewMode and/or theme', () => {
it('requesting a specific Context should return the one with the requested view-mode and/or theme', () => {
const component = getListableObjectComponent([typeMidPriority], ViewMode.DetailedListElement, Context.Search, 'custom');
expect(component).toEqual(TestMidPriorityComponent);
});
});
describe('If multiple components exist, each containing a different default value for one of the requested parameters', () => {
it('the component with the latest default value in the list should be returned', () => {
let component = getListableObjectComponent([typeMidPriority, typeLowPriority], ViewMode.DetailedListElement, Context.Search, 'custom');
expect(component).toEqual(TestMidPriorityComponent);
component = getListableObjectComponent([typeLowPriority, typeMidPriority, typeHighPriority], ViewMode.DetailedListElement, Context.Search, 'custom');
expect(component).toEqual(TestHighPriorityComponent);
});
});
describe('If two components exist for two different types, both configured for the same view-mode, but one for a specific context and/or theme', () => {
it('requesting a component for that specific context and/or theme while providing both types should return the most relevant one', () => {
const component = getListableObjectComponent([typeLowPriority2, typeLowPriority], ViewMode.DetailedListElement, Context.Search, 'custom');
expect(component).toEqual(TestLowPriorityComponent);
});
});
});
});

View File

@@ -11,6 +11,53 @@ export const DEFAULT_VIEW_MODE = ViewMode.ListElement;
export const DEFAULT_CONTEXT = Context.Any;
export const DEFAULT_THEME = '*';
/**
* A class used to compare two matches and their relevancy to determine which of the two gains priority over the other
*
* "level" represents the index of the first default value that was used to find the match with:
* ViewMode being index 0, Context index 1 and theme index 2. Examples:
* - If a default value was used for context, but not view-mode and theme, the "level" will be 1
* - If a default value was used for view-mode and context, but not for theme, the "level" will be 0
* - If no default value was used for any of the fields, the "level" will be 3
*
* "relevancy" represents the amount of values that didn't require a default value to fall back on. Examples:
* - If a default value was used for theme, but not view-mode and context, the "relevancy" will be 2
* - If a default value was used for view-mode and context, but not for theme, the "relevancy" will be 1
* - If a default value was used for all fields, the "relevancy" will be 0
* - If no default value was used for any of the fields, the "relevancy" will be 3
*
* To determine which of two MatchRelevancies is the most relevant, we compare "level" and "relevancy" in that order.
* If any of the two is higher than the other, that match is most relevant. Examples:
* - { level: 1, relevancy: 1 } is more relevant than { level: 0, relevancy: 2 }
* - { level: 1, relevancy: 1 } is less relevant than { level: 1, relevancy: 2 }
* - { level: 1, relevancy: 1 } is more relevant than { level: 1, relevancy: 0 }
* - { level: 1, relevancy: 1 } is less relevant than { level: 2, relevancy: 0 }
* - { level: 1, relevancy: 1 } is more relevant than null
*/
class MatchRelevancy {
constructor(public match: any,
public level: number,
public relevancy: number) {
}
isMoreRelevantThan(otherMatch: MatchRelevancy): boolean {
if (hasNoValue(otherMatch)) {
return true;
}
if (otherMatch.level > this.level) {
return false;
}
if (otherMatch.level === this.level && otherMatch.relevancy > this.relevancy) {
return false;
}
return true;
}
isLessRelevantThan(otherMatch: MatchRelevancy): boolean {
return !this.isMoreRelevantThan(otherMatch);
}
}
/**
* Factory to allow us to inject getThemeConfigFor so we can mock it in tests
*/
@@ -48,47 +95,70 @@ export function listableObjectComponent(objectType: string | GenericConstructor<
/**
* Getter to retrieve the matching listable object component
*
* Looping over the provided types, it'll attempt to find the best match depending on the {@link MatchRelevancy} returned by getMatch()
* The most relevant match between types is kept and eventually returned
*
* @param types The types of which one should match the listable component
* @param viewMode The view mode that should match the components
* @param context The context that should match the components
* @param theme The theme that should match the components
*/
export function getListableObjectComponent(types: (string | GenericConstructor<ListableObject>)[], viewMode: ViewMode, context: Context = DEFAULT_CONTEXT, theme: string = DEFAULT_THEME) {
let bestMatch;
let bestMatchValue = 0;
let currentBestMatch: MatchRelevancy = null;
for (const type of types) {
const typeMap = map.get(type);
if (hasValue(typeMap)) {
const typeModeMap = typeMap.get(viewMode);
if (hasValue(typeModeMap)) {
const contextMap = typeModeMap.get(context);
if (hasValue(contextMap)) {
const match = resolveTheme(contextMap, theme);
if (hasValue(match)) {
return match;
}
if (bestMatchValue < 3 && hasValue(contextMap.get(DEFAULT_THEME))) {
bestMatchValue = 3;
bestMatch = contextMap.get(DEFAULT_THEME);
}
}
if (bestMatchValue < 2 &&
hasValue(typeModeMap.get(DEFAULT_CONTEXT)) &&
hasValue(typeModeMap.get(DEFAULT_CONTEXT).get(DEFAULT_THEME))) {
bestMatchValue = 2;
bestMatch = typeModeMap.get(DEFAULT_CONTEXT).get(DEFAULT_THEME);
}
}
if (bestMatchValue < 1 &&
hasValue(typeMap.get(DEFAULT_VIEW_MODE)) &&
hasValue(typeMap.get(DEFAULT_VIEW_MODE).get(DEFAULT_CONTEXT)) &&
hasValue(typeMap.get(DEFAULT_VIEW_MODE).get(DEFAULT_CONTEXT).get(DEFAULT_THEME))) {
bestMatchValue = 1;
bestMatch = typeMap.get(DEFAULT_VIEW_MODE).get(DEFAULT_CONTEXT).get(DEFAULT_THEME);
const match = getMatch(typeMap, [viewMode, context, theme], [DEFAULT_VIEW_MODE, DEFAULT_CONTEXT, DEFAULT_THEME]);
if (hasNoValue(currentBestMatch) || currentBestMatch.isLessRelevantThan(match)) {
currentBestMatch = match;
}
}
}
return bestMatch;
return hasValue(currentBestMatch) ? currentBestMatch.match : null;
}
/**
* Find an object within a nested map, matching the provided keys as best as possible, falling back on defaults wherever
* needed.
*
* Starting off with a Map, it loops over the provided keys, going deeper into the map until it finds a value
* If at some point, no value is found, it'll attempt to use the default value for that index instead
* If the default value exists, the index is stored in the "level"
* If no default value exists, 1 is added to "relevancy"
* See {@link MatchRelevancy} what these represent
*
* @param typeMap a multi-dimensional map
* @param keys the keys of the multi-dimensional map to loop over. Each key represents a level within the map
* @param defaults the default values to use for each level, in case no value is found for the key at that index
* @returns matchAndLevel a {@link MatchRelevancy} object containing the match and its level of relevancy
*/
function getMatch(typeMap: Map<any, any>, keys: any[], defaults: any[]): MatchRelevancy {
let currentMap = typeMap;
let level = -1;
let relevancy = 0;
for (let i = 0; i < keys.length; i++) {
// If we're currently checking the theme, resolve it first to take extended themes into account
let currentMatch = defaults[i] === DEFAULT_THEME ? resolveTheme(currentMap, keys[i]) : currentMap.get(keys[i]);
if (hasNoValue(currentMatch)) {
currentMatch = currentMap.get(defaults[i]);
if (level === -1) {
level = i;
}
} else {
relevancy++;
}
if (hasValue(currentMatch)) {
if (currentMatch instanceof Map) {
currentMap = currentMatch as Map<any, any>;
} else {
return new MatchRelevancy(currentMatch, level > -1 ? level : i + 1, relevancy);
}
} else {
return null;
}
}
return null;
}
/**

View File

@@ -207,6 +207,8 @@
// "admin.registries.bitstream-formats.table.name": "Name",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.name": "Name",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.id" : "ID",
// "admin.registries.bitstream-formats.table.return": "Return",
// TODO New key - Add a translation

View File

@@ -165,6 +165,8 @@
// "admin.registries.bitstream-formats.table.name": "Name",
"admin.registries.bitstream-formats.table.name": "নাম",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.id" : "ID",
// "admin.registries.bitstream-formats.table.return": "Back",
"admin.registries.bitstream-formats.table.return": "পেছনে",

View File

@@ -202,6 +202,8 @@
// "admin.registries.bitstream-formats.table.name": "Name",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.name": "Name",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.id" : "ID",
// "admin.registries.bitstream-formats.table.return": "Return",
// TODO New key - Add a translation

View File

@@ -177,6 +177,8 @@
// "admin.registries.bitstream-formats.table.name": "Name",
"admin.registries.bitstream-formats.table.name": "Name",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.id" : "ID",
// "admin.registries.bitstream-formats.table.return": "Return",
"admin.registries.bitstream-formats.table.return": "Zurück",

View File

@@ -202,6 +202,8 @@
"admin.registries.bitstream-formats.table.internal": "εσωτερικός",
"admin.registries.bitstream-formats.table.mimetype": "mimetype",
"admin.registries.bitstream-formats.table.name": "Ονομα",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.id" : "ID",
"admin.registries.bitstream-formats.table.return": "Επιστροφή",
"admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Γνωστός",
"admin.registries.bitstream-formats.table.supportLevel.SUPPORTED": "Υποστηρίζεται",

View File

@@ -130,6 +130,7 @@
"admin.registries.bitstream-formats.table.mimetype": "MIME Type",
"admin.registries.bitstream-formats.table.name": "Name",
"admin.registries.bitstream-formats.table.id" : "ID",
"admin.registries.bitstream-formats.table.return": "Back",
@@ -2874,7 +2875,9 @@
"nav.login": "Log In",
"nav.logout": "User profile menu and Log Out",
"nav.user-profile-menu-and-logout": "User profile menu and Log Out",
"nav.logout": "Log Out",
"nav.main.description": "Main navigation bar",

View File

@@ -193,6 +193,8 @@
// "admin.registries.bitstream-formats.table.name": "Name",
"admin.registries.bitstream-formats.table.name": "Nombre",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.id" : "ID",
// "admin.registries.bitstream-formats.table.return": "Back",
"admin.registries.bitstream-formats.table.return": "Atrás",

View File

@@ -156,6 +156,8 @@
// "admin.registries.bitstream-formats.table.name": "Name",
"admin.registries.bitstream-formats.table.name": "Nimi",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.id" : "ID",
// "admin.registries.bitstream-formats.table.return": "Return",
"admin.registries.bitstream-formats.table.return": "Palaa",

View File

@@ -176,6 +176,8 @@
// "admin.registries.bitstream-formats.table.name": "Name",
"admin.registries.bitstream-formats.table.name": "Nom",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.id" : "ID",
// "admin.registries.bitstream-formats.table.return": "Back",
"admin.registries.bitstream-formats.table.return": "Retour",

View File

@@ -156,6 +156,8 @@
// "admin.registries.bitstream-formats.table.name": "Name",
"admin.registries.bitstream-formats.table.name": "Ainm",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.id" : "ID",
// "admin.registries.bitstream-formats.table.return": "Back",
"admin.registries.bitstream-formats.table.return": "Air ais",

View File

@@ -157,6 +157,8 @@
// "admin.registries.bitstream-formats.table.name": "Name",
"admin.registries.bitstream-formats.table.name": "Név",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.id" : "ID",
// "admin.registries.bitstream-formats.table.return": "Return",
"admin.registries.bitstream-formats.table.return": "Vissza",

View File

@@ -207,6 +207,8 @@
// "admin.registries.bitstream-formats.table.name": "Name",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.name": "Name",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.id" : "ID",
// "admin.registries.bitstream-formats.table.return": "Return",
// TODO New key - Add a translation

File diff suppressed because it is too large Load Diff

View File

@@ -168,6 +168,8 @@
// "admin.registries.bitstream-formats.table.name": "Name",
"admin.registries.bitstream-formats.table.name": "Nosaukums",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.id" : "ID",
// "admin.registries.bitstream-formats.table.return": "Return",
"admin.registries.bitstream-formats.table.return": "Atgriezties",

View File

@@ -169,6 +169,8 @@
// "admin.registries.bitstream-formats.table.name": "Name",
"admin.registries.bitstream-formats.table.name": "Naam",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.id" : "ID",
// "admin.registries.bitstream-formats.table.return": "Return",
"admin.registries.bitstream-formats.table.return": "Terug",

View File

@@ -207,6 +207,8 @@
// "admin.registries.bitstream-formats.table.name": "Name",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.name": "Name",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.id" : "ID",
// "admin.registries.bitstream-formats.table.return": "Return",
// TODO New key - Add a translation

View File

@@ -205,6 +205,9 @@
// "admin.registries.bitstream-formats.table.name": "Name",
"admin.registries.bitstream-formats.table.name": "Nome",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.id" : "ID",
// "admin.registries.bitstream-formats.table.return": "Back",
"admin.registries.bitstream-formats.table.return": "Voltar",

View File

@@ -368,6 +368,8 @@
// "admin.registries.bitstream-formats.table.name": "Name",
"admin.registries.bitstream-formats.table.name": "Nome",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.id" : "ID",
// "admin.registries.bitstream-formats.table.return": "Return",
"admin.registries.bitstream-formats.table.return": "Voltar",

View File

@@ -168,6 +168,8 @@
// "admin.registries.bitstream-formats.table.name": "Name",
"admin.registries.bitstream-formats.table.name": "Namn",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.id" : "ID",
// "admin.registries.bitstream-formats.table.return": "Back",
"admin.registries.bitstream-formats.table.return": "Tillbaka",

View File

@@ -207,6 +207,8 @@
// "admin.registries.bitstream-formats.table.name": "Name",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.name": "Name",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.id" : "ID",
// "admin.registries.bitstream-formats.table.return": "Return",
// TODO New key - Add a translation

View File

@@ -156,6 +156,8 @@
// "admin.registries.bitstream-formats.table.name": "Name",
"admin.registries.bitstream-formats.table.name": "İsim",
// TODO New key - Add a translation
"admin.registries.bitstream-formats.table.id" : "ID",
// "admin.registries.bitstream-formats.table.return": "Return",
"admin.registries.bitstream-formats.table.return": "Geri Dön",

View File

@@ -1,12 +1,15 @@
<nav [ngClass]="{'open': !(menuCollapsed | async)}" [@slideMobileNav]="!(windowService.isXsOrSm() | async) ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')"
class="navbar navbar-expand-md navbar-light p-0 navbar-container" role="navigation" [attr.aria-label]="'nav.main.description' | translate">
<div class="container h-100">
<div class="navbar-inner-container w-100 h-100" [class.container]="!(isXsOrSm$ | async)">
<a class="navbar-brand my-2" routerLink="/home">
<img src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate" />
</a>
<div id="collapsingNav" class="w-100 h-100">
<ul class="navbar-nav navbar-navigation me-auto mb-2 mb-lg-0 h-100 d-flex align-items-center">
<ul class="navbar-nav me-auto mb-2 mb-lg-0 h-100">
<li *ngIf="(isXsOrSm$ | async) && (isAuthenticated$ | async)">
<ds-user-menu [inExpandableNavbar]="true"></ds-user-menu>
</li>
<ng-container *ngFor="let section of (sections | async)">
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
</ng-container>

View File

@@ -29,7 +29,7 @@ nav.navbar {
/* 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)-0.02) {
> .container {
> .navbar-inner-container {
padding: 0 var(--bs-spacer);
a.navbar-brand {
display: none;

View File

@@ -21,12 +21,3 @@
font-size: 1.1rem
}
}
header {
.navbar-navigation > li {
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
}
}