Merge remote-tracking branch 'origin/main' into #1206

This commit is contained in:
Giuseppe Digilio
2021-06-25 14:18:26 +02:00
100 changed files with 1183 additions and 596 deletions

15
SECURITY.md Normal file
View File

@@ -0,0 +1,15 @@
# Security Policy
## Supported Versions
For information regarding which versions of DSpace are currently under support, please see our DSpace Software Support Policy:
https://wiki.lyrasis.org/display/DSPACE/DSpace+Software+Support+Policy
## Reporting a Vulnerability
If you believe you have found a security vulnerability in a supported version of DSpace, we encourage you to let us know right away.
We will investigate all legitimate reports and do our best to quickly fix the problem. Please see our DSpace Software Support Policy
for information on privately reporting vulnerabilities:
https://wiki.lyrasis.org/display/DSPACE/DSpace+Software+Support+Policy

View File

@@ -1,5 +1,9 @@
# Docker Compose files
***
:warning: **NOT PRODUCTION READY** The below Docker Compose resources are not guaranteed "production ready" at this time. They have been built for development/testing only. Therefore, DSpace Docker images may not be fully secured or up-to-date. While you are welcome to base your own images on these DSpace images/resources, these should not be used "as is" in any production scenario.
***
## docker directory
- docker-compose.yml
- Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker.

View File

@@ -69,7 +69,6 @@ exports.config = {
plugins: [{
path: '../node_modules/protractor-istanbul-plugin'
}],
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
@@ -85,7 +84,7 @@ exports.config = {
onPrepare: function () {
jasmine.getEnv().addReporter(new SpecReporter({
spec: {
displayStacktrace: true
displayStacktrace: 'pretty'
}
}));
}

View File

@@ -1,11 +1,10 @@
<li class="sidebar-section">
<a class="nav-item nav-link shortcut-icon" attr.aria-labelledby="sidebarName-{{section.id}}" [title]="('menu.section.icon.' + section.id) | translate" [routerLink]="itemModel.link">
<a href="javascript:void(0);" class="nav-item nav-link shortcut-icon" attr.aria-labelledby="sidebarName-{{section.id}}" [title]="('menu.section.icon.' + section.id) | translate" [routerLink]="itemModel.link">
<i class="fas fa-{{section.icon}} fa-fw"></i>
</a>
<div class="sidebar-collapsible">
<span id="sidebarName-{{section.id}}" class="section-header-text">
<ng-container
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
<a class="nav-item nav-link" tabindex="-1" [routerLink]="itemModel.link">{{itemModel.text | translate}}</a>
</span>
</div>
</li>

View File

@@ -10,14 +10,14 @@
<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="#">
<a class="shortcut-icon navbar-brand mr-0" href="javascript:void(0);">
<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="#">
<a class="navbar-brand mr-0" href="javascript:void(0);">
<h4 class="section-header-text mb-0">{{'menu.header.admin' |
translate}}</h4>
</a>
@@ -33,7 +33,7 @@
<div class="navbar-nav">
<div class="sidebar-section" id="sidebar-collapse-toggle">
<a class="nav-item nav-link shortcut-icon"
href="#"
href="javascript:void(0);"
(click)="toggle($event)">
<i *ngIf="(menuCollapsed | async)" class="fas fa-fw fa-angle-double-right"
[title]="'menu.section.icon.pin' | translate"></i>
@@ -42,7 +42,7 @@
</a>
<div class="sidebar-collapsible">
<a class="nav-item nav-link sidebar-section"
href="#"
href="javascript:void(0);"
(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>

View File

@@ -531,14 +531,17 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
* Create menu sections dependent on whether or not the current user can manage access control groups
*/
createAccessControlMenuSections() {
this.authorizationService.isAuthorized(FeatureID.CanManageGroups).subscribe((authorized) => {
observableCombineLatest(
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
this.authorizationService.isAuthorized(FeatureID.CanManageGroups)
).subscribe(([isSiteAdmin, canManageGroups]) => {
const menuList = [
/* Access Control */
{
id: 'access_control_people',
parentID: 'access_control',
active: false,
visible: authorized,
visible: isSiteAdmin,
model: {
type: MenuItemType.LINK,
text: 'menu.section.access_control_people',
@@ -549,7 +552,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
id: 'access_control_groups',
parentID: 'access_control',
active: false,
visible: authorized,
visible: canManageGroups,
model: {
type: MenuItemType.LINK,
text: 'menu.section.access_control_groups',
@@ -571,7 +574,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
{
id: 'access_control',
active: false,
visible: authorized,
visible: canManageGroups || isSiteAdmin,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.access_control'

View File

@@ -3,12 +3,12 @@
value: ((expanded | async) ? 'endBackground' : 'startBackground'),
params: {endColor: (sidebarActiveBg | async)}}">
<div class="icon-wrapper">
<a class="nav-item nav-link shortcut-icon" attr.aria.labelledby="sidebarName-{{section.id}}" [title]="('menu.section.icon.' + section.id) | translate" (click)="toggleSection($event)" href="#">
<a class="nav-item nav-link shortcut-icon" attr.aria.labelledby="sidebarName-{{section.id}}" [title]="('menu.section.icon.' + section.id) | translate" (click)="toggleSection($event)" href="javascript:void(0);">
<i class="fas fa-{{section.icon}} fa-fw"></i>
</a>
</div>
<div class="sidebar-collapsible">
<a class="nav-item nav-link" href="#"
<a class="nav-item nav-link" href="javascript:void(0);" tabindex="-1"
(click)="toggleSection($event)">
<span id="sidebarName-{{section.id}}" class="section-header-text">
<ng-container

View File

@@ -2,15 +2,15 @@
<div class="row">
<ng-container *ngVar="(dsoRD$ | async)?.payload as dso">
<div class="col-12 pb-4">
<h2 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate}}</h2>
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p>
<h2 id="header" class="border-bottom pb-2">{{ 'collection.delete.head' | translate}}</h2>
<p class="pb-2">{{ 'collection.delete.text' | translate:{ dso: dso.name } }}</p>
<div class="form-group row">
<div class="col text-right">
<button class="btn btn-outline-secondary" (click)="onCancel(dso)">
<i class="fas fa-times"></i> {{'community.delete.cancel' | translate}}
<i class="fas fa-times"></i> {{'collection.delete.cancel' | translate}}
</button>
<button class="btn btn-danger mr-2" (click)="onConfirm(dso)">
<i class="fas fa-trash"></i> {{'community.delete.confirm' | translate}}
<i class="fas fa-trash"></i> {{'collection.delete.confirm' | translate}}
</button>
</div>
</div>

View File

@@ -20,6 +20,7 @@ import { ThemedHomePageComponent } from './themed-home-page.component';
id: 'statistics_site',
active: true,
visible: true,
index: 2,
model: {
type: MenuItemType.LINK,
text: 'menu.section.statistics',

View File

@@ -1,4 +1,4 @@
<div class="simple-view-element" [class.d-none]="content.textContent.trim().length === 0 && hasNoValue(content.querySelector('img'))">
<div class="simple-view-element" [class.d-none]="hideIfNoTextContent && content.textContent.trim().length === 0">
<h5 class="simple-view-element-header" *ngIf="label">{{ label }}</h5>
<div #content class="simple-view-element-body">
<ng-content></ng-content>

View File

@@ -1,4 +1,4 @@
import { Component } from '@angular/core';
import { Component, Input } from '@angular/core';
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { MetadataFieldWrapperComponent } from './metadata-field-wrapper.component';
@@ -6,35 +6,34 @@ import { MetadataFieldWrapperComponent } from './metadata-field-wrapper.componen
/* tslint:disable:max-classes-per-file */
@Component({
selector: 'ds-component-without-content',
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
template: '<ds-metadata-field-wrapper [hideIfNoTextContent]="hideIfNoTextContent" [label]="\'test label\'">\n' +
'</ds-metadata-field-wrapper>'
})
class NoContentComponent {}
class NoContentComponent {
public hideIfNoTextContent = true;
}
@Component({
selector: 'ds-component-with-empty-spans',
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
template: '<ds-metadata-field-wrapper [hideIfNoTextContent]="hideIfNoTextContent" [label]="\'test label\'">\n' +
' <span></span>\n' +
' <span></span>\n' +
'</ds-metadata-field-wrapper>'
})
class SpanContentComponent {}
class SpanContentComponent {
@Input() hideIfNoTextContent = true;
}
@Component({
selector: 'ds-component-with-text',
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
template: '<ds-metadata-field-wrapper [hideIfNoTextContent]="hideIfNoTextContent" [label]="\'test label\'">\n' +
' <span>The quick brown fox jumps over the lazy dog</span>\n' +
'</ds-metadata-field-wrapper>'
})
class TextContentComponent {}
class TextContentComponent {
@Input() hideIfNoTextContent = true;
}
@Component({
selector: 'ds-component-with-image',
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
' <img src="https://some/image.png" alt="an alt text">\n' +
'</ds-metadata-field-wrapper>'
})
class ImgContentComponent {}
/* tslint:enable:max-classes-per-file */
describe('MetadataFieldWrapperComponent', () => {
@@ -43,7 +42,7 @@ describe('MetadataFieldWrapperComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [MetadataFieldWrapperComponent, NoContentComponent, SpanContentComponent, TextContentComponent, ImgContentComponent]
declarations: [MetadataFieldWrapperComponent, NoContentComponent, SpanContentComponent, TextContentComponent]
}).compileComponents();
}));
@@ -58,6 +57,7 @@ describe('MetadataFieldWrapperComponent', () => {
expect(component).toBeDefined();
});
describe('with hideIfNoTextContent=true', () => {
it('should not show the component when there is no content', () => {
const parentFixture = TestBed.createComponent(NoContentComponent);
parentFixture.detectChanges();
@@ -66,7 +66,7 @@ describe('MetadataFieldWrapperComponent', () => {
expect(nativeWrapper.classList.contains('d-none')).toBe(true);
});
it('should not show the component when there is DOM content but not text or an image', () => {
it('should not show the component when there is no text content', () => {
const parentFixture = TestBed.createComponent(SpanContentComponent);
parentFixture.detectChanges();
const parentNative = parentFixture.nativeElement;
@@ -82,14 +82,35 @@ describe('MetadataFieldWrapperComponent', () => {
parentFixture.detectChanges();
expect(nativeWrapper.classList.contains('d-none')).toBe(false);
});
});
it('should show the component when there is img content', () => {
const parentFixture = TestBed.createComponent(ImgContentComponent);
describe('with hideIfNoTextContent=false', () => {
it('should show the component when there is no content', () => {
const parentFixture = TestBed.createComponent(NoContentComponent);
parentFixture.componentInstance.hideIfNoTextContent = false;
parentFixture.detectChanges();
const parentNative = parentFixture.nativeElement;
const nativeWrapper = parentNative.querySelector(wrapperSelector);
expect(nativeWrapper.classList.contains('d-none')).toBe(false);
});
it('should show the component when there is no text content', () => {
const parentFixture = TestBed.createComponent(SpanContentComponent);
parentFixture.componentInstance.hideIfNoTextContent = false;
parentFixture.detectChanges();
const parentNative = parentFixture.nativeElement;
const nativeWrapper = parentNative.querySelector(wrapperSelector);
expect(nativeWrapper.classList.contains('d-none')).toBe(false);
});
it('should show the component when there is text content', () => {
const parentFixture = TestBed.createComponent(TextContentComponent);
parentFixture.componentInstance.hideIfNoTextContent = false;
parentFixture.detectChanges();
const parentNative = parentFixture.nativeElement;
const nativeWrapper = parentNative.querySelector(wrapperSelector);
parentFixture.detectChanges();
expect(nativeWrapper.classList.contains('d-none')).toBe(false);
});
});
});

View File

@@ -1,5 +1,4 @@
import { Component, Input } from '@angular/core';
import { hasNoValue } from '../../../shared/empty.util';
/**
* This component renders any content inside this wrapper.
@@ -17,10 +16,5 @@ export class MetadataFieldWrapperComponent {
*/
@Input() label: string;
/**
* Make hasNoValue() available in the template
*/
hasNoValue(o: any): boolean {
return hasNoValue(o);
}
@Input() hideIfNoTextContent = true;
}

View File

@@ -11,7 +11,7 @@
[retainScrollPosition]="true">
<div class="file-section row" *ngFor="let file of originals?.page;">
<div class="file-section row mb-3" *ngFor="let file of originals?.page;">
<div class="col-3">
<ds-thumbnail [thumbnail]="(file.thumbnail | async)?.payload"></ds-thumbnail>
</div>

View File

@@ -9,8 +9,8 @@
<div class="row">
<div class="col-xs-12 col-md-4">
<ng-container *ngIf="!mediaViewer.image">
<ds-metadata-field-wrapper>
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail [thumbnail]="thumbnail$ | async"></ds-thumbnail>
</ds-metadata-field-wrapper>
</ng-container>
<ng-container *ngIf="mediaViewer.image">

View File

@@ -1,11 +1,12 @@
import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from '../../../../../environments/environment';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
import { Bitstream } from '../../../../core/shared/bitstream.model';
import { Item } from '../../../../core/shared/item.model';
import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
import { takeUntilCompletedRemoteData } from '../../../../core/shared/operators';
import { getItemPageRoute } from '../../../item-page-routing-paths';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { RemoteData } from '../../../../core/data/remote-data';
@Component({
selector: 'ds-item',
@@ -17,6 +18,11 @@ import { getItemPageRoute } from '../../../item-page-routing-paths';
export class ItemComponent implements OnInit {
@Input() object: Item;
/**
* The Item's thumbnail
*/
thumbnail$: BehaviorSubject<RemoteData<Bitstream>>;
/**
* Route to the item page
*/
@@ -28,12 +34,12 @@ export class ItemComponent implements OnInit {
ngOnInit(): void {
this.itemPageRoute = getItemPageRoute(this.object);
}
// TODO refactor to return RemoteData, and thumbnail template to deal with loading
getThumbnail(): Observable<Bitstream> {
return this.bitstreamDataService.getThumbnailFor(this.object).pipe(
getFirstSucceededRemoteDataPayload()
);
this.thumbnail$ = new BehaviorSubject<RemoteData<Bitstream>>(undefined);
this.bitstreamDataService.getThumbnailFor(this.object).pipe(
takeUntilCompletedRemoteData(),
).subscribe((rd: RemoteData<Bitstream>) => {
this.thumbnail$.next(rd);
});
}
}

View File

@@ -9,8 +9,8 @@
<div class="row">
<div class="col-xs-12 col-md-4">
<ng-container *ngIf="!mediaViewer.image">
<ds-metadata-field-wrapper>
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail [thumbnail]="thumbnail$ | async"></ds-thumbnail>
</ds-metadata-field-wrapper>
</ng-container>
<ng-container *ngIf="mediaViewer.image">

View File

@@ -3,6 +3,10 @@ import { getAccessControlModuleRoute } from '../app-routing-paths';
export const GROUP_EDIT_PATH = 'groups';
export function getGroupsRoute() {
return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH).toString();
}
export function getGroupEditRoute(id: string) {
return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH, id).toString();
}

View File

@@ -5,6 +5,9 @@ import { GroupFormComponent } from './group-registry/group-form/group-form.compo
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
import { GROUP_EDIT_PATH } from './access-control-routing-paths';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { GroupPageGuard } from './group-registry/group-page.guard';
import { GroupAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
@NgModule({
imports: [
@@ -15,7 +18,8 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
resolve: {
breadcrumb: I18nBreadcrumbResolver
},
data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' }
data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' },
canActivate: [SiteAdministratorGuard]
},
{
path: GROUP_EDIT_PATH,
@@ -23,7 +27,8 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
resolve: {
breadcrumb: I18nBreadcrumbResolver
},
data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' }
data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' },
canActivate: [GroupAdministratorGuard]
},
{
path: `${GROUP_EDIT_PATH}/newGroup`,
@@ -31,7 +36,8 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
resolve: {
breadcrumb: I18nBreadcrumbResolver
},
data: { title: 'admin.access-control.groups.title.addGroup', breadcrumbKey: 'admin.access-control.groups.addGroup' }
data: { title: 'admin.access-control.groups.title.addGroup', breadcrumbKey: 'admin.access-control.groups.addGroup' },
canActivate: [GroupAdministratorGuard]
},
{
path: `${GROUP_EDIT_PATH}/:groupId`,
@@ -39,7 +45,8 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
resolve: {
breadcrumb: I18nBreadcrumbResolver
},
data: { title: 'admin.access-control.groups.title.singleGroup', breadcrumbKey: 'admin.access-control.groups.singleGroup' }
data: { title: 'admin.access-control.groups.title.singleGroup', breadcrumbKey: 'admin.access-control.groups.singleGroup' },
canActivate: [GroupPageGuard]
}
])
]

View File

@@ -0,0 +1,83 @@
import { GroupPageGuard } from './group-page.guard';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { ActivatedRouteSnapshot, Router } from '@angular/router';
import { of as observableOf } from 'rxjs';
import { AuthService } from '../../core/auth/auth.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
describe('GroupPageGuard', () => {
const groupsEndpointUrl = 'https://test.org/api/eperson/groups';
const groupUuid = '0d6f89df-f95a-4829-943c-f21f434fb892';
const groupEndpointUrl = `${groupsEndpointUrl}/${groupUuid}`;
const routeSnapshotWithGroupId = {
params: {
groupId: groupUuid,
}
} as unknown as ActivatedRouteSnapshot;
let guard: GroupPageGuard;
let halEndpointService: HALEndpointService;
let authorizationService: AuthorizationDataService;
let router: Router;
let authService: AuthService;
beforeEach(() => {
halEndpointService = jasmine.createSpyObj(['getEndpoint']);
(halEndpointService as any).getEndpoint.and.returnValue(observableOf(groupsEndpointUrl));
authorizationService = jasmine.createSpyObj(['isAuthorized']);
// NOTE: value is set in beforeEach
router = jasmine.createSpyObj(['parseUrl']);
(router as any).parseUrl.and.returnValue = {};
authService = jasmine.createSpyObj(['isAuthenticated']);
(authService as any).isAuthenticated.and.returnValue(observableOf(true));
guard = new GroupPageGuard(halEndpointService, authorizationService, router, authService);
});
it('should be created', () => {
expect(guard).toBeTruthy();
});
describe('canActivate', () => {
describe('when the current user can manage the group', () => {
beforeEach(() => {
(authorizationService as any).isAuthorized.and.returnValue(observableOf(true));
});
it('should return true', (done) => {
guard.canActivate(
routeSnapshotWithGroupId, { url: 'current-url'} as any
).subscribe((result) => {
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
FeatureID.CanManageGroup, groupEndpointUrl, undefined
);
expect(result).toBeTrue();
done();
});
});
});
describe('when the current user can not manage the group', () => {
beforeEach(() => {
(authorizationService as any).isAuthorized.and.returnValue(observableOf(false));
});
it('should not return true', (done) => {
guard.canActivate(
routeSnapshotWithGroupId, { url: 'current-url'} as any
).subscribe((result) => {
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
FeatureID.CanManageGroup, groupEndpointUrl, undefined
);
expect(result).not.toBeTrue();
done();
});
});
});
});
});

View File

@@ -0,0 +1,35 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { Observable, of as observableOf } from 'rxjs';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { AuthService } from '../../core/auth/auth.service';
import { SomeFeatureAuthorizationGuard } from '../../core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class GroupPageGuard extends SomeFeatureAuthorizationGuard {
protected groupsEndpoint = 'groups';
constructor(protected halEndpointService: HALEndpointService,
protected authorizationService: AuthorizationDataService,
protected router: Router,
protected authService: AuthService) {
super(authorizationService, router, authService);
}
getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID[]> {
return observableOf([FeatureID.CanManageGroup]);
}
getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
return this.halEndpointService.getEndpoint(this.groupsEndpoint).pipe(
map(groupsUrl => `${groupsUrl}/${route?.params?.groupId}`)
);
}
}

View File

@@ -33,9 +33,9 @@
</div>
</form>
<ds-loading *ngIf="searching$ | async"></ds-loading>
<ds-loading *ngIf="loading$ | async"></ds-loading>
<ds-pagination
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(searching$ | async)"
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(loading$ | async)"
[paginationOptions]="config"
[pageInfoState]="pageInfoState$"
[collectionSize]="(pageInfoState$ | async)?.totalElements"
@@ -59,11 +59,23 @@
<td>{{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}</td>
<td>
<div class="btn-group edit-field">
<button [routerLink]="groupService.getGroupEditPageRouterLink(groupDto.group)"
class="btn btn-outline-primary btn-sm"
title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: groupDto.group.name} }}">
<ng-container [ngSwitch]="groupDto.ableToEdit">
<button *ngSwitchCase="true"
[routerLink]="groupService.getGroupEditPageRouterLink(groupDto.group)"
class="btn btn-outline-primary btn-sm btn-edit"
title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: groupDto.group.name} }}"
>
<i class="fas fa-edit fa-fw"></i>
</button>
<button *ngSwitchCase="false"
[disabled]="true"
class="btn btn-outline-primary btn-sm btn-edit"
placement="left"
[ngbTooltip]="'admin.access-control.epeople.table.edit.buttons.edit-disabled' | translate"
>
<i class="fas fa-edit fa-fw"></i>
</button>
</ng-container>
<button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete"
(click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm"
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: groupDto.group.name} }}">

View File

@@ -30,6 +30,7 @@ import { routeServiceStub } from '../../shared/testing/route-service.stub';
import { RouterMock } from '../../shared/mocks/router.mock';
import { PaginationService } from '../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
describe('GroupRegistryComponent', () => {
let component: GroupsRegistryComponent;
@@ -43,6 +44,26 @@ describe('GroupRegistryComponent', () => {
let mockEPeople;
let paginationService;
/**
* Set authorizationService.isAuthorized to return the following values.
* @param isAdmin whether or not the current user is an admin.
* @param canManageGroup whether or not the current user can manage all groups.
*/
const setIsAuthorized = (isAdmin: boolean, canManageGroup: boolean) => {
(authorizationService as any).isAuthorized.and.callFake((featureId?: FeatureID) => {
switch (featureId) {
case FeatureID.AdministratorOf:
return observableOf(isAdmin);
case FeatureID.CanManageGroup:
return observableOf(canManageGroup);
case FeatureID.CanDelete:
return observableOf(true);
default:
throw new Error(`setIsAuthorized: this fake implementation does not support ${featureId}.`);
}
});
};
beforeEach(waitForAsync(() => {
mockGroups = [GroupMock, GroupMock2];
mockEPeople = [EPersonMock, EPersonMock2];
@@ -131,9 +152,8 @@ describe('GroupRegistryComponent', () => {
return createSuccessfulRemoteDataObject$(undefined);
}
};
authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true)
});
authorizationService = jasmine.createSpyObj('authorizationService', ['isAuthorized']);
setIsAuthorized(true, true);
paginationService = new PaginationServiceStub();
TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
@@ -180,6 +200,81 @@ describe('GroupRegistryComponent', () => {
});
});
describe('edit buttons', () => {
describe('when the user is a general admin', () => {
beforeEach(fakeAsync(() => {
// NOTE: setting canManageGroup to false should not matter, since isAdmin takes priority
setIsAuthorized(true, false);
// force rerender after setup changes
component.search({ query: '' });
tick();
fixture.detectChanges();
}));
it('should be active', () => {
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(4) button.btn-edit'));
expect(editButtonsFound.length).toEqual(2);
editButtonsFound.forEach((editButtonFound) => {
expect(editButtonFound.nativeElement.disabled).toBeFalse();
});
});
it('should not check the canManageGroup permissions', () => {
expect(authorizationService.isAuthorized).not.toHaveBeenCalledWith(
FeatureID.CanManageGroup, mockGroups[0].self
);
expect(authorizationService.isAuthorized).not.toHaveBeenCalledWith(
FeatureID.CanManageGroup, mockGroups[0].self, undefined // treated differently
);
expect(authorizationService.isAuthorized).not.toHaveBeenCalledWith(
FeatureID.CanManageGroup, mockGroups[1].self
);
expect(authorizationService.isAuthorized).not.toHaveBeenCalledWith(
FeatureID.CanManageGroup, mockGroups[1].self, undefined // treated differently
);
});
});
describe('when the user can edit the groups', () => {
beforeEach(fakeAsync(() => {
setIsAuthorized(false, true);
// force rerender after setup changes
component.search({ query: '' });
tick();
fixture.detectChanges();
}));
it('should be active', () => {
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(4) button.btn-edit'));
expect(editButtonsFound.length).toEqual(2);
editButtonsFound.forEach((editButtonFound) => {
expect(editButtonFound.nativeElement.disabled).toBeFalse();
});
});
});
describe('when the user can not edit the groups', () => {
beforeEach(fakeAsync(() => {
setIsAuthorized(false, false);
// force rerender after setup changes
component.search({ query: '' });
tick();
fixture.detectChanges();
}));
it('should not be active', () => {
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(4) button.btn-edit'));
expect(editButtonsFound.length).toEqual(2);
editButtonsFound.forEach((editButtonFound) => {
expect(editButtonFound.nativeElement.disabled).toBeTrue();
});
});
});
});
describe('search', () => {
describe('when searching with query', () => {
let groupIdsFound;

View File

@@ -9,7 +9,7 @@ import {
of as observableOf,
Subscription
} from 'rxjs';
import { catchError, map, switchMap, take } from 'rxjs/operators';
import { catchError, map, switchMap, take, tap } from 'rxjs/operators';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
@@ -75,7 +75,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
/**
* A boolean representing if a search is pending
*/
searching$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
// Current search in groups registry
currentSearchQuery: string;
@@ -118,12 +118,12 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
* @param data Contains query param
*/
search(data: any) {
this.searching$.next(true);
if (hasValue(this.searchSub)) {
this.searchSub.unsubscribe();
this.subs = this.subs.filter((sub: Subscription) => sub !== this.searchSub);
}
this.searchSub = this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
tap(() => this.loading$.next(true)),
switchMap((paginationOptions) => {
const query: string = data.query;
if (query != null && this.currentSearchQuery !== query) {
@@ -141,18 +141,22 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
if (groups.page.length === 0) {
return observableOf(buildPaginatedList(groups.pageInfo, []));
}
return this.authorizationService.isAuthorized(FeatureID.AdministratorOf).pipe(
switchMap((isSiteAdmin: boolean) => {
return observableCombineLatest(groups.page.map((group: Group) => {
if (!this.deletedGroupsIds.includes(group.id)) {
if (hasValue(group) && !this.deletedGroupsIds.includes(group.id)) {
return observableCombineLatest([
this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined),
this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self),
this.canManageGroup$(isSiteAdmin, group),
this.hasLinkedDSO(group),
this.getSubgroups(group),
this.getMembers(group)
]).pipe(
map(([isAuthorized, hasLinkedDSO, subgroups, members]:
[boolean, boolean, RemoteData<PaginatedList<Group>>, RemoteData<PaginatedList<EPerson>>]) => {
map(([canDelete, canManageGroup, hasLinkedDSO, subgroups, members]:
[boolean, boolean, boolean, RemoteData<PaginatedList<Group>>, RemoteData<PaginatedList<EPerson>>]) => {
const groupDtoModel: GroupDtoModel = new GroupDtoModel();
groupDtoModel.ableToDelete = isAuthorized && !hasLinkedDSO;
groupDtoModel.ableToDelete = canDelete && !hasLinkedDSO;
groupDtoModel.ableToEdit = canManageGroup;
groupDtoModel.group = group;
groupDtoModel.subgroups = subgroups.payload;
groupDtoModel.epersons = members.payload;
@@ -165,15 +169,25 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
return buildPaginatedList(groups.pageInfo, dtos);
}));
})
);
})
).subscribe((value: PaginatedList<GroupDtoModel>) => {
this.groupsDto$.next(value);
this.pageInfoState$.next(value.pageInfo);
this.searching$.next(false);
this.loading$.next(false);
});
this.subs.push(this.searchSub);
}
canManageGroup$(isSiteAdmin: boolean, group: Group): Observable<boolean> {
if (isSiteAdmin) {
return observableOf(true);
} else {
return this.authorizationService.isAuthorized(FeatureID.CanManageGroup, group.self);
}
}
/**
* Delete Group
*/

View File

@@ -46,6 +46,7 @@ import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component
import { ThemedHeaderComponent } from './header/themed-header.component';
import { ThemedFooterComponent } from './footer/themed-footer.component';
import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component';
import { ThemedHeaderNavbarWrapperComponent } from './header-nav-wrapper/themed-header-navbar-wrapper.component';
export function getBase() {
return environment.ui.nameSpace;
@@ -129,6 +130,7 @@ const DECLARATIONS = [
HeaderComponent,
ThemedHeaderComponent,
HeaderNavbarWrapperComponent,
ThemedHeaderNavbarWrapperComponent,
AdminSidebarComponent,
AdminSidebarSectionComponent,
ExpandableAdminSidebarSectionComponent,

View File

@@ -10,6 +10,7 @@ export enum FeatureID {
ReinstateItem = 'reinstateItem',
EPersonRegistration = 'epersonRegistration',
CanManageGroups = 'canManageGroups',
CanManageGroup = 'canManageGroup',
IsCollectionAdmin = 'isCollectionAdmin',
IsCommunityAdmin = 'isCommunityAdmin',
CanDownload = 'canDownload',

View File

@@ -579,4 +579,19 @@ describe('RequestService', () => {
});
});
});
describe('uriEncodeBody', () => {
it('should properly encode the body', () => {
const body = {
'property1': 'multiple\nlines\nto\nsend',
'property2': 'sp&ci@l characters',
'sp&ci@l-chars in prop': 'test123',
};
const queryParams = service.uriEncodeBody(body);
expect(queryParams).toEqual(
'property1=multiple%0Alines%0Ato%0Asend&property2=sp%26ci%40l%20characters&sp%26ci%40l-chars%20in%20prop=test123'
);
});
});
});

View File

@@ -265,11 +265,13 @@ export class RequestService {
if (isNotEmpty(body) && typeof body === 'object') {
Object.keys(body)
.forEach((param) => {
const paramValue = `${param}=${body[param]}`;
const encodedParam = encodeURIComponent(param);
const encodedBody = encodeURIComponent(body[param]);
const paramValue = `${encodedParam}=${encodedBody}`;
queryParams = isEmpty(queryParams) ? queryParams.concat(paramValue) : queryParams.concat('&', paramValue);
});
}
return encodeURI(queryParams);
return queryParams;
}
/**

View File

@@ -17,6 +17,11 @@ export class GroupDtoModel {
*/
public ableToDelete: boolean;
/**
* Whether or not the current user is able to edit the linked group
*/
public ableToEdit: boolean;
/**
* List of subgroups of this group
*/

View File

@@ -8,14 +8,14 @@
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
class="card-img-top full-width">
<div>
<ds-grid-thumbnail [thumbnail]="getThumbnail() | async">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
</ds-thumbnail>
</div>
</a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
<div>
<ds-grid-thumbnail [thumbnail]="getThumbnail() | async">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
</ds-thumbnail>
</div>
</span>
<div class="card-body">

View File

@@ -8,14 +8,14 @@
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
class="card-img-top full-width">
<div>
<ds-grid-thumbnail [thumbnail]="getThumbnail() | async">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
</ds-thumbnail>
</div>
</a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
<div>
<ds-grid-thumbnail [thumbnail]="getThumbnail() | async">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
</ds-thumbnail>
</div>
</span>
<div class="card-body">

View File

@@ -8,14 +8,14 @@
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
class="card-img-top full-width">
<div>
<ds-grid-thumbnail [thumbnail]="getThumbnail() | async">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
</ds-thumbnail>
</div>
</a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
<div>
<ds-grid-thumbnail [thumbnail]="getThumbnail() | async">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
</ds-thumbnail>
</div>
</span>
<div class="card-body">

View File

@@ -8,8 +8,8 @@
</div>
<div class="row">
<div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper>
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail [thumbnail]="thumbnail$ | async"></ds-thumbnail>
</ds-metadata-field-wrapper>
<ds-generic-item-page-field [item]="object"
[fields]="['publicationvolume.volumeNumber']"

View File

@@ -8,8 +8,8 @@
</div>
<div class="row">
<div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper>
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail [thumbnail]="thumbnail$ | async"></ds-thumbnail>
</ds-metadata-field-wrapper>
<ds-generic-item-page-field [item]="object"
[fields]="['publicationvolume.volumeNumber']"

View File

@@ -8,8 +8,8 @@
</div>
<div class="row">
<div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper>
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail [thumbnail]="thumbnail$ | async"></ds-thumbnail>
</ds-metadata-field-wrapper>
<ds-generic-item-page-field class="item-page-fields" [item]="object"
[fields]="['creativeworkseries.issn']"

View File

@@ -8,14 +8,14 @@
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
class="card-img-top full-width">
<div>
<ds-grid-thumbnail [thumbnail]="getThumbnail() | async">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
</ds-thumbnail>
</div>
</a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
<div>
<ds-grid-thumbnail [thumbnail]="getThumbnail() | async">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
</ds-thumbnail>
</div>
</span>
<div class="card-body">

View File

@@ -8,14 +8,14 @@
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
class="card-img-top full-width">
<div>
<ds-grid-thumbnail [thumbnail]="getThumbnail() | async">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
</ds-thumbnail>
</div>
</a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
<div>
<ds-grid-thumbnail [thumbnail]="getThumbnail() | async">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
</ds-thumbnail>
</div>
</span>
<div class="card-body">

View File

@@ -8,14 +8,14 @@
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
class="card-img-top full-width">
<div>
<ds-grid-thumbnail [thumbnail]="getThumbnail() | async">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
</ds-thumbnail>
</div>
</a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
<div>
<ds-grid-thumbnail [thumbnail]="getThumbnail() | async">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
</ds-thumbnail>
</div>
</span>
<div class="card-body">

View File

@@ -8,8 +8,13 @@
</div>
<div class="row">
<div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper>
<ds-thumbnail [thumbnail]="getThumbnail() | async" [defaultImage]="'assets/images/orgunit-placeholder.svg'"></ds-thumbnail>
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail [thumbnail]="thumbnail$ | async"
[defaultImage]="'assets/images/orgunit-placeholder.svg'"
[alt]="'thumbnail.orgunit.alt'"
[placeholder]="'thumbnail.orgunit.placeholder'"
>
</ds-thumbnail>
</ds-metadata-field-wrapper>
<ds-generic-item-page-field [item]="object"
[fields]="['organization.foundingDate']"

View File

@@ -8,8 +8,12 @@
</div>
<div class="row">
<div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper>
<ds-thumbnail [thumbnail]="getThumbnail() | async" [defaultImage]="'assets/images/person-placeholder.svg'"></ds-thumbnail>
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail [thumbnail]="thumbnail$ | async"
[defaultImage]="'assets/images/person-placeholder.svg'"
[alt]="'thumbnail.person.alt'"
[placeholder]="'thumbnail.person.placeholder'">
</ds-thumbnail>
</ds-metadata-field-wrapper>
<ds-generic-item-page-field [item]="object"
[fields]="['person.email']"

View File

@@ -8,8 +8,13 @@
</div>
<div class="row">
<div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper>
<ds-thumbnail [thumbnail]="getThumbnail() | async" [defaultImage]="'assets/images/project-placeholder.svg'"></ds-thumbnail>
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail
[thumbnail]="thumbnail$ | async"
[defaultImage]="'assets/images/project-placeholder.svg'"
[alt]="'thumbnail.project.alt'"
[placeholder]="'thumbnail.project.placeholder'">
</ds-thumbnail>
</ds-metadata-field-wrapper>
<!--<ds-generic-item-page-field [item]="object"-->
<!--[fields]="['project.identifier.status']"-->

View File

@@ -1,6 +1,7 @@
<footer class="top-footer text-lg-start">
<footer class="text-lg-start">
<div *ngIf="showTopFooter" class="top-footer">
<!-- Grid container -->
<div *ngIf="showTopFooter" class="container p-4">
<div class=" container p-4">
<!--Grid row-->
<div class="row">
@@ -48,25 +49,31 @@
</div>
<!--Grid row-->
</div>
</div>
<!-- Grid container -->
<!-- Copyright -->
<div class="footer p-1 d-flex justify-content-center align-items-center text-white">
<div class="bottom-footer p-1 d-flex justify-content-center align-items-center text-white">
<div class="content-container">
<p class="m-0">
<a class="text-white" href="http://www.dspace.org/">{{ 'footer.link.dspace' | translate}}</a>
<a class="text-white"
href="http://www.dspace.org/">{{ 'footer.link.dspace' | translate}}</a>
{{ 'footer.copyright' | translate:{year: dateObj | date:'y'} }}
<a class="text-white" href="https://www.lyrasis.org/">{{ 'footer.link.lyrasis' | translate}}</a>
<a class="text-white"
href="https://www.lyrasis.org/">{{ 'footer.link.lyrasis' | translate}}</a>
</p>
<ul class="footer-info list-unstyled small d-flex justify-content-center mb-0">
<li>
<a class="text-white" href="#" (click)="showCookieSettings()">{{ 'footer.link.cookies' | translate}}</a>
<a class="text-white" href="#"
(click)="showCookieSettings()">{{ 'footer.link.cookies' | translate}}</a>
</li>
<li>
<a class="text-white" routerLink="info/privacy">{{ 'footer.link.privacy-policy' | translate}}</a>
<a class="text-white"
routerLink="info/privacy">{{ 'footer.link.privacy-policy' | translate}}</a>
</li>
<li>
<a class="text-white" routerLink="info/end-user-agreement">{{ 'footer.link.end-user-agreement' | translate}}</a>
<a class="text-white"
routerLink="info/end-user-agreement">{{ 'footer.link.end-user-agreement' | translate}}</a>
</li>
</ul>
</div>

View File

@@ -1,11 +1,10 @@
:host {
.top-footer {
background-color: var(--ds-top-footer-bg);
border-top: var(--ds-footer-border);
footer {
background-color: var(--ds-footer-bg);
text-align: center;
padding: var(--ds-footer-padding);
z-index: var(--ds-footer-z-index);
border-top: var(--ds-footer-border);
padding: var(--ds-footer-padding);
p {
margin: 0;
@@ -15,12 +14,18 @@
height: var(--ds-footer-logo-height);
}
.footer {
background-color: var(--ds-footer-bg);
.top-footer {
background-color: var(--ds-top-footer-bg);
padding: var(--ds-footer-padding);
margin: calc(var(--ds-footer-padding) * -1);
}
.bottom-footer {
ul {
li {
display: inline-flex;
a {
padding: 0 calc(var(--bs-spacer) / 2);
color: inherit

View File

@@ -1,3 +1,4 @@
<div [ngClass]="{'open': !(isNavBarCollapsed | async)}">
<ds-themed-header></ds-themed-header>
<ds-themed-navbar></ds-themed-navbar>
</div>

View File

@@ -5,7 +5,3 @@
position: sticky;
}
}
:host {
z-index: var(--ds-nav-z-index);
}

View File

@@ -0,0 +1,3 @@
:host {
z-index: var(--ds-nav-z-index);
}

View File

@@ -0,0 +1,25 @@
import { Component } from '@angular/core';
import { ThemedComponent } from '../shared/theme-support/themed.component';
import { HeaderNavbarWrapperComponent } from './header-navbar-wrapper.component';
/**
* Themed wrapper for BreadcrumbsComponent
*/
@Component({
selector: 'ds-themed-header-navbar-wrapper',
styleUrls: ['./themed-header-navbar-wrapper.component.scss'],
templateUrl: '../shared/theme-support/themed.component.html',
})
export class ThemedHeaderNavbarWrapperComponent extends ThemedComponent<HeaderNavbarWrapperComponent> {
protected getComponentName(): string {
return 'HeaderNavbarWrapperComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../themes/${themeName}/app/header-nav-wrapper/header-navbar-wrapper.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./header-navbar-wrapper.component`);
}
}

View File

@@ -1,24 +1,23 @@
<header class="header">
<nav role="navigation" [attr.aria-label]="'nav.user.description' |translate" class="container navbar navbar-expand-md px-0">
<div class="d-flex flex-grow-1">
<a class="navbar-brand m-2" routerLink="/home">
<img src="assets/images/dspace-logo.svg" alt="logo"/>
<header>
<div class="container">
<div class="d-flex flex-row justify-content-between">
<a class="navbar-brand my-2" routerLink="/home">
<img src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate"/>
</a>
</div>
<div class="d-flex flex-grow-1 ml-auto justify-content-end align-items-center">
<ds-search-navbar class="navbar-search"></ds-search-navbar>
<nav role="navigation" [attr.aria-label]="'nav.user.description' | translate" class="navbar navbar-light navbar-expand-md flex-shrink-0 px-0">
<ds-search-navbar></ds-search-navbar>
<ds-lang-switch></ds-lang-switch>
<ds-auth-nav-menu></ds-auth-nav-menu>
<ds-impersonate-navbar></ds-impersonate-navbar>
<div class="pl-2">
<button class="navbar-toggler" type="button" (click)="toggleNavbar()"
aria-controls="collapsingNav"
aria-expanded="false" aria-label="Toggle navigation">
aria-expanded="false" [attr.aria-label]="'nav.toggle' | translate">
<span class="navbar-toggler-icon fas fa-bars fa-fw" aria-hidden="true"></span>
</button>
</div>
</div>
</nav>
<ds-themed-navbar></ds-themed-navbar>
</div>
</div>
</header>

View File

@@ -1,19 +1,23 @@
@media screen and (min-width: map-get($grid-breakpoints, md)) {
nav.navbar {
display: none;
}
.header {
background-color: var(--ds-header-bg);
.navbar-brand img {
max-height: var(--ds-header-logo-height);
max-width: 100%;
@media screen and (max-width: map-get($grid-breakpoints, sm)) {
max-height: var(--ds-header-logo-height-xs);
}
}
.navbar-brand img {
@media screen and (max-width: map-get($grid-breakpoints, md)) {
height: var(--ds-header-logo-height-xs);
}
}
.navbar-toggler .navbar-toggler-icon {
background-image: none !important;
line-height: 1.5;
color: var(--bs-link-color);
}
.navbar ::ng-deep {
a {
color: var(--ds-header-icon-color);
&:hover, &focus {
color: var(--ds-header-icon-color-hover);
}
}
}

View File

@@ -1,4 +1,4 @@
<li class="nav-item dropdown h-100 d-flex flex-column justify-content-center"
<li class="nav-item dropdown"
(mouseenter)="activateSection($event)"
(mouseleave)="deactivateSection($event)">
<a href="#" class="nav-link dropdown-toggle" routerLinkActive="active"

View File

@@ -1,4 +1,4 @@
<li class="nav-item h-100 d-flex flex-column justify-content-center">
<li class="nav-item">
<ng-container
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
</li>

View File

@@ -1,24 +1,18 @@
<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" role="navigation" [attr.aria-label]="'nav.main.description' |translate">
<div class="container h-100">
<a class="navbar-brand my-2" routerLink="/home">
<img src="assets/images/dspace-logo.svg" alt="logo"/>
</a>
<div id="collapsingNav" class="w-100 h-100">
<ul class="navbar-nav me-auto mb-2 mb-lg-0 h-100">
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="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="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
</ng-container>
</ul>
</div>
<ds-search-navbar class="navbar-collapsed"></ds-search-navbar>
<ds-lang-switch class="navbar-collapsed"></ds-lang-switch>
<ds-auth-nav-menu class="navbar-collapsed"></ds-auth-nav-menu>
<ds-impersonate-navbar class="navbar-collapsed"></ds-impersonate-navbar>
</div>
</div>
</nav>

View File

@@ -1,14 +1,12 @@
nav.navbar {
border-top: 1px var(--ds-header-navbar-border-top-color) solid;
border-bottom: 1px var(--ds-header-navbar-border-bottom-color) solid;
border-bottom: 1px var(--bs-gray-400) solid;
align-items: baseline;
color: var(--ds-header-icon-color);
}
/** Mobile menu styling **/
@media screen and (max-width: map-get($grid-breakpoints, md)) {
.navbar {
width: 100%;
width: 100vw;
background-color: var(--bs-white);
position: absolute;
overflow: hidden;
@@ -31,20 +29,9 @@ nav.navbar {
@media screen and (max-width: map-get($grid-breakpoints, md)) {
> .container {
padding: 0 var(--bs-spacer);
a.navbar-brand {
display: none;
}
.navbar-collapsed {
display: none;
}
}
padding: 0;
}
height: 80px;
}
a.navbar-brand img {
max-height: var(--ds-header-logo-height);
}
.navbar-nav {

View File

@@ -46,6 +46,7 @@ export class NavbarComponent extends MenuComponent {
id: `browse_global_communities_and_collections`,
active: false,
visible: true,
index: 0,
model: {
type: MenuItemType.LINK,
text: `menu.section.browse_global_communities_and_collections`,
@@ -57,11 +58,11 @@ export class NavbarComponent extends MenuComponent {
id: 'browse_global',
active: false,
visible: true,
index: 1,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.browse_global'
} as TextMenuItemModel,
index: 0
},
];
// Read the different Browse-By types from config and add them to the browse menu

View File

@@ -4,7 +4,7 @@
value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'),
params: {collapsedSidebarWidth: (collapsedSidebarWidth | async), totalSidebarWidth: (totalSidebarWidth | async)}
}">
<ds-header-navbar-wrapper></ds-header-navbar-wrapper>
<ds-themed-header-navbar-wrapper></ds-themed-header-navbar-wrapper>
<ds-notifications-board
[options]="notificationOptions">

View File

@@ -1 +1 @@
<a class="nav-item nav-link" [ngClass]="{'disabled': !hasLink}" [routerLink]="getRouterLink()">{{item.text | translate}}</a>
<a href="javascript:void(0);" class="nav-item nav-link" [ngClass]="{'disabled': !hasLink}" [routerLink]="getRouterLink()">{{item.text | translate}}</a>

View File

@@ -1 +1 @@
<a class="nav-item nav-link" role="button" (click)="item.function()">{{item.text | translate}}</a>
<a href="javascript:void(0);" class="nav-item nav-link" role="button" (click)="item.function()">{{item.text | translate}}</a>

View File

@@ -3,6 +3,7 @@ import { TranslateService } from '@ngx-translate/core';
export function getMockTranslateService(): TranslateService {
return jasmine.createSpyObj('translateService', {
get: jasmine.createSpy('get'),
use: jasmine.createSpy('use'),
instant: jasmine.createSpy('instant'),
setDefaultLang: jasmine.createSpy('setDefaultLang')
});

View File

@@ -1,3 +1,3 @@
<div class="mt-2 mb-2">
<span class="text-muted">{{'submission.workflow.tasks.generic.submitter' | translate}} : <span class="badge badge-light">{{(submitter$ | async)?.name}}</span></span>
<span class="text-muted">{{'submission.workflow.tasks.generic.submitter' | translate}} : <span class="badge badge-info">{{(submitter$ | async)?.name}}</span></span>
</div>

View File

@@ -9,7 +9,7 @@
</h2>
<div class="row mb-1">
<div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper>
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
</ds-metadata-field-wrapper>
<ng-container *ngVar="(getFiles() | async) as bitstreams">

View File

@@ -1,11 +1,11 @@
<div class="card">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/collections/', object.id]" class="card-img-top">
<ds-grid-thumbnail [thumbnail]="(object.logo | async)?.payload">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="(object.logo | async)?.payload" [limitWidth]="false">
</ds-thumbnail>
</a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top">
<ds-grid-thumbnail [thumbnail]="(object.logo | async)?.payload">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="(object.logo | async)?.payload" [limitWidth]="false">
</ds-thumbnail>
</span>
<div class="card-body">
<h4 class="card-title">{{object.name}}</h4>

View File

@@ -1,11 +1,11 @@
<div class="card">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/communities/', object.id]" class="card-img-top">
<ds-grid-thumbnail [thumbnail]="(object.logo | async)?.payload">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="(object.logo | async)?.payload" [limitWidth]="false">
</ds-thumbnail>
</a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top">
<ds-grid-thumbnail [thumbnail]="(object.logo | async)?.payload">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="(object.logo | async)?.payload" [limitWidth]="false">
</ds-thumbnail>
</span>
<div class="card-body">
<h4 class="card-title">{{object.name}}</h4>

View File

@@ -1,3 +0,0 @@
<div class="thumbnail">
<img [src]="src | dsSafeUrl" (error)="errorHandler($event)" />
</div>

View File

@@ -1,50 +0,0 @@
import { DebugElement } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Bitstream } from '../../../core/shared/bitstream.model';
import { SafeUrlPipe } from '../../utils/safe-url-pipe';
import { GridThumbnailComponent } from './grid-thumbnail.component';
describe('GridThumbnailComponent', () => {
let comp: GridThumbnailComponent;
let fixture: ComponentFixture<GridThumbnailComponent>;
let de: DebugElement;
let el: HTMLElement;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [GridThumbnailComponent, SafeUrlPipe]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GridThumbnailComponent);
comp = fixture.componentInstance; // BannerComponent test instance
de = fixture.debugElement.query(By.css('div.thumbnail'));
el = de.nativeElement;
});
it('should display image', () => {
const thumbnail = new Bitstream();
thumbnail._links = {
self: { href: 'self.url' },
bundle: { href: 'bundle.url' },
format: { href: 'format.url' },
content: { href: 'content.url' },
};
comp.thumbnail = thumbnail;
fixture.detectChanges();
const image: HTMLElement = de.query(By.css('img')).nativeElement;
expect(image.getAttribute('src')).toBe(comp.thumbnail._links.content.href);
});
it('should display placeholder', () => {
const thumbnail = new Bitstream();
comp.thumbnail = thumbnail;
fixture.detectChanges();
const image: HTMLElement = de.query(By.css('img')).nativeElement;
expect(image.getAttribute('src')).toBe(comp.defaultImage);
});
});

View File

@@ -1,72 +0,0 @@
import {
Component,
Input,
OnChanges,
OnInit,
SimpleChanges,
} from '@angular/core';
import { Bitstream } from '../../../core/shared/bitstream.model';
import { hasValue } from '../../empty.util';
/**
* This component renders a given Bitstream as a thumbnail.
* One input parameter of type Bitstream is expected.
* If no Bitstream is provided, a holderjs image will be rendered instead.
*/
@Component({
selector: 'ds-grid-thumbnail',
styleUrls: ['./grid-thumbnail.component.scss'],
templateUrl: './grid-thumbnail.component.html',
})
export class GridThumbnailComponent implements OnInit, OnChanges {
@Input() thumbnail: Bitstream;
data: any = {};
/**
* The default 'holder.js' image
*/
@Input() defaultImage? =
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/PjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMjYwIiBoZWlnaHQ9IjE4MCIgdmlld0JveD0iMCAwIDI2MCAxODAiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiPjwhLS0KU291cmNlIFVSTDogaG9sZGVyLmpzLzEwMCV4MTgwL3RleHQ6Tm8gVGh1bWJuYWlsCkNyZWF0ZWQgd2l0aCBIb2xkZXIuanMgMi42LjAuCkxlYXJuIG1vcmUgYXQgaHR0cDovL2hvbGRlcmpzLmNvbQooYykgMjAxMi0yMDE1IEl2YW4gTWFsb3BpbnNreSAtIGh0dHA6Ly9pbXNreS5jbwotLT48ZGVmcz48c3R5bGUgdHlwZT0idGV4dC9jc3MiPjwhW0NEQVRBWyNob2xkZXJfMTVmNzJmMmFlMGIgdGV4dCB7IGZpbGw6I0FBQUFBQTtmb250LXdlaWdodDpib2xkO2ZvbnQtZmFtaWx5OkFyaWFsLCBIZWx2ZXRpY2EsIE9wZW4gU2Fucywgc2Fucy1zZXJpZiwgbW9ub3NwYWNlO2ZvbnQtc2l6ZToxM3B0IH0gXV0+PC9zdHlsZT48L2RlZnM+PGcgaWQ9ImhvbGRlcl8xNWY3MmYyYWUwYiI+PHJlY3Qgd2lkdGg9IjI2MCIgaGVpZ2h0PSIxODAiIGZpbGw9IiNFRUVFRUUiLz48Zz48dGV4dCB4PSI3Mi4yNDIxODc1IiB5PSI5NiI+Tm8gVGh1bWJuYWlsPC90ZXh0PjwvZz48L2c+PC9zdmc+';
src: string;
errorHandler(event) {
event.currentTarget.src = this.defaultImage;
}
/**
* Initialize the src
*/
ngOnInit(): void {
this.src = this.defaultImage;
this.checkThumbnail(this.thumbnail);
}
/**
* If the old input is undefined and the new one is a bitsream then set src
*/
ngOnChanges(changes: SimpleChanges): void {
if (
!hasValue(changes.thumbnail.previousValue) &&
hasValue(changes.thumbnail.currentValue)
) {
this.checkThumbnail(changes.thumbnail.currentValue);
}
}
/**
* check if the Bitstream has any content than set the src
*/
checkThumbnail(thumbnail: Bitstream) {
if (
hasValue(thumbnail) &&
hasValue(thumbnail._links) &&
thumbnail._links.content.href
) {
this.src = thumbnail._links.content.href;
}
}
}

View File

@@ -1,7 +1,7 @@
:host ::ng-deep {
--ds-wrapper-grid-spacing: calc(var(--bs-spacer) / 2);
div.thumbnail > img {
div.thumbnail > .thumbnail-content {
height: var(--ds-card-thumbnail-height);
width: 100%;
display: block;

View File

@@ -1,11 +1,11 @@
<div class="card">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/collections/', dso.id]" class="card-img-top">
<ds-grid-thumbnail [thumbnail]="(dso.logo | async)?.payload">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false">
</ds-thumbnail>
</a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top">
<ds-grid-thumbnail [thumbnail]="(dso.logo | async)?.payload">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false">
</ds-thumbnail>
</span>
<div class="card-body">
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>

View File

@@ -1,11 +1,11 @@
<div class="card">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/communities/', dso.id]" class="card-img-top">
<ds-grid-thumbnail [thumbnail]="(dso.logo | async)?.payload">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false">
</ds-thumbnail>
</a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top">
<ds-grid-thumbnail [thumbnail]="(dso.logo | async)?.payload">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false">
</ds-thumbnail>
</span>
<div class="card-body">
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>

View File

@@ -6,14 +6,14 @@
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
class="card-img-top full-width">
<div>
<ds-grid-thumbnail [thumbnail]="getThumbnail() | async">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
</ds-thumbnail>
</div>
</a>
<span *ngIf="linkType == linkTypes.None" class="card-img-top full-width">
<div>
<ds-grid-thumbnail [thumbnail]="getThumbnail() | async">
</ds-grid-thumbnail>
<ds-thumbnail [thumbnail]="getThumbnail() | async" [limitWidth]="false">
</ds-thumbnail>
</div>
</span>
<div class="card-body">

View File

@@ -1,3 +1,3 @@
<div *ngIf="typeMessage">
<span class="badge badge-light">{{ typeMessage | translate }}</span>
<span class="badge badge-info">{{ typeMessage | translate }}</span>
</div>

View File

@@ -46,7 +46,6 @@ import { ThumbnailComponent } from '../thumbnail/thumbnail.component';
import { SearchFormComponent } from './search-form/search-form.component';
import { SearchResultGridElementComponent } from './object-grid/search-result-grid-element/search-result-grid-element.component';
import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbnail.component';
import { VarDirective } from './utils/var.directive';
import { AuthNavMenuComponent } from './auth-nav-menu/auth-nav-menu.component';
import { LogOutComponent } from './log-out/log-out.component';
@@ -54,8 +53,7 @@ import { FormComponent } from './form/form.component';
import { DsDynamicOneboxComponent } from './form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component';
import { DsDynamicScrollableDropdownComponent } from './form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
import {
DsDynamicFormControlContainerComponent,
dsDynamicFormControlMapFn
DsDynamicFormControlContainerComponent, dsDynamicFormControlMapFn,
} from './form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component';
import { DsDynamicFormComponent } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form.component';
import { DragClickDirective } from './utils/drag-click.directive';
@@ -340,7 +338,6 @@ const COMPONENTS = [
SidebarFilterComponent,
SidebarFilterSelectedOptionComponent,
ThumbnailComponent,
GridThumbnailComponent,
UploaderComponent,
FileDropzoneNoUploaderComponent,
ItemListPreviewComponent,

View File

@@ -1,10 +0,0 @@
<div class="submission-submit-container">
<div class="submission-submit-container">
<ds-submission-form [collectionId]="collectionId"
[sections]="sections"
[selfUrl]="selfUrl"
[submissionDefinition]="submissionDefinition"
[item]="item"
[submissionId]="submissionId"></ds-submission-form>
</div>
</div>

View File

@@ -26,7 +26,6 @@ describe('SubmissionSubmitComponent Component', () => {
let itemDataService: ItemDataService;
let router: RouterStub;
const submissionId = '826';
const submissionObject: any = mockSubmissionObject;
beforeEach(waitForAsync(() => {
@@ -67,27 +66,23 @@ describe('SubmissionSubmitComponent Component', () => {
router = null;
});
it('should init properly when a valid SubmissionObject has been retrieved',() => {
submissionServiceStub.createSubmission.and.returnValue(observableOf(submissionObject));
fixture.detectChanges();
expect(comp.submissionId.toString()).toEqual(submissionId);
expect(comp.collectionId).toBe(submissionObject.collection.id);
expect(comp.selfUrl).toBe(submissionObject._links.self.href);
expect(comp.sections).toBe(submissionObject.sections);
expect(comp.submissionDefinition).toBe(submissionObject.submissionDefinition);
});
it('should redirect to mydspace when an empty SubmissionObject has been retrieved',() => {
submissionServiceStub.createSubmission.and.returnValue(observableOf({}));
fixture.detectChanges();
expect(router.navigate).toHaveBeenCalled();
expect(router.navigate).toHaveBeenCalledWith(['/mydspace']);
});
it('should redirect to workspaceitem edit when a not empty SubmissionObject has been retrieved',() => {
submissionServiceStub.createSubmission.and.returnValue(observableOf({ id: '1234'}));
fixture.detectChanges();
expect(router.navigate).toHaveBeenCalledWith(['/workspaceitems', '1234', 'edit'], { replaceUrl: true});
});

View File

@@ -122,13 +122,7 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit {
this.notificationsService.info(null, this.translate.get('submission.general.cannot_submit'));
this.router.navigate(['/mydspace']);
} else {
this.collectionId = (submissionObject.collection as Collection).id;
this.sections = submissionObject.sections;
this.selfUrl = submissionObject._links.self.href;
this.submissionDefinition = (submissionObject.submissionDefinition as SubmissionDefinitionsModel);
this.submissionId = submissionObject.id;
this.itemLink$.next(submissionObject._links.item.href);
this.item = submissionObject.item as Item;
this.router.navigate(['/workspaceitems', submissionObject.id, 'edit'], { replaceUrl: true});
}
}
}),

View File

@@ -1,4 +1,14 @@
<div class="thumbnail">
<img [src]="src | dsSafeUrl" (error)="errorHandler()" class="img-fluid"/>
<div class="thumbnail" [class.limit-width]="limitWidth">
<ds-loading *ngIf="isLoading; else showThumbnail" class="thumbnail-content" [showMessage]="false">
text-content
</ds-loading>
<ng-template #showThumbnail>
<img *ngIf="src !== null" class="thumbnail-content img-fluid"
[src]="src | dsSafeUrl" [alt]="alt | translate" (error)="errorHandler()">
<div *ngIf="src === null" class="thumbnail-content outer">
<div class="inner">
<div class="thumbnail-placeholder w-100 h-100 p-3 lead">{{ placeholder | translate }}</div>
</div>
</div>
</ng-template>
</div>

View File

@@ -1,3 +1,35 @@
.limit-width {
max-width: var(--ds-thumbnail-max-width);
}
img {
max-width: 100%;
}
.outer { // .outer/.inner generated ~ https://ratiobuddy.com/
position: relative;
&:before {
display: block;
content: "";
width: 100%;
padding-top: (297 / 210) * 100%; // A4 ratio
}
> .inner {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
> .thumbnail-placeholder {
background: var(--ds-thumbnail-placeholder-background);
border: var(--ds-thumbnail-placeholder-border);
color: var(--ds-thumbnail-placeholder-color);
font-weight: bold;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
}
}

View File

@@ -1,10 +1,22 @@
import { DebugElement } from '@angular/core';
import { DebugElement, Pipe, PipeTransform } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Bitstream } from '../core/shared/bitstream.model';
import { SafeUrlPipe } from '../shared/utils/safe-url-pipe';
import { THUMBNAIL_PLACEHOLDER, ThumbnailComponent } from './thumbnail.component';
import { ThumbnailComponent } from './thumbnail.component';
import { RemoteData } from '../core/data/remote-data';
import {
createFailedRemoteDataObject, createPendingRemoteDataObject, createSuccessfulRemoteDataObject,
} from '../shared/remote-data.utils';
// tslint:disable-next-line:pipe-prefix
@Pipe({ name: 'translate' })
class MockTranslatePipe implements PipeTransform {
transform(key: string): string {
return 'TRANSLATED ' + key;
}
}
describe('ThumbnailComponent', () => {
let comp: ThumbnailComponent;
@@ -14,33 +26,18 @@ describe('ThumbnailComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ThumbnailComponent, SafeUrlPipe]
declarations: [ThumbnailComponent, SafeUrlPipe, MockTranslatePipe],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ThumbnailComponent);
comp = fixture.componentInstance; // BannerComponent test instance
comp = fixture.componentInstance; // ThumbnailComponent test instance
de = fixture.debugElement.query(By.css('div.thumbnail'));
el = de.nativeElement;
});
describe('when the thumbnail exists', () => {
it('should display an image', () => {
const thumbnail = new Bitstream();
thumbnail._links = {
self: { href: 'self.url' },
bundle: { href: 'bundle.url' },
format: { href: 'format.url' },
content: { href: 'content.url' },
};
comp.thumbnail = thumbnail;
fixture.detectChanges();
const image: HTMLElement = de.query(By.css('img')).nativeElement;
expect(image.getAttribute('src')).toBe(comp.thumbnail._links.content.href);
});
});
describe(`when the thumbnail doesn't exist`, () => {
const withoutThumbnail = () => {
describe('and there is a default image', () => {
it('should display the default image', () => {
comp.src = 'http://bit.stream';
@@ -48,14 +45,114 @@ describe('ThumbnailComponent', () => {
comp.errorHandler();
expect(comp.src).toBe(comp.defaultImage);
});
it('should include the alt text', () => {
comp.src = 'http://bit.stream';
comp.defaultImage = 'http://default.img';
comp.errorHandler();
comp.ngOnChanges();
fixture.detectChanges();
const image: HTMLElement = de.query(By.css('img')).nativeElement;
expect(image.getAttribute('alt')).toBe('TRANSLATED ' + comp.alt);
});
});
describe('and there is no default image', () => {
it('should display the placeholder', () => {
comp.src = 'http://default.img';
comp.defaultImage = 'http://default.img';
comp.errorHandler();
expect(comp.src).toBe(THUMBNAIL_PLACEHOLDER);
});
expect(comp.src).toBe(null);
comp.ngOnChanges();
fixture.detectChanges();
const placeholder = fixture.debugElement.query(By.css('div.thumbnail-placeholder')).nativeElement;
expect(placeholder.innerHTML).toBe('TRANSLATED ' + comp.placeholder);
});
});
};
describe('with thumbnail as Bitstream', () => {
let thumbnail: Bitstream;
beforeEach(() => {
thumbnail = new Bitstream();
thumbnail._links = {
self: { href: 'self.url' },
bundle: { href: 'bundle.url' },
format: { href: 'format.url' },
content: { href: 'content.url' },
};
});
it('should display an image', () => {
comp.thumbnail = thumbnail;
comp.ngOnChanges();
fixture.detectChanges();
const image: HTMLElement = de.query(By.css('img')).nativeElement;
expect(image.getAttribute('src')).toBe(comp.thumbnail._links.content.href);
});
it('should include the alt text', () => {
comp.thumbnail = thumbnail;
comp.ngOnChanges();
fixture.detectChanges();
const image: HTMLElement = de.query(By.css('img')).nativeElement;
expect(image.getAttribute('alt')).toBe('TRANSLATED ' + comp.alt);
});
describe('when there is no thumbnail', () => {
withoutThumbnail();
});
});
describe('with thumbnail as RemoteData<Bitstream>', () => {
let thumbnail: RemoteData<Bitstream>;
describe('while loading', () => {
beforeEach(() => {
thumbnail = createPendingRemoteDataObject();
});
it('should show a loading animation', () => {
comp.thumbnail = thumbnail;
comp.ngOnChanges();
fixture.detectChanges();
expect(de.query(By.css('ds-loading'))).toBeTruthy();
});
});
describe('when there is a thumbnail', () => {
beforeEach(() => {
const bitstream = new Bitstream();
bitstream._links = {
self: { href: 'self.url' },
bundle: { href: 'bundle.url' },
format: { href: 'format.url' },
content: { href: 'content.url' },
};
thumbnail = createSuccessfulRemoteDataObject(bitstream);
});
it('should display an image', () => {
comp.thumbnail = thumbnail;
comp.ngOnChanges();
fixture.detectChanges();
const image: HTMLElement = de.query(By.css('img')).nativeElement;
expect(image.getAttribute('src')).toBe(comp.thumbnail.payload._links.content.href);
});
it('should display the alt text', () => {
comp.thumbnail = thumbnail;
comp.ngOnChanges();
fixture.detectChanges();
const image: HTMLElement = de.query(By.css('img')).nativeElement;
expect(image.getAttribute('alt')).toBe('TRANSLATED ' + comp.alt);
});
});
describe('when there is no thumbnail', () => {
beforeEach(() => {
thumbnail = createFailedRemoteDataObject();
});
withoutThumbnail();
});
});
});

View File

@@ -1,61 +1,93 @@
import { Component, Input, OnInit } from '@angular/core';
import { Component, Input, OnChanges } from '@angular/core';
import { Bitstream } from '../core/shared/bitstream.model';
import { hasValue } from '../shared/empty.util';
/**
* A fallback placeholder image as a base64 string
*/
export const THUMBNAIL_PLACEHOLDER = 'data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2293%22%20height%3D%22120%22%20viewBox%3D%220%200%2093%20120%22%20preserveAspectRatio%3D%22none%22%3E%3C!--%0ASource%20URL%3A%20holder.js%2F93x120%3Ftext%3DNo%20Thumbnail%0ACreated%20with%20Holder.js%202.8.2.%0ALearn%20more%20at%20http%3A%2F%2Fholderjs.com%0A(c)%202012-2015%20Ivan%20Malopinsky%20-%20http%3A%2F%2Fimsky.co%0A--%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%3C!%5BCDATA%5B%23holder_1543e460b05%20text%20%7B%20fill%3A%23AAAAAA%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A10pt%20%7D%20%5D%5D%3E%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_1543e460b05%22%3E%3Crect%20width%3D%2293%22%20height%3D%22120%22%20fill%3D%22%23FFFFFF%22%2F%3E%3Cg%3E%3Ctext%20x%3D%2235.6171875%22%20y%3D%2257%22%3ENo%3C%2Ftext%3E%3Ctext%20x%3D%2210.8125%22%20y%3D%2272%22%3EThumbnail%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E';
import { RemoteData } from '../core/data/remote-data';
/**
* This component renders a given Bitstream as a thumbnail.
* One input parameter of type Bitstream is expected.
* If no Bitstream is provided, a holderjs image will be rendered instead.
* If no Bitstream is provided, a HTML placeholder will be rendered instead.
*/
@Component({
selector: 'ds-thumbnail',
styleUrls: ['./thumbnail.component.scss'],
templateUrl: './thumbnail.component.html'
templateUrl: './thumbnail.component.html',
})
export class ThumbnailComponent implements OnInit {
export class ThumbnailComponent implements OnChanges {
/**
* The thumbnail Bitstream
*/
@Input() thumbnail: Bitstream;
@Input() thumbnail: Bitstream | RemoteData<Bitstream>;
/**
* The default image, used if the thumbnail isn't set or can't be downloaded
* The default image, used if the thumbnail isn't set or can't be downloaded.
* If defaultImage is null, a HTML placeholder is used instead.
*/
@Input() defaultImage? = THUMBNAIL_PLACEHOLDER;
@Input() defaultImage? = null;
/**
* The src attribute used in the template to render the image.
*/
src: string;
src: string = null;
/**
* Initialize the thumbnail.
* i18n key of thumbnail alt text
*/
@Input() alt? = 'thumbnail.default.alt';
/**
* i18n key of HTML placeholder text
*/
@Input() placeholder? = 'thumbnail.default.placeholder';
/**
* Limit thumbnail width to --ds-thumbnail-max-width
*/
@Input() limitWidth? = true;
isLoading: boolean;
/**
* Resolve the thumbnail.
* Use a default image if no actual image is available.
*/
ngOnInit(): void {
if (hasValue(this.thumbnail) && hasValue(this.thumbnail._links) && hasValue(this.thumbnail._links.content) && this.thumbnail._links.content.href) {
this.src = this.thumbnail._links.content.href;
ngOnChanges(): void {
if (this.thumbnail === undefined || this.thumbnail === null) {
return;
}
if (this.thumbnail instanceof Bitstream) {
this.resolveThumbnail(this.thumbnail as Bitstream);
} else {
const thumbnailRD = this.thumbnail as RemoteData<Bitstream>;
if (thumbnailRD.isLoading) {
this.isLoading = true;
} else {
this.resolveThumbnail(thumbnailRD.payload as Bitstream);
}
}
}
private resolveThumbnail(thumbnail: Bitstream): void {
if (hasValue(thumbnail) && hasValue(thumbnail._links)
&& hasValue(thumbnail._links.content)
&& thumbnail._links.content.href) {
this.src = thumbnail._links.content.href;
} else {
this.src = this.defaultImage;
}
this.isLoading = false;
}
/**
* Handle image download errors.
* If the image can't be found, use the defaultImage instead.
* If that also can't be found, use the base64 placeholder.
* If that also can't be found, use null to fall back to the HTML placeholder.
*/
errorHandler() {
if (this.src !== this.defaultImage) {
this.src = this.defaultImage;
} else {
this.src = THUMBNAIL_PLACEHOLDER;
this.src = null;
}
}
}

View File

@@ -238,6 +238,8 @@
"admin.access-control.epeople.table.edit.buttons.edit": "Edit \"{{name}}\"",
"admin.access-control.epeople.table.edit.buttons.edit-disabled": "You are not authorized to edit this group",
"admin.access-control.epeople.table.edit.buttons.remove": "Delete \"{{name}}\"",
"admin.access-control.epeople.no-items": "No EPeople to show.",
@@ -2353,6 +2355,8 @@
"nav.stop-impersonating": "Stop impersonating EPerson",
"nav.toggle" : "Toggle navigation",
"nav.user.description" : "User profile bar",
@@ -3135,6 +3139,8 @@
"submission.import-external.source.sherpaJournal": "SHERPA Journals",
"submission.import-external.source.sherpaJournalIssn": "SHERPA Journals by ISSN",
"submission.import-external.source.sherpaPublisher": "SHERPA Publishers",
"submission.import-external.source.orcid": "ORCID",
@@ -3540,6 +3546,24 @@
"thumbnail.default.alt": "Thumbnail Image",
"thumbnail.default.placeholder": "No Thumbnail Available",
"thumbnail.project.alt": "Project Logo",
"thumbnail.project.placeholder": "Project Placeholder Image",
"thumbnail.orgunit.alt": "OrgUnit Logo",
"thumbnail.orgunit.placeholder": "OrgUnit Placeholder Image",
"thumbnail.person.alt": "Profile Picture",
"thumbnail.person.placeholder": "No Profile Picture Available",
"title": "DSpace",

View File

@@ -12,12 +12,8 @@ $image-path: "../assets/images" !default;
/** Bootstrap Variables **/
/* Colors */
$gray-base: #000 !default;
$gray-900: lighten($gray-base, 13.5%) !default; // #222
$gray-800: lighten($gray-base, 26.6%) !default; // #444
$gray-700: lighten($gray-base, 46.6%) !default; // #777
$gray-600: lighten($gray-base, 73.3%) !default; // #bbb
$gray-100: lighten($gray-base, 93.5%) !default; // #eee
$gray-700: #495057 !default; // Bootstrap $gray-700
$gray-100: #f8f9fa !default; // $gray-100
/* Reassign color vars to semantic color scheme */
$blue: #2B4E72 !default;

View File

@@ -20,7 +20,7 @@
--ds-sidebar-z-index: 20;
--ds-header-bg: #{$white};
--ds-header-logo-height: 40px;
--ds-header-logo-height: 50px;
--ds-header-logo-height-xs: 50px;
--ds-header-icon-color: #{$cyan};
--ds-header-icon-color-hover: #{darken($white, 15%)};
@@ -46,6 +46,9 @@
--ds-edit-item-language-field-width: 43px;
--ds-thumbnail-max-width: 175px;
--ds-thumbnail-placeholder-background: #{$gray-100};
--ds-thumbnail-placeholder-border: 1px solid #{$gray-300};
--ds-thumbnail-placeholder-color: #{lighten($gray-800, 7%)};
--ds-dso-selector-list-max-height: 475px;
--ds-dso-selector-current-background-color: #eeeeee;
@@ -61,12 +64,12 @@
--ds-sidebar-items-width: #{$sidebar-items-width};
--ds-total-sidebar-width: #{$total-sidebar-width};
--ds-top-footer-bg: #{$gray-200} !important;
--ds-footer-bg: #{theme-color('primary')} !important;
--ds-top-footer-bg: #{$gray-200};
--ds-footer-bg: #{theme-color('primary')};
--ds-footer-border: 1px solid var(--bs-gray-400);
--ds-footer-padding: 0 !important;
--ds-footer-padding-bottom: 0 !important;
--ds-footer-logo-height: 50px !important;
--ds-footer-padding: 0;
--ds-footer-padding-bottom: 0;
--ds-footer-logo-height: 50px;
$home-news-link-color: $cyan;
--ds-home-news-link-color: #{$home-news-link-color};

View File

@@ -0,0 +1,15 @@
import { Component } from '@angular/core';
import { HeaderNavbarWrapperComponent as BaseComponent } from '../../../../app/header-nav-wrapper/header-navbar-wrapper.component';
/**
* This component represents a wrapper for the horizontal navbar and the header
*/
@Component({
selector: 'ds-header-navbar-wrapper',
// styleUrls: ['header-navbar-wrapper.component.scss'],
styleUrls: ['../../../../app/header-nav-wrapper/header-navbar-wrapper.component.scss'],
// templateUrl: 'header-navbar-wrapper.component.html',
templateUrl: '../../../../app/header-nav-wrapper/header-navbar-wrapper.component.html',
})
export class HeaderNavbarWrapperComponent extends BaseComponent {
}

View File

@@ -3,14 +3,27 @@
// still uses Sass variables internally. So if you want to override bootstrap (or other sass
// variables) you can do so here. Their CSS counterparts will include the changes you make here
// $blue: #007bff !default;
// $indigo: #6610f2 !default;
// $purple: #6f42c1 !default;
// $pink: #e83e8c !default;
// $red: #dc3545 !default;
// $orange: #fd7e14 !default;
// $yellow: #ffc107 !default;
// $green: #28a745 !default;
// $teal: #20c997 !default;
// $cyan: #17a2b8 !default;
// $font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !default;
//
// $gray-700: #495057 !default; // Bootstrap $gray-700
// $gray-100: #f8f9fa !default; // $gray-100
//
// $blue: #2B4E72 !default;
// $green: #94BA65 !default;
// $cyan: #006666 !default;
// $yellow: #ec9433 !default;
// $red: #CF4444 !default;
// $dark: darken($blue, 17%) !default;
//
// $theme-colors: (
// primary: $blue,
// secondary: $gray-700,
// success: $green,
// info: $cyan,
// warning: $yellow,
// danger: $red,
// light: $gray-100,
// dark: $dark
// ) !default;
//
// $link-color: map-get($theme-colors, info) !default;

View File

@@ -78,6 +78,7 @@ import { NavbarComponent } from './app/navbar/navbar.component';
import { HeaderComponent } from './app/header/header.component';
import { FooterComponent } from './app/footer/footer.component';
import { BreadcrumbsComponent } from './app/breadcrumbs/breadcrumbs.component';
import { HeaderNavbarWrapperComponent } from './app/header-nav-wrapper/header-navbar-wrapper.component';
const DECLARATIONS = [
HomePageComponent,
@@ -117,6 +118,7 @@ const DECLARATIONS = [
FooterComponent,
HeaderComponent,
NavbarComponent,
HeaderNavbarWrapperComponent,
BreadcrumbsComponent
];

View File

@@ -6,12 +6,8 @@
color: white;
background-color: var(--bs-info);
position: relative;
background-position-y: -200px;
background-image: url('/assets/dspace/images/banner.jpg');
background-size: cover;
@media screen and (max-width: map-get($grid-breakpoints, lg)) {
background-position-y: 0;
}
.container {
position: relative;

View File

@@ -0,0 +1,3 @@
<div [ngClass]="{'open': !(isNavBarCollapsed | async)}">
<ds-themed-header></ds-themed-header>
</div>

View File

@@ -0,0 +1,13 @@
import { Component } from '@angular/core';
import { HeaderNavbarWrapperComponent as BaseComponent } from '../../../../app/header-nav-wrapper/header-navbar-wrapper.component';
/**
* 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 extends BaseComponent {
}

View File

@@ -0,0 +1,24 @@
<header class="header">
<nav role="navigation" [attr.aria-label]="'nav.user.description' | translate" class="container navbar navbar-expand-md px-0">
<div class="d-flex flex-grow-1">
<a class="navbar-brand m-2" routerLink="/home">
<img src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate"/>
</a>
</div>
<div class="d-flex flex-grow-1 ml-auto justify-content-end align-items-center">
<ds-search-navbar class="navbar-search"></ds-search-navbar>
<ds-lang-switch></ds-lang-switch>
<ds-auth-nav-menu></ds-auth-nav-menu>
<ds-impersonate-navbar></ds-impersonate-navbar>
<div class="pl-2">
<button class="navbar-toggler" type="button" (click)="toggleNavbar()"
aria-controls="collapsingNav"
aria-expanded="false" [attr.aria-label]="'nav.toggle' | translate">
<span class="navbar-toggler-icon fas fa-bars fa-fw" aria-hidden="true"></span>
</button>
</div>
</div>
</nav>
<ds-themed-navbar></ds-themed-navbar>
</header>

View File

@@ -0,0 +1,19 @@
@media screen and (min-width: map-get($grid-breakpoints, md)) {
nav.navbar {
display: none;
}
.header {
background-color: var(--ds-header-bg);
}
}
.navbar-brand img {
@media screen and (max-width: map-get($grid-breakpoints, md)) {
height: var(--ds-header-logo-height-xs);
}
}
.navbar-toggler .navbar-toggler-icon {
background-image: none !important;
line-height: 1.5;
color: var(--bs-link-color);
}

View File

@@ -0,0 +1,13 @@
import { Component } from '@angular/core';
import { HeaderComponent as BaseComponent } from '../../../../app/header/header.component';
/**
* Represents the header with the logo and simple navigation
*/
@Component({
selector: 'ds-header',
styleUrls: ['header.component.scss'],
templateUrl: 'header.component.html',
})
export class HeaderComponent extends BaseComponent {
}

View File

@@ -0,0 +1,24 @@
<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">
<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 me-auto mb-2 mb-lg-0 h-100">
<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>
</ul>
</div>
<ds-search-navbar class="navbar-collapsed"></ds-search-navbar>
<ds-lang-switch class="navbar-collapsed"></ds-lang-switch>
<ds-auth-nav-menu class="navbar-collapsed"></ds-auth-nav-menu>
<ds-impersonate-navbar class="navbar-collapsed"></ds-impersonate-navbar>
</div>
</nav>

View File

@@ -1,5 +1,57 @@
@import 'src/app/navbar/navbar.component.scss';
nav.navbar {
border-top: 1px var(--ds-header-navbar-border-top-color) solid;
border-bottom: 5px var(--bs-green) solid;
align-items: baseline;
color: var(--ds-header-icon-color);
}
/** Mobile menu styling **/
@media screen and (max-width: map-get($grid-breakpoints, md)) {
.navbar {
width: 100%;
background-color: var(--bs-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: calc(var(--bs-spacer) / -2);
margin-right: calc(var(--bs-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 var(--bs-spacer);
a.navbar-brand {
display: none;
}
.navbar-collapsed {
display: none;
}
}
padding: 0;
}
height: 80px;
}
a.navbar-brand img {
max-height: var(--ds-header-logo-height);
}
.navbar-nav {
::ng-deep a.nav-link {
color: var(--ds-navbar-link-color);
}
::ng-deep a.nav-link:hover {
color: var(--ds-navbar-link-color-hover);
}
}

View File

@@ -8,7 +8,7 @@ import { slideMobileNav } from '../../../../app/shared/animations/slide';
@Component({
selector: 'ds-navbar',
styleUrls: ['./navbar.component.scss'],
templateUrl: '../../../../app/navbar/navbar.component.html',
templateUrl: './navbar.component.html',
animations: [slideMobileNav]
})
export class NavbarComponent extends BaseComponent {

View File

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

View File

@@ -1,6 +1,7 @@
// Override or add CSS variables for your theme here
:root {
--ds-header-logo-height: 40px;
--ds-banner-text-background: rgba(0, 0, 0, 0.45);
--ds-banner-background-gradient-width: 300px;
--ds-home-news-link-color: #{$green};

View File

@@ -6,10 +6,6 @@
@import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200;0,300;0,400;0,600;0,700;0,800;1,200;1,300;1,400;1,600;1,700;1,800&display=swap');
$font-family-sans-serif: 'Nunito', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
$gray-100: #e8ebf3 !default;
$gray-400: #ced4da !default;
$gray-600: #959595 !default;
$gray-800: #444444 !default;
$navbar-dark-color: #FFFFFF;
@@ -21,10 +17,13 @@ $yellow: #ec9433 !default;
$red: #CF4444 !default;
$dark: #43515f !default;
$body-color: $gray-800 !default;
$gray-800: #343a40 !default;
$gray-400: #ced4da !default;
$gray-100: #f8f9fa !default;
$table-accent-bg: $gray-100 !default;
$table-hover-bg: $gray-400 !default;
$body-color: $gray-800 !default; // Bootstrap $gray-800
$table-accent-bg: $gray-100 !default; // Bootstrap $gray-100
$table-hover-bg: $gray-400 !default; // Bootstrap $gray-400
$yiq-contrasted-threshold: 170 !default;

View File

@@ -41,9 +41,13 @@ import { CollectionPageModule } from '../../app/+collection-page/collection-page
import { SubmissionModule } from '../../app/submission/submission.module';
import { MyDSpacePageModule } from '../../app/+my-dspace-page/my-dspace-page.module';
import { NavbarComponent } from './app/navbar/navbar.component';
import { HeaderComponent } from './app/header/header.component';
import { HeaderNavbarWrapperComponent } from './app/header-nav-wrapper/header-navbar-wrapper.component';
const DECLARATIONS = [
HomeNewsComponent,
HeaderComponent,
HeaderNavbarWrapperComponent,
NavbarComponent
];

View File

@@ -2663,15 +2663,15 @@ browserify-zlib@^0.2.0:
pako "~1.0.5"
browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.15.0, browserslist@^4.6.4, browserslist@^4.9.1:
version "4.16.0"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.0.tgz#410277627500be3cb28a1bfe037586fbedf9488b"
integrity sha512-/j6k8R0p3nxOC6kx5JGAxsnhc9ixaWJfYc+TNTzxg6+ARaESAvQGV7h0uNOB4t+pLQJZWzcrMxXOxjgsCj3dqQ==
version "4.16.6"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2"
integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==
dependencies:
caniuse-lite "^1.0.30001165"
colorette "^1.2.1"
electron-to-chromium "^1.3.621"
caniuse-lite "^1.0.30001219"
colorette "^1.2.2"
electron-to-chromium "^1.3.723"
escalade "^3.1.1"
node-releases "^1.1.67"
node-releases "^1.1.71"
browserstack@^1.5.1:
version "1.6.0"
@@ -2905,10 +2905,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001032, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001165:
version "1.0.30001165"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001165.tgz#32955490d2f60290bb186bb754f2981917fa744f"
integrity sha512-8cEsSMwXfx7lWSUMA2s08z9dIgsnR5NAqjXP23stdsU3AUWkCr/rr4s4OFtHXn5XXr6+7kam3QFVoYyXNPdJPA==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001032, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001165, caniuse-lite@^1.0.30001219:
version "1.0.30001237"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001237.tgz#4b7783661515b8e7151fc6376cfd97f0e427b9e5"
integrity sha512-pDHgRndit6p1NR2GhzMbQ6CkRrp4VKuSsqbcLeOQppYPKOYkKT/6ZvZDvKJUqcmtyWIAHuZq3SVS2vc1egCZzw==
canonical-path@1.0.0:
version "1.0.0"
@@ -3220,10 +3220,10 @@ color@^3.0.0:
color-convert "^1.9.1"
color-string "^1.5.4"
colorette@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b"
integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==
colorette@^1.2.1, colorette@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
colors@1.4.0, colors@^1.1.2, colors@^1.4.0:
version "1.4.0"
@@ -4208,9 +4208,9 @@ dns-equal@^1.0.0:
integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0=
dns-packet@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a"
integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==
version "1.3.4"
resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.4.tgz#e3455065824a2507ba886c55a89963bb107dec6f"
integrity sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==
dependencies:
ip "^1.1.0"
safe-buffer "^5.0.1"
@@ -4364,10 +4364,10 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
electron-to-chromium@^1.3.621:
version "1.3.622"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.622.tgz#9726bd2e67a5462154750ce9701ca6af07d07877"
integrity sha512-AJT0Fm1W0uZlMVVkkJrcCVvczDuF8tPm3bwzQf5WO8AaASB2hwTRP7B8pU5rqjireH+ib6am8+hH5/QkXzzYKw==
electron-to-chromium@^1.3.723:
version "1.3.752"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.752.tgz#0728587f1b9b970ec9ffad932496429aef750d09"
integrity sha512-2Tg+7jSl3oPxgsBsWKh5H83QazTkmWG/cnNwJplmyZc7KcN61+I10oUgaXSVk/NwfvN3BdkKDR4FYuRBQQ2v0A==
elliptic@^6.5.3:
version "6.5.4"
@@ -7710,10 +7710,10 @@ node-libs-browser@^2.2.1:
util "^0.11.0"
vm-browserify "^1.0.1"
node-releases@^1.1.67:
version "1.1.67"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.67.tgz#28ebfcccd0baa6aad8e8d4d8fe4cbc49ae239c12"
integrity sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==
node-releases@^1.1.71:
version "1.1.73"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20"
integrity sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==
nodemon@^2.0.2:
version "2.0.6"