Merge branch 'master' into w2p-55990_Move-item-component

This commit is contained in:
Yana De Pauw
2018-11-29 11:02:57 +01:00
19 changed files with 1806 additions and 40 deletions

View File

@@ -163,7 +163,8 @@
},
"results": {
"head": "Search Results",
"no-results": "There were no results for this search"
"no-results": "Your search returned no results. Having trouble finding what you're looking for? Try putting",
"no-results-link": "quotes around it"
},
"sidebar": {
"close": "Back to results",

View File

@@ -1,6 +1,6 @@
<div class="simple-view-element">
<h5 class="simple-view-element-header" *ngIf="label">{{ label }}</h5>
<div class="simple-view-element-body">
<ng-content></ng-content>
</div>
<div class="simple-view-element" [class.d-none]="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>
</div>
</div>

View File

@@ -0,0 +1,67 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Component, DebugElement } from '@angular/core';
import { MetadataFieldWrapperComponent } from './metadata-field-wrapper.component';
@Component({
selector: 'ds-component-with-content',
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
' <div class="my-content">\n' +
' <span></span>\n' +
' </div>\n' +
'</ds-metadata-field-wrapper>'
})
class ContentComponent {}
describe('MetadataFieldWrapperComponent', () => {
let component: MetadataFieldWrapperComponent;
let fixture: ComponentFixture<MetadataFieldWrapperComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MetadataFieldWrapperComponent, ContentComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MetadataFieldWrapperComponent);
component = fixture.componentInstance;
});
const wrapperSelector = '.simple-view-element';
const labelSelector = '.simple-view-element-header';
const contentSelector = '.my-content';
it('should create', () => {
expect(component).toBeDefined();
});
it('should not show the component when there is no content', () => {
component.label = 'test label';
fixture.detectChanges();
const parentNative = fixture.nativeElement;
const nativeWrapper = parentNative.querySelector(wrapperSelector);
expect(nativeWrapper.classList.contains('d-none')).toBe(true);
});
it('should not show the component when there is DOM content but no text', () => {
const parentFixture = TestBed.createComponent(ContentComponent);
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(ContentComponent);
parentFixture.detectChanges();
const parentNative = parentFixture.nativeElement;
const nativeContent = parentNative.querySelector(contentSelector);
nativeContent.textContent = 'lorem ipsum';
const nativeWrapper = parentNative.querySelector(wrapperSelector);
parentFixture.detectChanges();
expect(nativeWrapper.classList.contains('d-none')).toBe(false);
});
});

View File

@@ -1,4 +1,5 @@
import { Component, Input } from '@angular/core';
import { Metadatum } from '../../../core/shared/metadatum.model';
/**
* This component renders the configured 'values' into the ds-metadata-field-wrapper component.
@@ -11,7 +12,7 @@ import { Component, Input } from '@angular/core';
})
export class MetadataValuesComponent {
@Input() values: any;
@Input() values: Metadatum[];
@Input() separator: string;

View File

@@ -7,5 +7,12 @@
[hideGear]="true">
</ds-viewable-collection></div>
<ds-loading *ngIf="!searchResults || searchResults?.isLoading" message="{{'loading.search-results' | translate}}"></ds-loading>
<ds-error *ngIf="searchResults?.hasFailed" message="{{'error.search-results' | translate}}"></ds-error>
<ds-error *ngIf="searchResults?.payload?.page.length == 0" message="{{'search.results.no-results' | translate}}"></ds-error>
<ds-error *ngIf="searchResults?.hasFailed && (!searchResults?.error || searchResults?.error?.statusCode != 400)" message="{{'error.search-results' | translate}}"></ds-error>
<div *ngIf="searchResults?.payload?.page.length == 0 || searchResults?.error?.statusCode == 400">
{{ 'search.results.no-results' | translate }}
<a [routerLink]="['/search']"
[queryParams]="{ query: surroundStringWithQuotes(searchConfig?.query) }"
queryParamsHandling="merge">
{{"search.results.no-results-link" | translate}}
</a>
</div>

View File

@@ -1,40 +1,92 @@
import { ComponentFixture, TestBed, async, tick, fakeAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ResourceType } from '../../core/shared/resource-type';
import { Community } from '../../core/shared/community.model';
import { TranslateModule } from '@ngx-translate/core';
import { SearchResultsComponent } from './search-results.component';
import { QueryParamsDirectiveStub } from '../../shared/testing/query-params-directive-stub';
describe('SearchResultsComponent', () => {
let comp: SearchResultsComponent;
let fixture: ComponentFixture<SearchResultsComponent>;
let heading: DebugElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [SearchResultsComponent],
imports: [TranslateModule.forRoot(), NoopAnimationsModule],
declarations: [
SearchResultsComponent,
QueryParamsDirectiveStub],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SearchResultsComponent);
comp = fixture.componentInstance; // SearchFormComponent test instance
heading = fixture.debugElement.query(By.css('heading'));
comp = fixture.componentInstance; // SearchResultsComponent test instance
});
it('should display heading when results are not empty', fakeAsync(() => {
(comp as any).searchResults = 'test';
(comp as any).searchConfig = {pagination: ''};
it('should display results when results are not empty', () => {
(comp as any).searchResults = { hasSucceeded: true, isLoading: false, payload: { page: { length: 2 } } };
(comp as any).searchConfig = {};
fixture.detectChanges();
tick();
expect(heading).toBeDefined();
}));
expect(fixture.debugElement.query(By.css('ds-viewable-collection'))).not.toBeNull();
});
it('should not display heading when results is empty', () => {
expect(heading).toBeNull();
it('should not display link when results are not empty', () => {
(comp as any).searchResults = { hasSucceeded: true, isLoading: false, payload: { page: { length: 2 } } };
(comp as any).searchConfig = {};
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('a'))).toBeNull();
});
it('should display error message if error is != 400', () => {
(comp as any).searchResults = { hasFailed: true, error: { statusCode: 500 } };
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('ds-error'))).not.toBeNull();
});
it('should display link with new search where query is quoted if search return a error 400', () => {
(comp as any).searchResults = { hasFailed: true, error: { statusCode: 400 } };
(comp as any).searchConfig = { query: 'foobar' };
fixture.detectChanges();
const linkDes = fixture.debugElement.queryAll(By.directive(QueryParamsDirectiveStub));
// get attached link directive instances
// using each DebugElement's injector
const routerLinkQuery = linkDes.map((de) => de.injector.get(QueryParamsDirectiveStub));
expect(routerLinkQuery.length).toBe(1, 'should have 1 router link with query params');
expect(routerLinkQuery[0].queryParams.query).toBe('"foobar"', 'query params should be "foobar"');
});
it('should display link with new search where query is quoted if search result is empty', () => {
(comp as any).searchResults = { payload: { page: { length: 0 } } };
(comp as any).searchConfig = { query: 'foobar' };
fixture.detectChanges();
const linkDes = fixture.debugElement.queryAll(By.directive(QueryParamsDirectiveStub));
// get attached link directive instances
// using each DebugElement's injector
const routerLinkQuery = linkDes.map((de) => de.injector.get(QueryParamsDirectiveStub));
expect(routerLinkQuery.length).toBe(1, 'should have 1 router link with query params');
expect(routerLinkQuery[0].queryParams.query).toBe('"foobar"', 'query params should be "foobar"');
});
it('should add quotes around the given string', () => {
expect(comp.surroundStringWithQuotes('teststring')).toEqual('"teststring"');
});
it('should not add quotes around the given string if they are already there', () => {
expect(comp.surroundStringWithQuotes('"teststring"')).toEqual('"teststring"');
});
it('should not add quotes around a given empty string', () => {
expect(comp.surroundStringWithQuotes('')).toEqual('');
});
});

View File

@@ -6,6 +6,7 @@ import { SearchOptions } from '../search-options.model';
import { SearchResult } from '../search-result.model';
import { PaginatedList } from '../../core/data/paginated-list';
import { ViewMode } from '../../core/shared/view-mode.model';
import { isNotEmpty } from '../../shared/empty.util';
@Component({
selector: 'ds-search-results',
@@ -35,4 +36,16 @@ export class SearchResultsComponent {
*/
@Input() viewMode: ViewMode;
/**
* Method to change the given string by surrounding it by quotes if not already present.
*/
surroundStringWithQuotes(input: string): string {
let result = input;
if (isNotEmpty(result) && !(result.startsWith('\"') && result.endsWith('\"'))) {
result = `"${result}"`;
}
return result;
}
}

View File

@@ -340,14 +340,10 @@ export class AuthService {
this.getRedirectUrl()
.first()
.subscribe((redirectUrl) => {
if (isNotEmpty(redirectUrl)) {
this.clearRedirectUrl();
// override the route reuse strategy
this.router.routeReuseStrategy.shouldReuseRoute = () => {
return false;
};
this.router.navigated = false;
this.router.onSameUrlNavigation = 'reload';
const url = decodeURIComponent(redirectUrl);
this.router.navigateByUrl(url);
/* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */

View File

@@ -0,0 +1,59 @@
import { RemoteDataBuildService } from './remote-data-build.service';
import { Item } from '../../shared/item.model';
import { PaginatedList } from '../../data/paginated-list';
import { PageInfo } from '../../shared/page-info.model';
import { RemoteData } from '../../data/remote-data';
import { Observable } from 'rxjs/Observable';
const pageInfo = new PageInfo();
const array = [
Object.assign(new Item(), {
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'Item nr 1'
}]
}),
Object.assign(new Item(), {
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'Item nr 2'
}]
})
];
const paginatedList = new PaginatedList(pageInfo, array);
const arrayRD = new RemoteData(false, false, true, undefined, array);
const paginatedListRD = new RemoteData(false, false, true, undefined, paginatedList);
describe('RemoteDataBuildService', () => {
let service: RemoteDataBuildService;
beforeEach(() => {
service = new RemoteDataBuildService(undefined, undefined, undefined);
});
describe('when toPaginatedList is called', () => {
let expected: RemoteData<PaginatedList<Item>>;
beforeEach(() => {
expected = paginatedListRD;
});
it('should return the correct remoteData of a paginatedList when the input is a (remoteData of an) array', () => {
const result = (service as any).toPaginatedList(Observable.of(arrayRD), pageInfo);
result.subscribe((resultRD) => {
expect(resultRD).toEqual(expected);
});
});
it('should return the correct remoteData of a paginatedList when the input is a (remoteData of a) paginated list', () => {
const result = (service as any).toPaginatedList(Observable.of(paginatedListRD), pageInfo);
result.subscribe((resultRD) => {
expect(resultRD).toEqual(expected);
});
});
});
});

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { distinctUntilChanged, flatMap, map, startWith } from 'rxjs/operators';
import { distinctUntilChanged, filter, flatMap, map, startWith, switchMap } from 'rxjs/operators';
import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util';
import { PaginatedList } from '../../data/paginated-list';
import { RemoteData } from '../../data/remote-data';
@@ -190,7 +190,7 @@ export class RemoteDataBuildService {
}
if (hasValue(normalized[relationship].page)) {
links[relationship] = this.aggregatePaginatedList(result, normalized[relationship].pageInfo);
links[relationship] = this.toPaginatedList(result, normalized[relationship].pageInfo);
} else {
links[relationship] = result;
}
@@ -254,8 +254,14 @@ export class RemoteDataBuildService {
})
}
aggregatePaginatedList<T>(input: Observable<RemoteData<T[]>>, pageInfo: PageInfo): Observable<RemoteData<PaginatedList<T>>> {
return input.map((rd) => Object.assign(rd, {payload: new PaginatedList(pageInfo, rd.payload)}));
private toPaginatedList<T>(input: Observable<RemoteData<T[] | PaginatedList<T>>>, pageInfo: PageInfo): Observable<RemoteData<PaginatedList<T>>> {
return input.map((rd: RemoteData<T[] | PaginatedList<T>>) => {
if (Array.isArray(rd.payload)) {
return Object.assign(rd, { payload: new PaginatedList(pageInfo, rd.payload) })
} else {
return Object.assign(rd, { payload: new PaginatedList(pageInfo, rd.payload.page) });
}
});
}
}

View File

@@ -6,7 +6,6 @@ import { ObjectCacheService } from '../cache/object-cache.service';
import { GlobalConfig } from '../../../config/global-config.interface';
import { GenericConstructor } from '../shared/generic-constructor';
import { PaginatedList } from './paginated-list';
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { ResourceType } from '../shared/resource-type';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
@@ -15,7 +14,7 @@ function isObjectLevel(halObj: any) {
}
function isPaginatedResponse(halObj: any) {
return isNotEmpty(halObj.page) && hasValue(halObj._embedded);
return hasValue(halObj.page) && hasValue(halObj._embedded);
}
/* tslint:disable:max-classes-per-file */
@@ -130,7 +129,7 @@ export abstract class BaseResponseParsingService {
}
processPageInfo(payload: any): PageInfo {
if (isNotEmpty(payload.page)) {
if (hasValue(payload.page)) {
const pageObj = Object.assign({}, payload.page, { _links: payload._links });
const pageInfoObject = new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
if (pageInfoObject.currentPage >= 0) {

View File

@@ -34,6 +34,9 @@ import { MockItem } from '../../shared/mocks/mock-item';
import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader';
import { BrowseService } from '../browse/browse.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { PaginatedList } from '../data/paginated-list';
import { PageInfo } from '../shared/page-info.model';
import { EmptyError } from 'rxjs/util/EmptyError';
/* tslint:disable:max-classes-per-file */
@Component({
@@ -181,6 +184,22 @@ describe('MetadataService', () => {
expect(tagStore.get('description')[0].content).toEqual('This is a dummy item component for testing!');
}));
describe('when the item has no bitstreams', () => {
beforeEach(() => {
spyOn(MockItem, 'getFiles').and.returnValue(Observable.of([]));
});
it('processRemoteData should not produce an EmptyError', fakeAsync(() => {
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(MockItem));
spyOn(metadataService, 'processRemoteData').and.callThrough();
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
tick();
expect(metadataService.processRemoteData).not.toThrow(new EmptyError());
}));
});
const mockRemoteData = (mockItem: Item): Observable<RemoteData<Item>> => {
return Observable.of(new RemoteData<Item>(
false,

View File

@@ -269,8 +269,11 @@ export class MetadataService {
private setCitationPdfUrlTag(): void {
if (this.currentObject.value instanceof Item) {
const item = this.currentObject.value as Item;
item.getFiles().filter((files) => isNotEmpty(files)).first().subscribe((bitstreams: Bitstream[]) => {
for (const bitstream of bitstreams) {
item.getFiles()
.first((files) => isNotEmpty(files))
.catch((error) => { console.debug(error); return [] })
.subscribe((bitstreams: Bitstream[]) => {
for (const bitstream of bitstreams) {
bitstream.format.first()
.map((rd: RemoteData<BitstreamFormat>) => rd.payload)
.filter((format: BitstreamFormat) => hasValue(format))

View File

@@ -1,4 +1,5 @@
import { Observable } from 'rxjs/Observable';
import { filter, map, startWith, tap } from 'rxjs/operators';
import { DSpaceObject } from './dspace-object.model';
import { Collection } from './collection.model';

View File

@@ -59,7 +59,7 @@ export const toDSpaceObjectListRD = () =>
source.pipe(
map((rd: RemoteData<PaginatedList<SearchResult<T>>>) => {
const dsoPage: T[] = rd.payload.page.map((searchResult: SearchResult<T>) => searchResult.dspaceObject);
const payload = Object.assign(rd.payload, { page: dsoPage }) as PaginatedList<T>;
const payload = Object.assign(rd.payload, { page: dsoPage }) as any;
return Object.assign(rd, {payload: payload});
})
);

View File

@@ -3,6 +3,7 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs/Subscription';
import { hasValue } from '../empty.util';
@Component({
selector: 'ds-loading',
@@ -28,7 +29,7 @@ export class LoadingComponent implements OnDestroy, OnInit {
}
ngOnDestroy() {
if (this.subscription !== undefined) {
if (hasValue(this.subscription)) {
this.subscription.unsubscribe();
}
}

View File

@@ -0,0 +1,10 @@
import { Directive, Input } from '@angular/core';
/* tslint:disable:directive-class-suffix */
@Directive({
// tslint:disable-next-line:directive-selector
selector: '[queryParams]',
})
export class QueryParamsDirectiveStub {
@Input('queryParams') queryParams: any;
}

View File

@@ -0,0 +1,15 @@
import { NgModule } from '@angular/core';
import { QueryParamsDirectiveStub } from './query-params-directive-stub';
/**
* This module isn't used. It serves to prevent the AoT compiler
* complaining about components/pipes/directives that were
* created only for use in tests.
* See https://github.com/angular/angular/issues/13590
*/
@NgModule({
declarations: [
QueryParamsDirectiveStub
]
})
export class TestModule {}

1516
yarn.lock

File diff suppressed because it is too large Load Diff