mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge remote-tracking branch 'origin/main' into #1206
This commit is contained in:
15
SECURITY.md
Normal file
15
SECURITY.md
Normal 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
|
@@ -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.
|
||||
|
@@ -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'
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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'
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -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',
|
||||
|
@@ -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>
|
||||
|
@@ -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,38 +57,60 @@ describe('MetadataFieldWrapperComponent', () => {
|
||||
expect(component).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not show the component when there is no content', () => {
|
||||
const parentFixture = TestBed.createComponent(NoContentComponent);
|
||||
parentFixture.detectChanges();
|
||||
const parentNative = parentFixture.nativeElement;
|
||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
||||
expect(nativeWrapper.classList.contains('d-none')).toBe(true);
|
||||
describe('with hideIfNoTextContent=true', () => {
|
||||
it('should not show the component when there is no content', () => {
|
||||
const parentFixture = TestBed.createComponent(NoContentComponent);
|
||||
parentFixture.detectChanges();
|
||||
const parentNative = parentFixture.nativeElement;
|
||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
||||
expect(nativeWrapper.classList.contains('d-none')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not show the component when there is no text content', () => {
|
||||
const parentFixture = TestBed.createComponent(SpanContentComponent);
|
||||
parentFixture.detectChanges();
|
||||
const parentNative = parentFixture.nativeElement;
|
||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
||||
expect(nativeWrapper.classList.contains('d-none')).toBe(true);
|
||||
});
|
||||
|
||||
it('should show the component when there is text content', () => {
|
||||
const parentFixture = TestBed.createComponent(TextContentComponent);
|
||||
parentFixture.detectChanges();
|
||||
const parentNative = parentFixture.nativeElement;
|
||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
||||
parentFixture.detectChanges();
|
||||
expect(nativeWrapper.classList.contains('d-none')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not show the component when there is DOM content but not text or an image', () => {
|
||||
const parentFixture = TestBed.createComponent(SpanContentComponent);
|
||||
parentFixture.detectChanges();
|
||||
const parentNative = parentFixture.nativeElement;
|
||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
||||
expect(nativeWrapper.classList.contains('d-none')).toBe(true);
|
||||
});
|
||||
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 text content', () => {
|
||||
const parentFixture = TestBed.createComponent(TextContentComponent);
|
||||
parentFixture.detectChanges();
|
||||
const parentNative = parentFixture.nativeElement;
|
||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
||||
parentFixture.detectChanges();
|
||||
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 img content', () => {
|
||||
const parentFixture = TestBed.createComponent(ImgContentComponent);
|
||||
parentFixture.detectChanges();
|
||||
const parentNative = parentFixture.nativeElement;
|
||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
||||
parentFixture.detectChanges();
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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">
|
||||
|
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -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">
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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]
|
||||
}
|
||||
])
|
||||
]
|
||||
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
35
src/app/access-control/group-registry/group-page.guard.ts
Normal file
35
src/app/access-control/group-registry/group-page.guard.ts
Normal 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}`)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@@ -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} }}">
|
||||
<i class="fas fa-edit fa-fw"></i>
|
||||
</button>
|
||||
<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} }}">
|
||||
|
@@ -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;
|
||||
|
@@ -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,39 +141,53 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
if (groups.page.length === 0) {
|
||||
return observableOf(buildPaginatedList(groups.pageInfo, []));
|
||||
}
|
||||
return observableCombineLatest(groups.page.map((group: Group) => {
|
||||
if (!this.deletedGroupsIds.includes(group.id)) {
|
||||
return observableCombineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined),
|
||||
this.hasLinkedDSO(group),
|
||||
this.getSubgroups(group),
|
||||
this.getMembers(group)
|
||||
]).pipe(
|
||||
map(([isAuthorized, hasLinkedDSO, subgroups, members]:
|
||||
[boolean, boolean, RemoteData<PaginatedList<Group>>, RemoteData<PaginatedList<EPerson>>]) => {
|
||||
const groupDtoModel: GroupDtoModel = new GroupDtoModel();
|
||||
groupDtoModel.ableToDelete = isAuthorized && !hasLinkedDSO;
|
||||
groupDtoModel.group = group;
|
||||
groupDtoModel.subgroups = subgroups.payload;
|
||||
groupDtoModel.epersons = members.payload;
|
||||
return groupDtoModel;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
})).pipe(map((dtos: GroupDtoModel[]) => {
|
||||
return buildPaginatedList(groups.pageInfo, dtos);
|
||||
}));
|
||||
return this.authorizationService.isAuthorized(FeatureID.AdministratorOf).pipe(
|
||||
switchMap((isSiteAdmin: boolean) => {
|
||||
return observableCombineLatest(groups.page.map((group: Group) => {
|
||||
if (hasValue(group) && !this.deletedGroupsIds.includes(group.id)) {
|
||||
return observableCombineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self),
|
||||
this.canManageGroup$(isSiteAdmin, group),
|
||||
this.hasLinkedDSO(group),
|
||||
this.getSubgroups(group),
|
||||
this.getMembers(group)
|
||||
]).pipe(
|
||||
map(([canDelete, canManageGroup, hasLinkedDSO, subgroups, members]:
|
||||
[boolean, boolean, boolean, RemoteData<PaginatedList<Group>>, RemoteData<PaginatedList<EPerson>>]) => {
|
||||
const groupDtoModel: GroupDtoModel = new GroupDtoModel();
|
||||
groupDtoModel.ableToDelete = canDelete && !hasLinkedDSO;
|
||||
groupDtoModel.ableToEdit = canManageGroup;
|
||||
groupDtoModel.group = group;
|
||||
groupDtoModel.subgroups = subgroups.payload;
|
||||
groupDtoModel.epersons = members.payload;
|
||||
return groupDtoModel;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
})).pipe(map((dtos: GroupDtoModel[]) => {
|
||||
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
|
||||
*/
|
||||
|
@@ -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,
|
||||
|
@@ -10,6 +10,7 @@ export enum FeatureID {
|
||||
ReinstateItem = 'reinstateItem',
|
||||
EPersonRegistration = 'epersonRegistration',
|
||||
CanManageGroups = 'canManageGroups',
|
||||
CanManageGroup = 'canManageGroup',
|
||||
IsCollectionAdmin = 'isCollectionAdmin',
|
||||
IsCommunityAdmin = 'isCommunityAdmin',
|
||||
CanDownload = 'canDownload',
|
||||
|
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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
|
||||
*/
|
||||
|
@@ -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">
|
||||
|
@@ -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">
|
||||
|
@@ -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">
|
||||
|
@@ -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']"
|
||||
|
@@ -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']"
|
||||
|
@@ -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']"
|
||||
|
@@ -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">
|
||||
|
@@ -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">
|
||||
|
@@ -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">
|
||||
|
@@ -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']"
|
||||
|
@@ -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']"
|
||||
|
@@ -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']"-->
|
||||
|
@@ -1,72 +1,79 @@
|
||||
<footer class="top-footer text-lg-start">
|
||||
<!-- Grid container -->
|
||||
<div *ngIf="showTopFooter" class="container p-4">
|
||||
<!--Grid row-->
|
||||
<div class="row">
|
||||
<footer class="text-lg-start">
|
||||
<div *ngIf="showTopFooter" class="top-footer">
|
||||
<!-- Grid container -->
|
||||
<div class=" container p-4">
|
||||
<!--Grid row-->
|
||||
<div class="row">
|
||||
|
||||
<!--Grid column-->
|
||||
<div class="col-lg-4 col-md-6 mb-4 mb-lg-0">
|
||||
<h5 class="text-uppercase">Footer Content</h5>
|
||||
<!--Grid column-->
|
||||
<div class="col-lg-4 col-md-6 mb-4 mb-lg-0">
|
||||
<h5 class="text-uppercase">Footer Content</h5>
|
||||
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li>
|
||||
<a routerLink="./" class="">Lorem ipsum</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="./" class="">Ut facilisis</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="./" class="">Aenean sit</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li>
|
||||
<a routerLink="./" class="">Lorem ipsum</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="./" class="">Ut facilisis</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="./" class="">Aenean sit</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!--Grid column-->
|
||||
|
||||
<!--Grid column-->
|
||||
<div class="col-lg-4 col-md-6 mb-4 mb-lg-0">
|
||||
<h5 class="text-uppercase">Footer Content</h5>
|
||||
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li>
|
||||
<a routerLink="./" class="">Suspendisse potenti</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!--Grid column-->
|
||||
|
||||
<!--Grid column-->
|
||||
<div class="col-lg-4 col-md-12 mb-4 mb-md-0">
|
||||
<h5 class="text-uppercase">Footer Content</h5>
|
||||
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Iste atque ea quis
|
||||
molestias. Fugiat pariatur maxime quis culpa corporis vitae repudiandae aliquam
|
||||
voluptatem veniam, est atque cumque eum delectus sint!
|
||||
</p>
|
||||
</div>
|
||||
<!--Grid column-->
|
||||
</div>
|
||||
<!--Grid column-->
|
||||
|
||||
<!--Grid column-->
|
||||
<div class="col-lg-4 col-md-6 mb-4 mb-lg-0">
|
||||
<h5 class="text-uppercase">Footer Content</h5>
|
||||
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li>
|
||||
<a routerLink="./" class="">Suspendisse potenti</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!--Grid column-->
|
||||
|
||||
<!--Grid column-->
|
||||
<div class="col-lg-4 col-md-12 mb-4 mb-md-0">
|
||||
<h5 class="text-uppercase">Footer Content</h5>
|
||||
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Iste atque ea quis
|
||||
molestias. Fugiat pariatur maxime quis culpa corporis vitae repudiandae aliquam
|
||||
voluptatem veniam, est atque cumque eum delectus sint!
|
||||
</p>
|
||||
</div>
|
||||
<!--Grid column-->
|
||||
<!--Grid row-->
|
||||
</div>
|
||||
<!--Grid row-->
|
||||
</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>
|
||||
|
@@ -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
|
||||
|
@@ -1,3 +1,4 @@
|
||||
<div [ngClass]="{'open': !(isNavBarCollapsed | async)}">
|
||||
<ds-themed-header></ds-themed-header>
|
||||
<ds-themed-navbar></ds-themed-navbar>
|
||||
</div>
|
||||
|
@@ -5,7 +5,3 @@
|
||||
position: sticky;
|
||||
}
|
||||
}
|
||||
|
||||
:host {
|
||||
z-index: var(--ds-nav-z-index);
|
||||
}
|
||||
|
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
z-index: var(--ds-nav-z-index);
|
||||
}
|
@@ -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`);
|
||||
}
|
||||
}
|
@@ -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>
|
||||
<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">
|
||||
<span class="navbar-toggler-icon fas fa-bars fa-fw" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<ds-themed-navbar></ds-themed-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" [attr.aria-label]="'nav.toggle' | translate">
|
||||
<span class="navbar-toggler-icon fas fa-bars fa-fw" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
@@ -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);
|
||||
background-image: none !important;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.navbar ::ng-deep {
|
||||
a {
|
||||
color: var(--ds-header-icon-color);
|
||||
|
||||
&:hover, &focus {
|
||||
color: var(--ds-header-icon-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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"
|
||||
|
@@ -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>
|
||||
|
@@ -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">
|
||||
<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>
|
||||
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>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
|
@@ -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">
|
||||
|
@@ -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>
|
@@ -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>
|
@@ -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')
|
||||
});
|
||||
|
@@ -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>
|
||||
|
@@ -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">
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -1,3 +0,0 @@
|
||||
<div class="thumbnail">
|
||||
<img [src]="src | dsSafeUrl" (error)="errorHandler($event)" />
|
||||
</div>
|
@@ -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);
|
||||
});
|
||||
|
||||
});
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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">
|
||||
|
@@ -1,3 +1,3 @@
|
||||
<div *ngIf="typeMessage">
|
||||
<span class="badge badge-light">{{ typeMessage | translate }}</span>
|
||||
<span class="badge badge-info">{{ typeMessage | translate }}</span>
|
||||
</div>
|
||||
|
@@ -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,
|
||||
|
@@ -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>
|
||||
|
@@ -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});
|
||||
|
||||
});
|
||||
|
||||
|
@@ -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});
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
@@ -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>
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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",
|
||||
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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};
|
||||
|
@@ -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 {
|
||||
}
|
@@ -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;
|
||||
|
@@ -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
|
||||
];
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -0,0 +1,3 @@
|
||||
<div [ngClass]="{'open': !(isNavBarCollapsed | async)}">
|
||||
<ds-themed-header></ds-themed-header>
|
||||
</div>
|
@@ -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 {
|
||||
}
|
24
src/themes/dspace/app/header/header.component.html
Normal file
24
src/themes/dspace/app/header/header.component.html
Normal 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>
|
19
src/themes/dspace/app/header/header.component.scss
Normal file
19
src/themes/dspace/app/header/header.component.scss
Normal 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);
|
||||
}
|
13
src/themes/dspace/app/header/header.component.ts
Normal file
13
src/themes/dspace/app/header/header.component.ts
Normal 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 {
|
||||
}
|
24
src/themes/dspace/app/navbar/navbar.component.html
Normal file
24
src/themes/dspace/app/navbar/navbar.component.html
Normal 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>
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -3,7 +3,7 @@
|
||||
// imports the base global style
|
||||
@import '../../../styles/_global-styles.scss';
|
||||
|
||||
.facet-filter,.setting-option {
|
||||
.facet-filter, .setting-option {
|
||||
background-color: var(--bs-light);
|
||||
border-radius: var(--bs-border-radius);
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
@@ -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};
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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
|
||||
];
|
||||
|
||||
|
52
yarn.lock
52
yarn.lock
@@ -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"
|
||||
|
Reference in New Issue
Block a user