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

This commit is contained in:
Giuseppe Digilio
2021-06-14 17:27:07 +02:00
25 changed files with 2874 additions and 3694 deletions

View File

@@ -16,6 +16,9 @@ jobs:
DSPACE_REST_PORT: 8080 DSPACE_REST_PORT: 8080
DSPACE_REST_NAMESPACE: '/server' DSPACE_REST_NAMESPACE: '/server'
DSPACE_REST_SSL: false DSPACE_REST_SSL: false
# When Chrome version is specified, we pin to a specific version of Chrome & ChromeDriver
# Comment this out to use the latest release of both.
CHROME_VERSION: "90.0.4430.212-1"
strategy: strategy:
# Create a matrix of Node versions to test against (in parallel) # Create a matrix of Node versions to test against (in parallel)
matrix: matrix:
@@ -34,10 +37,20 @@ jobs:
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- name: Install latest Chrome (for e2e tests) # If CHROME_VERSION env variable specified above, then pin to that version.
# Otherwise, just install latest version of Chrome.
- name: Install Chrome (for e2e tests)
run: | run: |
if [[ -z "${CHROME_VERSION}" ]]
then
echo "Installing latest stable version"
sudo apt-get update sudo apt-get update
sudo apt-get --only-upgrade install google-chrome-stable -y sudo apt-get --only-upgrade install google-chrome-stable -y
else
echo "Installing version ${CHROME_VERSION}"
wget -q "https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb"
sudo dpkg -i "google-chrome-stable_${CHROME_VERSION}_amd64.deb"
fi
google-chrome --version google-chrome --version
# https://github.com/actions/cache/blob/main/examples.md#node---yarn # https://github.com/actions/cache/blob/main/examples.md#node---yarn
@@ -53,8 +66,11 @@ jobs:
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: ${{ runner.os }}-yarn- restore-keys: ${{ runner.os }}-yarn-
- name: Install the latest chromedriver compatible with the installed chrome version - name: Install latest ChromeDriver compatible with installed Chrome
run: yarn global add chromedriver --detect_chromedriver_version # needs to be npm, the --detect_chromedriver_version flag doesn't work with yarn global
run: |
npm install -g chromedriver --detect_chromedriver_version
chromedriver -v
- name: Install Yarn dependencies - name: Install Yarn dependencies
run: yarn install --frozen-lockfile run: yarn install --frozen-lockfile

View File

@@ -47,6 +47,7 @@
"src/robots.txt" "src/robots.txt"
], ],
"styles": [ "styles": [
"src/styles/startup.scss",
{ {
"input": "src/styles/base-theme.scss", "input": "src/styles/base-theme.scss",
"inject": false, "inject": false,

View File

@@ -8,7 +8,11 @@ import { ItemPageFieldComponent } from '../item-page-field.component';
templateUrl: '../item-page-field.component.html' templateUrl: '../item-page-field.component.html'
}) })
/** /**
* This component is used for displaying the author (dc.contributor.author, dc.creator and dc.contributor) metadata of an item * This component is used for displaying the author (dc.contributor.author, dc.creator and
* dc.contributor) metadata of an item.
*
* Note that it purely deals with metadata. It won't turn related Person authors into links to their
* item page. For that use a {@link MetadataRepresentationListComponent} instead.
*/ */
export class ItemPageAuthorFieldComponent extends ItemPageFieldComponent { export class ItemPageAuthorFieldComponent extends ItemPageFieldComponent {

View File

@@ -18,7 +18,12 @@
</ng-container> </ng-container>
<ds-item-page-file-section [item]="object"></ds-item-page-file-section> <ds-item-page-file-section [item]="object"></ds-item-page-file-section>
<ds-item-page-date-field [item]="object"></ds-item-page-date-field> <ds-item-page-date-field [item]="object"></ds-item-page-date-field>
<ds-item-page-author-field [item]="object"></ds-item-page-author-field> <ds-metadata-representation-list class="ds-item-page-mixed-author-field"
[parentItem]="object"
[itemType]="'Person'"
[metadataFields]="['dc.contributor.author', 'dc.creator']"
[label]="'relationships.isAuthorOf' | translate">
</ds-metadata-representation-list>
<ds-generic-item-page-field [item]="object" <ds-generic-item-page-field [item]="object"
[fields]="['journal.title']" [fields]="['journal.title']"
[label]="'publication.page.journal-title'"> [label]="'publication.page.journal-title'">
@@ -37,12 +42,6 @@
</ds-generic-item-page-field> </ds-generic-item-page-field>
</div> </div>
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-6">
<ds-metadata-representation-list
[parentItem]="object"
[itemType]="'Person'"
[metadataField]="'dc.contributor.author'"
[label]="'relationships.isAuthorOf' | translate">
</ds-metadata-representation-list>
<ds-related-items <ds-related-items
[parentItem]="object" [parentItem]="object"
[relationType]="'isProjectOfPublication'" [relationType]="'isProjectOfPublication'"

View File

@@ -88,9 +88,14 @@ describe('PublicationComponent', () => {
expect(fields.length).toBeGreaterThanOrEqual(1); expect(fields.length).toBeGreaterThanOrEqual(1);
}); });
it('should contain a component to display the author', () => { it('should not contain a metadata only author field', () => {
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-author-field')); const fields = fixture.debugElement.queryAll(By.css('ds-item-page-author-field'));
expect(fields.length).toBeGreaterThanOrEqual(1); expect(fields.length).toBe(0);
});
it('should contain a mixed metadata and relationship field for authors', () => {
const fields = fixture.debugElement.queryAll(By.css('.ds-item-page-mixed-author-field'));
expect(fields.length).toBe(1);
}); });
it('should contain a component to display the abstract', () => { it('should contain a component to display the abstract', () => {

View File

@@ -18,7 +18,12 @@
</ng-container> </ng-container>
<ds-item-page-file-section [item]="object"></ds-item-page-file-section> <ds-item-page-file-section [item]="object"></ds-item-page-file-section>
<ds-item-page-date-field [item]="object"></ds-item-page-date-field> <ds-item-page-date-field [item]="object"></ds-item-page-date-field>
<ds-item-page-author-field [item]="object"></ds-item-page-author-field> <ds-metadata-representation-list class="ds-item-page-mixed-author-field"
[parentItem]="object"
[itemType]="'Person'"
[metadataFields]="['dc.contributor.author', 'dc.creator']"
[label]="'relationships.isAuthorOf' | translate">
</ds-metadata-representation-list>
<ds-generic-item-page-field [item]="object" <ds-generic-item-page-field [item]="object"
[fields]="['journal.title']" [fields]="['journal.title']"
[label]="'item.page.journal-title'"> [label]="'item.page.journal-title'">
@@ -37,12 +42,6 @@
</ds-generic-item-page-field> </ds-generic-item-page-field>
</div> </div>
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-6">
<ds-metadata-representation-list
[parentItem]="object"
[itemType]="'Person'"
[metadataField]="'dc.contributor.author'"
[label]="'relationships.isAuthorOf' | translate">
</ds-metadata-representation-list>
<ds-item-page-abstract-field [item]="object"></ds-item-page-abstract-field> <ds-item-page-abstract-field [item]="object"></ds-item-page-abstract-field>
<ds-generic-item-page-field [item]="object" <ds-generic-item-page-field [item]="object"
[fields]="['dc.description']" [fields]="['dc.description']"

View File

@@ -89,9 +89,14 @@ describe('UntypedItemComponent', () => {
expect(fields.length).toBeGreaterThanOrEqual(1); expect(fields.length).toBeGreaterThanOrEqual(1);
}); });
it('should contain a component to display the author', () => { it('should not contain a metadata only author field', () => {
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-author-field')); const fields = fixture.debugElement.queryAll(By.css('ds-item-page-author-field'));
expect(fields.length).toBeGreaterThanOrEqual(1); expect(fields.length).toBe(0);
});
it('should contain a mixed metadata and relationship field for authors', () => {
const fields = fixture.debugElement.queryAll(By.css('.ds-item-page-mixed-author-field'));
expect(fields.length).toBe(1);
}); });
it('should contain a component to display the abstract', () => { it('should contain a component to display the abstract', () => {

View File

@@ -5,13 +5,13 @@ import { MetadataRepresentationListComponent } from './metadata-representation-l
import { RelationshipService } from '../../../core/data/relationship.service'; import { RelationshipService } from '../../../core/data/relationship.service';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$, createFailedRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { VarDirective } from '../../../shared/utils/var.directive'; import { VarDirective } from '../../../shared/utils/var.directive';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
const itemType = 'Person'; const itemType = 'Person';
const metadataField = 'dc.contributor.author'; const metadataFields = ['dc.contributor.author', 'dc.creator'];
const parentItem: Item = Object.assign(new Item(), { const parentItem: Item = Object.assign(new Item(), {
id: 'parent-item', id: 'parent-item',
metadata: { metadata: {
@@ -28,6 +28,20 @@ const parentItem: Item = Object.assign(new Item(), {
place: 1 place: 1
} }
], ],
'dc.creator': [
{
language: null,
value: 'Related Creator with authority',
authority: 'virtual::related-creator',
place: 3,
},
{
language: null,
value: 'Related Creator with authority - unauthorized',
authority: 'virtual::related-creator-unauthorized',
place: 4,
},
],
'dc.title': [ 'dc.title': [
{ {
language: null, language: null,
@@ -47,21 +61,49 @@ const relatedAuthor: Item = Object.assign(new Item(), {
] ]
} }
}); });
const relation: Relationship = Object.assign(new Relationship(), { const relatedCreator: Item = Object.assign(new Item(), {
id: 'related-creator',
metadata: {
'dc.title': [
{
language: null,
value: 'Related Creator'
}
],
'dspace.entity.type': 'Person',
}
});
const authorRelation: Relationship = Object.assign(new Relationship(), {
leftItem: createSuccessfulRemoteDataObject$(parentItem), leftItem: createSuccessfulRemoteDataObject$(parentItem),
rightItem: createSuccessfulRemoteDataObject$(relatedAuthor) rightItem: createSuccessfulRemoteDataObject$(relatedAuthor)
}); });
let relationshipService: RelationshipService; const creatorRelation: Relationship = Object.assign(new Relationship(), {
leftItem: createSuccessfulRemoteDataObject$(parentItem),
rightItem: createSuccessfulRemoteDataObject$(relatedCreator),
});
const creatorRelationUnauthorized: Relationship = Object.assign(new Relationship(), {
leftItem: createSuccessfulRemoteDataObject$(parentItem),
rightItem: createFailedRemoteDataObject$('Unauthorized', 401),
});
let relationshipService;
describe('MetadataRepresentationListComponent', () => { describe('MetadataRepresentationListComponent', () => {
let comp: MetadataRepresentationListComponent; let comp: MetadataRepresentationListComponent;
let fixture: ComponentFixture<MetadataRepresentationListComponent>; let fixture: ComponentFixture<MetadataRepresentationListComponent>;
relationshipService = jasmine.createSpyObj('relationshipService', relationshipService = {
{ findById: (id: string) => {
findById: createSuccessfulRemoteDataObject$(relation) if (id === 'related-author') {
return createSuccessfulRemoteDataObject$(authorRelation);
} }
); if (id === 'related-creator') {
return createSuccessfulRemoteDataObject$(creatorRelation);
}
if (id === 'related-creator-unauthorized') {
return createSuccessfulRemoteDataObject$(creatorRelationUnauthorized);
}
},
};
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -81,13 +123,13 @@ describe('MetadataRepresentationListComponent', () => {
comp = fixture.componentInstance; comp = fixture.componentInstance;
comp.parentItem = parentItem; comp.parentItem = parentItem;
comp.itemType = itemType; comp.itemType = itemType;
comp.metadataField = metadataField; comp.metadataFields = metadataFields;
fixture.detectChanges(); fixture.detectChanges();
})); }));
it('should load 2 ds-metadata-representation-loader components', () => { it('should load 4 ds-metadata-representation-loader components', () => {
const fields = fixture.debugElement.queryAll(By.css('ds-metadata-representation-loader')); const fields = fixture.debugElement.queryAll(By.css('ds-metadata-representation-loader'));
expect(fields.length).toBe(2); expect(fields.length).toBe(4);
}); });
it('should contain one page of items', () => { it('should contain one page of items', () => {

View File

@@ -42,7 +42,7 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList
/** /**
* The metadata field to use for fetching metadata from the item * The metadata field to use for fetching metadata from the item
*/ */
@Input() metadataField: string; @Input() metadataFields: string[];
/** /**
* An i18n label to use as a title for the list * An i18n label to use as a title for the list
@@ -70,7 +70,7 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList
* @param page The page to fetch * @param page The page to fetch
*/ */
getPage(page: number): Observable<MetadataRepresentation[]> { getPage(page: number): Observable<MetadataRepresentation[]> {
const metadata = this.parentItem.findMetadataSortedByPlace(this.metadataField); const metadata = this.parentItem.findMetadataSortedByPlace(this.metadataFields);
this.total = metadata.length; this.total = metadata.length;
return this.resolveMetadataRepresentations(metadata, page); return this.resolveMetadataRepresentations(metadata, page);
} }
@@ -91,9 +91,11 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
switchMap((relRD: RemoteData<Relationship>) => switchMap((relRD: RemoteData<Relationship>) =>
observableCombineLatest(relRD.payload.leftItem, relRD.payload.rightItem).pipe( observableCombineLatest(relRD.payload.leftItem, relRD.payload.rightItem).pipe(
filter(([leftItem, rightItem]) => leftItem.hasSucceeded && rightItem.hasSucceeded), filter(([leftItem, rightItem]) => leftItem.hasCompleted && rightItem.hasCompleted),
map(([leftItem, rightItem]) => { map(([leftItem, rightItem]) => {
if (leftItem.payload.id === this.parentItem.id) { if (!leftItem.hasSucceeded || !rightItem.hasSucceeded) {
return observableOf(Object.assign(new MetadatumRepresentation(this.itemType), metadatum));
} else if (rightItem.hasSucceeded && leftItem.payload.id === this.parentItem.id) {
return rightItem.payload; return rightItem.payload;
} else if (rightItem.payload.id === this.parentItem.id) { } else if (rightItem.payload.id === this.parentItem.id) {
return leftItem.payload; return leftItem.payload;

View File

@@ -1 +1,3 @@
<ds-themed-root [isNotAuthBlocking]="isNotAuthBlocking$ | async" [isLoading]="isLoading$ | async"></ds-themed-root> <ds-themed-root
[shouldShowFullscreenLoader]="(isAuthBlocking$ | async) || (isThemeLoading$ | async)"
[shouldShowRouteLoader]="isRouteLoading$ | async"></ds-themed-root>

View File

@@ -1,4 +1,4 @@
import { delay, map, distinctUntilChanged, filter, take } from 'rxjs/operators'; import { delay, distinctUntilChanged, filter, take } from 'rxjs/operators';
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
@@ -7,6 +7,7 @@ import {
Inject, Inject,
OnInit, OnInit,
Optional, Optional,
PLATFORM_ID,
} from '@angular/core'; } from '@angular/core';
import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router'; import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router';
@@ -32,7 +33,7 @@ import { LocaleService } from './core/locale/locale.service';
import { hasValue, isNotEmpty } from './shared/empty.util'; import { hasValue, isNotEmpty } from './shared/empty.util';
import { KlaroService } from './shared/cookies/klaro.service'; import { KlaroService } from './shared/cookies/klaro.service';
import { GoogleAnalyticsService } from './statistics/google-analytics.service'; import { GoogleAnalyticsService } from './statistics/google-analytics.service';
import { DOCUMENT } from '@angular/common'; import { DOCUMENT, isPlatformBrowser } from '@angular/common';
import { ThemeService } from './shared/theme-support/theme.service'; import { ThemeService } from './shared/theme-support/theme.service';
import { BASE_THEME_NAME } from './shared/theme-support/theme.constants'; import { BASE_THEME_NAME } from './shared/theme-support/theme.constants';
import { DEFAULT_THEME_CONFIG } from './shared/theme-support/theme.effects'; import { DEFAULT_THEME_CONFIG } from './shared/theme-support/theme.effects';
@@ -45,7 +46,6 @@ import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class AppComponent implements OnInit, AfterViewInit { export class AppComponent implements OnInit, AfterViewInit {
isLoading$: BehaviorSubject<boolean> = new BehaviorSubject(true);
sidebarVisible: Observable<boolean>; sidebarVisible: Observable<boolean>;
slideSidebarOver: Observable<boolean>; slideSidebarOver: Observable<boolean>;
collapsedSidebarWidth: Observable<string>; collapsedSidebarWidth: Observable<string>;
@@ -57,11 +57,23 @@ export class AppComponent implements OnInit, AfterViewInit {
/** /**
* Whether or not the authentication is currently blocking the UI * Whether or not the authentication is currently blocking the UI
*/ */
isNotAuthBlocking$: Observable<boolean>; isAuthBlocking$: Observable<boolean>;
/**
* Whether or not the app is in the process of rerouting
*/
isRouteLoading$: BehaviorSubject<boolean> = new BehaviorSubject(true);
/**
* Whether or not the theme is in the process of being swapped
*/
isThemeLoading$: BehaviorSubject<boolean> = new BehaviorSubject(false);
constructor( constructor(
@Inject(NativeWindowService) private _window: NativeWindowRef, @Inject(NativeWindowService) private _window: NativeWindowRef,
@Inject(DOCUMENT) private document: any, @Inject(DOCUMENT) private document: any,
@Inject(PLATFORM_ID) private platformId: any,
private themeService: ThemeService, private themeService: ThemeService,
private translate: TranslateService, private translate: TranslateService,
private store: Store<HostWindowState>, private store: Store<HostWindowState>,
@@ -83,6 +95,10 @@ export class AppComponent implements OnInit, AfterViewInit {
this.models = models; this.models = models;
this.themeService.getThemeName$().subscribe((themeName: string) => { this.themeService.getThemeName$().subscribe((themeName: string) => {
if (isPlatformBrowser(this.platformId)) {
// the theme css will never download server side, so this should only happen on the browser
this.isThemeLoading$.next(true);
}
if (hasValue(themeName)) { if (hasValue(themeName)) {
this.setThemeCss(themeName); this.setThemeCss(themeName);
} else if (hasValue(DEFAULT_THEME_CONFIG)) { } else if (hasValue(DEFAULT_THEME_CONFIG)) {
@@ -118,13 +134,12 @@ export class AppComponent implements OnInit, AfterViewInit {
} }
ngOnInit() { ngOnInit() {
this.isNotAuthBlocking$ = this.store.pipe(select(isAuthenticationBlocking)).pipe( this.isAuthBlocking$ = this.store.pipe(select(isAuthenticationBlocking)).pipe(
map((isBlocking: boolean) => isBlocking === false),
distinctUntilChanged() distinctUntilChanged()
); );
this.isNotAuthBlocking$ this.isAuthBlocking$
.pipe( .pipe(
filter((notBlocking: boolean) => notBlocking), filter((isBlocking: boolean) => isBlocking === false),
take(1) take(1)
).subscribe(() => this.initializeKlaro()); ).subscribe(() => this.initializeKlaro());
@@ -156,12 +171,12 @@ export class AppComponent implements OnInit, AfterViewInit {
delay(0) delay(0)
).subscribe((event) => { ).subscribe((event) => {
if (event instanceof NavigationStart) { if (event instanceof NavigationStart) {
this.isLoading$.next(true); this.isRouteLoading$.next(true);
} else if ( } else if (
event instanceof NavigationEnd || event instanceof NavigationEnd ||
event instanceof NavigationCancel event instanceof NavigationCancel
) { ) {
this.isLoading$.next(false); this.isRouteLoading$.next(false);
} }
}); });
} }
@@ -209,6 +224,8 @@ export class AppComponent implements OnInit, AfterViewInit {
} }
}); });
} }
// the fact that this callback is used, proves we're on the browser.
this.isThemeLoading$.next(false);
}; };
head.appendChild(link); head.appendChild(link);
} }

View File

@@ -127,7 +127,8 @@ describe('BrowseService', () => {
}); });
describe('getBrowseEntriesFor and findList', () => { describe('getBrowseEntriesFor and findList', () => {
const mockAuthorName = 'Donald Smith'; // should contain special characters such that url encoding can be tested as well
const mockAuthorName = 'Donald Smith & Sons';
beforeEach(() => { beforeEach(() => {
requestService = getMockRequestService(getRequestEntry$(true)); requestService = getMockRequestService(getRequestEntry$(true));
@@ -152,7 +153,7 @@ describe('BrowseService', () => {
describe('when findList is called with a valid browse definition id', () => { describe('when findList is called with a valid browse definition id', () => {
it('should call hrefOnlyDataService.findAllByHref with the expected href', () => { it('should call hrefOnlyDataService.findAllByHref with the expected href', () => {
const expected = browseDefinitions[1]._links.items.href + '?filterValue=' + mockAuthorName; const expected = browseDefinitions[1]._links.items.href + '?filterValue=' + encodeURIComponent(mockAuthorName);
scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe()); scheduler.schedule(() => service.getBrowseItemsFor(mockAuthorName, new BrowseEntrySearchOptions(browseDefinitions[1].id)).subscribe());
scheduler.flush(); scheduler.flush();

View File

@@ -130,7 +130,7 @@ export class BrowseService {
args.push(`startsWith=${options.startsWith}`); args.push(`startsWith=${options.startsWith}`);
} }
if (isNotEmpty(filterValue)) { if (isNotEmpty(filterValue)) {
args.push(`filterValue=${filterValue}`); args.push(`filterValue=${encodeURIComponent(filterValue)}`);
} }
if (isNotEmpty(args)) { if (isNotEmpty(args)) {
href = new URLCombiner(href, `?${args.join('&')}`).toString(); href = new URLCombiner(href, `?${args.join('&')}`).toString();

View File

@@ -163,8 +163,8 @@ export class DSpaceObject extends ListableObject implements CacheableObject {
* Find metadata on a specific field and order all of them using their "place" property. * Find metadata on a specific field and order all of them using their "place" property.
* @param key * @param key
*/ */
findMetadataSortedByPlace(key: string): MetadataValue[] { findMetadataSortedByPlace(keyOrKeys: string | string[]): MetadataValue[] {
return this.allMetadata([key]).sort((a: MetadataValue, b: MetadataValue) => { return this.allMetadata(keyOrKeys).sort((a: MetadataValue, b: MetadataValue) => {
if (hasNoValue(a.place) && hasNoValue(b.place)) { if (hasNoValue(a.place) && hasNoValue(b.place)) {
return 0; return 0;
} }

View File

@@ -22,7 +22,7 @@ import { isNotEmpty } from '../../shared/empty.util';
@dataService(SUBMISSION_CC_LICENSE_URL) @dataService(SUBMISSION_CC_LICENSE_URL)
export class SubmissionCcLicenseUrlDataService extends DataService<SubmissionCcLicenceUrl> { export class SubmissionCcLicenseUrlDataService extends DataService<SubmissionCcLicenceUrl> {
protected linkPath = 'submissioncclicenseUrl-search'; protected linkPath = 'submissioncclicenseUrls-search';
constructor( constructor(
protected comparator: DefaultChangeAnalyzer<SubmissionCcLicenceUrl>, protected comparator: DefaultChangeAnalyzer<SubmissionCcLicenceUrl>,

View File

@@ -18,7 +18,7 @@
<ds-metadata-representation-list <ds-metadata-representation-list
[parentItem]="object" [parentItem]="object"
[itemType]="'OrgUnit'" [itemType]="'OrgUnit'"
[metadataField]="'project.contributor.other'" [metadataFields]="['project.contributor.other']"
[label]="'project.page.contributor' | translate"> [label]="'project.page.contributor' | translate">
</ds-metadata-representation-list> </ds-metadata-representation-list>
<ds-generic-item-page-field [item]="object" <ds-generic-item-page-field [item]="object"

View File

@@ -1,4 +1,4 @@
<div class="outer-wrapper" *ngIf="isNotAuthBlocking; else authLoader"> <div class="outer-wrapper" *ngIf="!shouldShowFullscreenLoader; else fullScreenLoader">
<ds-admin-sidebar></ds-admin-sidebar> <ds-admin-sidebar></ds-admin-sidebar>
<div class="inner-wrapper" [@slideSidebarPadding]="{ <div class="inner-wrapper" [@slideSidebarPadding]="{
value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'), value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'),
@@ -12,10 +12,10 @@
<main class="main-content"> <main class="main-content">
<ds-themed-breadcrumbs></ds-themed-breadcrumbs> <ds-themed-breadcrumbs></ds-themed-breadcrumbs>
<div class="container d-flex justify-content-center align-items-center h-100" *ngIf="isLoading"> <div class="container d-flex justify-content-center align-items-center h-100" *ngIf="shouldShowRouteLoader">
<ds-loading [showMessage]="false"></ds-loading> <ds-loading [showMessage]="false"></ds-loading>
</div> </div>
<div [class.d-none]="isLoading"> <div [class.d-none]="shouldShowRouteLoader">
<router-outlet></router-outlet> <router-outlet></router-outlet>
</div> </div>
</main> </main>
@@ -23,8 +23,8 @@
<ds-themed-footer></ds-themed-footer> <ds-themed-footer></ds-themed-footer>
</div> </div>
</div> </div>
<ng-template #authLoader> <ng-template #fullScreenLoader>
<div class="text-center ds-full-screen-loader d-flex align-items-center flex-column justify-content-center"> <div class="ds-full-screen-loader">
<ds-loading [showMessage]="false"></ds-loading> <ds-loading [showMessage]="false"></ds-loading>
</div> </div>
</ng-template> </ng-template>

View File

@@ -27,6 +27,7 @@ import { provideMockStore } from '@ngrx/store/testing';
import { RouteService } from '../core/services/route.service'; import { RouteService } from '../core/services/route.service';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { MenuServiceStub } from '../shared/testing/menu-service.stub'; import { MenuServiceStub } from '../shared/testing/menu-service.stub';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('RootComponent', () => { describe('RootComponent', () => {
let component: RootComponent; let component: RootComponent;
@@ -36,6 +37,7 @@ describe('RootComponent', () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ imports: [
CommonModule, CommonModule,
NoopAnimationsModule,
StoreModule.forRoot(authReducer, storeModuleConfig), StoreModule.forRoot(authReducer, storeModuleConfig),
TranslateModule.forRoot({ TranslateModule.forRoot({
loader: { loader: {

View File

@@ -38,14 +38,14 @@ export class RootComponent implements OnInit {
models; models;
/** /**
* Whether or not the authentication is currently blocking the UI * Whether or not to show a full screen loader
*/ */
@Input() isNotAuthBlocking: boolean; @Input() shouldShowFullscreenLoader: boolean;
/** /**
* Whether or not the the application is loading; * Whether or not to show a loader across the router outlet
*/ */
@Input() isLoading: boolean; @Input() shouldShowRouteLoader: boolean;
constructor( constructor(
@Inject(NativeWindowService) private _window: NativeWindowRef, @Inject(NativeWindowService) private _window: NativeWindowRef,

View File

@@ -11,14 +11,14 @@ export class ThemedRootComponent extends ThemedComponent<RootComponent> {
/** /**
* Whether or not the authentication is currently blocking the UI * Whether or not the authentication is currently blocking the UI
*/ */
@Input() isNotAuthBlocking: boolean; @Input() shouldShowFullscreenLoader: boolean;
/** /**
* Whether or not the the application is loading; * Whether or not the the application is loading;
*/ */
@Input() isLoading: boolean; @Input() shouldShowRouteLoader: boolean;
protected inAndOutputNames: (keyof RootComponent & keyof this)[] = ['isLoading', 'isNotAuthBlocking']; protected inAndOutputNames: (keyof RootComponent & keyof this)[] = ['shouldShowRouteLoader', 'shouldShowFullscreenLoader'];
protected getComponentName(): string { protected getComponentName(): string {
return 'RootComponent'; return 'RootComponent';

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,6 @@
<title>DSpace</title> <title>DSpace</title>
<meta name="viewport" content="width=device-width,minimum-scale=1"> <meta name="viewport" content="width=device-width,minimum-scale=1">
<link rel="icon" type="image/x-icon" href="assets/images/favicon.ico" /> <link rel="icon" type="image/x-icon" href="assets/images/favicon.ico" />
<link class="theme-css" rel="stylesheet" href="/dspace-theme.css">
</head> </head>
<body> <body>

View File

@@ -40,10 +40,6 @@ ds-admin-sidebar {
z-index: var(--ds-sidebar-z-index); z-index: var(--ds-sidebar-z-index);
} }
.ds-full-screen-loader {
height: 100vh;
}
.sticky-top { .sticky-top {
z-index: 0; z-index: 0;
} }

8
src/styles/startup.scss Normal file
View File

@@ -0,0 +1,8 @@
.ds-full-screen-loader {
height: 100vh;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}

View File

@@ -3,7 +3,7 @@
<div class="jumbotron jumbotron-fluid"> <div class="jumbotron jumbotron-fluid">
<div class="d-flex flex-wrap"> <div class="d-flex flex-wrap">
<div> <div>
<h1 class="display-3">DSpace 7 - Beta 5</h1> <h1 class="display-3">DSpace 7</h1>
<p class="lead">DSpace is the world leading open source repository platform that enables <p class="lead">DSpace is the world leading open source repository platform that enables
organisations to:</p> organisations to:</p>
</div> </div>
@@ -20,10 +20,7 @@
</li> </li>
</ul> </ul>
<p>Join an international community of <a href="https://wiki.lyrasis.org/display/DSPACE/DSpace+Positioning" target="_blank">leading institutions using DSpace</a>.</p> <p>Join an international community of <a href="https://wiki.lyrasis.org/display/DSPACE/DSpace+Positioning" target="_blank">leading institutions using DSpace</a>.</p>
<p>Participate in the <a href="https://wiki.lyrasis.org/display/DSPACE/DSpace+Release+7.0+Testathon+Page" <p>The test user accounts below have their password set to the name of this
target="_blank">official community Testathon</a>
from <strong>April 19th through May 7th</strong>. The test user accounts below have their password set to the name of
this
software in lowercase.</p> software in lowercase.</p>
<ul> <ul>
<li>Demo Site Administrator = dspacedemo+admin@gmail.com</li> <li>Demo Site Administrator = dspacedemo+admin@gmail.com</li>