mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-12 12:33:07 +00:00
Merge remote-tracking branch 'atmire/mixing-text-authority-entities' into w2p-61133_Merge-345-and-master
Conflicts: src/app/+collection-page/collection-page.component.ts src/app/+collection-page/collection-page.module.ts src/app/+item-page/field-components/metadata-uri-values/metadata-uri-values.component.ts src/app/+item-page/field-components/metadata-values/metadata-values.component.ts src/app/+item-page/simple/field-components/specific-field/item-page-field.component.html src/app/+item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.html src/app/+search-page/search-service/search.service.ts src/app/core/core.module.ts src/app/core/data/base-response-parsing.service.ts src/app/core/data/data.service.ts src/app/core/data/request.service.spec.ts src/app/core/metadata/metadata.service.spec.ts src/app/core/shared/dspace-object.model.ts src/app/core/shared/metadatum.model.ts src/app/core/shared/operators.ts src/app/core/shared/resource-type.ts src/app/shared/object-list/item-list-element/item-list-element.component.html src/app/shared/object-list/item-list-element/item-list-element.component.spec.ts src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.spec.ts src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts src/app/shared/services/route.service.ts src/app/shared/shared.module.ts
This commit is contained in:
@@ -10,10 +10,10 @@ module.exports = {
|
|||||||
// The REST API server settings.
|
// The REST API server settings.
|
||||||
rest: {
|
rest: {
|
||||||
ssl: true,
|
ssl: true,
|
||||||
host: 'dspace7.4science.it',
|
host: 'dspace7-entities.atmire.com',
|
||||||
port: 443,
|
port: 443,
|
||||||
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||||
nameSpace: '/dspace-spring-rest/api'
|
nameSpace: '/rest/api'
|
||||||
},
|
},
|
||||||
// Caching settings
|
// Caching settings
|
||||||
cache: {
|
cache: {
|
||||||
|
@@ -12,8 +12,8 @@ describe('protractor App', () => {
|
|||||||
expect<any>(page.getPageTitleText()).toEqual('DSpace Angular :: Home');
|
expect<any>(page.getPageTitleText()).toEqual('DSpace Angular :: Home');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display header "Welcome to DSpace"', () => {
|
it('should contain a news section', () => {
|
||||||
page.navigateTo();
|
page.navigateTo();
|
||||||
expect<any>(page.getFirstHeaderText()).toEqual('Welcome to DSpace');
|
expect<any>(page.getHomePageNewsText()).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -9,11 +9,7 @@ export class ProtractorPage {
|
|||||||
return browser.getTitle();
|
return browser.getTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
getFirstPText() {
|
getHomePageNewsText() {
|
||||||
return element(by.xpath('//p[1]')).getText();
|
return element(by.xpath('//ds-home-news')).getText();
|
||||||
}
|
|
||||||
|
|
||||||
getFirstHeaderText() {
|
|
||||||
return element(by.xpath('//h1[1]')).getText();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -91,12 +91,14 @@
|
|||||||
},
|
},
|
||||||
"item": {
|
"item": {
|
||||||
"page": {
|
"page": {
|
||||||
"author": "Author",
|
"author": "Authors",
|
||||||
"abstract": "Abstract",
|
"abstract": "Abstract",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"uri": "URI",
|
"uri": "URI",
|
||||||
"files": "Files",
|
"files": "Files",
|
||||||
"collections": "Collections",
|
"collections": "Collections",
|
||||||
|
"subject": "Keywords",
|
||||||
|
"citation": "Citation",
|
||||||
"filesection": {
|
"filesection": {
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"name": "Name:",
|
"name": "Name:",
|
||||||
@@ -270,6 +272,84 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"relationships": {
|
||||||
|
"isPublicationOf": "Publications",
|
||||||
|
"isProjectOf": "Projects",
|
||||||
|
"isOrgUnitOf": "Org Units",
|
||||||
|
"isAuthorOf": "Authors",
|
||||||
|
"isPersonOf": "Authors",
|
||||||
|
"isJournalOf": "Journals",
|
||||||
|
"isSingleJournalOf": "Journal",
|
||||||
|
"isVolumeOf": "Volumes",
|
||||||
|
"isSingleVolumeOf": "Volume",
|
||||||
|
"isIssueOf": "Issues",
|
||||||
|
"isJournalIssueOf": "Journal Issue",
|
||||||
|
"isPublicationOfJournalIssue": "Articles"
|
||||||
|
},
|
||||||
|
"person": {
|
||||||
|
"page": {
|
||||||
|
"jobtitle": "Job Title",
|
||||||
|
"lastname": "Last Name",
|
||||||
|
"firstname": "First Name",
|
||||||
|
"email": "Email Address",
|
||||||
|
"orcid": "ORCID",
|
||||||
|
"birthdate": "Birth Date",
|
||||||
|
"staffid": "Staff ID",
|
||||||
|
"link": {
|
||||||
|
"full": "Show all metadata"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"project": {
|
||||||
|
"page": {
|
||||||
|
"status": "Status",
|
||||||
|
"id": "ID",
|
||||||
|
"expectedcompletion": "Expected Completion",
|
||||||
|
"description": "Description",
|
||||||
|
"keyword": "Keywords"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"orgunit": {
|
||||||
|
"page": {
|
||||||
|
"dateestablished": "Date established",
|
||||||
|
"city": "City",
|
||||||
|
"country": "Country",
|
||||||
|
"id": "ID",
|
||||||
|
"description": "Description"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"journal": {
|
||||||
|
"page": {
|
||||||
|
"issn": "ISSN",
|
||||||
|
"publisher": "Publisher",
|
||||||
|
"description": "Description",
|
||||||
|
"editor": "Editor-in-Chief"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"journalvolume": {
|
||||||
|
"page": {
|
||||||
|
"volume": "Volume",
|
||||||
|
"issuedate": "Issue Date",
|
||||||
|
"description": "Description"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"journalissue": {
|
||||||
|
"page": {
|
||||||
|
"number": "Number",
|
||||||
|
"issuedate": "Issue Date",
|
||||||
|
"description": "Description",
|
||||||
|
"keyword": "Keywords",
|
||||||
|
"journal-title": "Journal Title",
|
||||||
|
"journal-issn": "Journal ISSN"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"publication": {
|
||||||
|
"page": {
|
||||||
|
"journal-title": "Journal Title",
|
||||||
|
"journal-issn": "Journal ISSN",
|
||||||
|
"volume-title": "Volume Title"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"browse": {
|
"browse": {
|
||||||
"header": "All of DSpace"
|
"header": "All of DSpace"
|
||||||
@@ -319,6 +399,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
|
"journal": {
|
||||||
|
"title": "DSpace Angular :: Journal Search",
|
||||||
|
"results": {
|
||||||
|
"head": "Journal Search Results"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"person": {
|
||||||
|
"title": "DSpace Angular :: Person Search",
|
||||||
|
"results": {
|
||||||
|
"head": "Person Search Results"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"publication": {
|
||||||
|
"title": "DSpace Angular :: Publication Search",
|
||||||
|
"results": {
|
||||||
|
"head": "Publication Search Results"
|
||||||
|
}
|
||||||
|
},
|
||||||
"title": "DSpace Angular :: Search",
|
"title": "DSpace Angular :: Search",
|
||||||
"description": "",
|
"description": "",
|
||||||
"form": {
|
"form": {
|
||||||
@@ -355,7 +453,8 @@
|
|||||||
"f.dateIssued.min": "Start date",
|
"f.dateIssued.min": "Start date",
|
||||||
"f.dateIssued.max": "End date",
|
"f.dateIssued.max": "End date",
|
||||||
"f.subject": "Subject",
|
"f.subject": "Subject",
|
||||||
"f.has_content_in_original_bundle": "Has files"
|
"f.has_content_in_original_bundle": "Has files",
|
||||||
|
"f.entityType": "Item Type"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"show-more": "Show more",
|
"show-more": "Show more",
|
||||||
@@ -383,6 +482,10 @@
|
|||||||
},
|
},
|
||||||
"has_content_in_original_bundle": {
|
"has_content_in_original_bundle": {
|
||||||
"head": "Has files"
|
"head": "Has files"
|
||||||
|
},
|
||||||
|
"entityType": {
|
||||||
|
"placeholder": "Item Type",
|
||||||
|
"head": "Item Type"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
resources/images/orgunit-placeholder.jpg
Normal file
BIN
resources/images/orgunit-placeholder.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 70 KiB |
BIN
resources/images/person-placeholder.png
Normal file
BIN
resources/images/person-placeholder.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 138 KiB |
BIN
resources/images/project-placeholder.png
Normal file
BIN
resources/images/project-placeholder.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 313 KiB |
@@ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router';
|
|||||||
import { Observable, Subscription } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
import { CollectionDataService } from '../core/data/collection-data.service';
|
import { CollectionDataService } from '../core/data/collection-data.service';
|
||||||
|
import { ItemDataService } from '../core/data/item-data.service';
|
||||||
import { PaginatedList } from '../core/data/paginated-list';
|
import { PaginatedList } from '../core/data/paginated-list';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ import { Item } from '../core/shared/item.model';
|
|||||||
import { fadeIn, fadeInOut } from '../shared/animations/fade';
|
import { fadeIn, fadeInOut } from '../shared/animations/fade';
|
||||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||||
import { filter, flatMap, map, tap } from 'rxjs/operators';
|
import { combineLatest, filter, first, flatMap, map } from 'rxjs/operators';
|
||||||
import { SearchService } from '../+search-page/search-service/search.service';
|
import { SearchService } from '../+search-page/search-service/search.service';
|
||||||
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
||||||
import { toDSpaceObjectListRD } from '../core/shared/operators';
|
import { toDSpaceObjectListRD } from '../core/shared/operators';
|
||||||
@@ -42,7 +43,7 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private collectionDataService: CollectionDataService,
|
private collectionDataService: CollectionDataService,
|
||||||
private searchService: SearchService,
|
private itemDataService: ItemDataService,
|
||||||
private metadata: MetadataService,
|
private metadata: MetadataService,
|
||||||
private route: ActivatedRoute
|
private route: ActivatedRoute
|
||||||
) {
|
) {
|
||||||
@@ -56,7 +57,7 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.collectionRD$ = this.route.data.pipe(
|
this.collectionRD$ = this.route.data.pipe(
|
||||||
map((data) => data.collection),
|
map((data) => data.collection),
|
||||||
tap((data) => this.collectionId = data.payload.id)
|
first()
|
||||||
);
|
);
|
||||||
this.logoRD$ = this.collectionRD$.pipe(
|
this.logoRD$ = this.collectionRD$.pipe(
|
||||||
map((rd: RemoteData<Collection>) => rd.payload),
|
map((rd: RemoteData<Collection>) => rd.payload),
|
||||||
@@ -68,26 +69,33 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
|||||||
this.metadata.processRemoteData(this.collectionRD$);
|
this.metadata.processRemoteData(this.collectionRD$);
|
||||||
const page = +params.page || this.paginationConfig.currentPage;
|
const page = +params.page || this.paginationConfig.currentPage;
|
||||||
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
|
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
|
||||||
|
const sortDirection = +params.page || this.sortConfig.direction;
|
||||||
const pagination = Object.assign({},
|
const pagination = Object.assign({},
|
||||||
this.paginationConfig,
|
this.paginationConfig,
|
||||||
{ currentPage: page, pageSize: pageSize }
|
{ currentPage: page, pageSize: pageSize }
|
||||||
);
|
);
|
||||||
|
const sort = Object.assign({},
|
||||||
|
this.sortConfig,
|
||||||
|
{ direction: sortDirection, field: params.sortField }
|
||||||
|
);
|
||||||
|
this.collectionRD$.subscribe((rd: RemoteData<Collection>) => {
|
||||||
|
this.collectionId = rd.payload.id;
|
||||||
this.updatePage({
|
this.updatePage({
|
||||||
pagination: pagination,
|
pagination: pagination,
|
||||||
sort: this.sortConfig
|
sort: sort
|
||||||
});
|
});
|
||||||
}));
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePage(searchOptions) {
|
updatePage(searchOptions) {
|
||||||
this.itemRD$ = this.searchService.search(
|
this.itemRD$ = this.itemDataService.findAll({
|
||||||
new PaginatedSearchOptions({
|
scopeID: this.collectionId,
|
||||||
scope: this.collectionId,
|
currentPage: searchOptions.pagination.currentPage,
|
||||||
pagination: searchOptions.pagination,
|
elementsPerPage: searchOptions.pagination.pageSize,
|
||||||
sort: searchOptions.sort,
|
sort: searchOptions.sort
|
||||||
dsoType: DSpaceObjectType.ITEM
|
});
|
||||||
})).pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
@@ -7,15 +7,14 @@ import { CollectionPageComponent } from './collection-page.component';
|
|||||||
import { CollectionPageRoutingModule } from './collection-page-routing.module';
|
import { CollectionPageRoutingModule } from './collection-page-routing.module';
|
||||||
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
|
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
|
||||||
import { CollectionFormComponent } from './collection-form/collection-form.component';
|
import { CollectionFormComponent } from './collection-form/collection-form.component';
|
||||||
import { SearchPageModule } from '../+search-page/search-page.module';
|
|
||||||
import { EditCollectionPageComponent } from './edit-collection-page/edit-collection-page.component';
|
import { EditCollectionPageComponent } from './edit-collection-page/edit-collection-page.component';
|
||||||
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||||
|
import { SearchService } from '../+search-page/search-service/search.service';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
SearchPageModule,
|
|
||||||
CollectionPageRoutingModule
|
CollectionPageRoutingModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -24,6 +23,9 @@ import { DeleteCollectionPageComponent } from './delete-collection-page/delete-c
|
|||||||
EditCollectionPageComponent,
|
EditCollectionPageComponent,
|
||||||
DeleteCollectionPageComponent,
|
DeleteCollectionPageComponent,
|
||||||
CollectionFormComponent
|
CollectionFormComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
SearchService
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CollectionPageModule {
|
export class CollectionPageModule {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<div class="simple-view-element" [class.d-none]="content.textContent.trim().length === 0">
|
<div class="simple-view-element" [class.d-none]="content.textContent.trim().length === 0 && hasNoValue(content.querySelector('img'))">
|
||||||
<h5 class="simple-view-element-header" *ngIf="label">{{ label }}</h5>
|
<h5 class="simple-view-element-header" *ngIf="label">{{ label }}</h5>
|
||||||
<div #content class="simple-view-element-body">
|
<div #content class="simple-view-element-body">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
|
@@ -1,18 +1,41 @@
|
|||||||
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
|
import { Component } from '@angular/core';
|
||||||
import { By } from '@angular/platform-browser';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { Component, DebugElement } from '@angular/core';
|
|
||||||
|
|
||||||
import { MetadataFieldWrapperComponent } from './metadata-field-wrapper.component';
|
import { MetadataFieldWrapperComponent } from './metadata-field-wrapper.component';
|
||||||
|
|
||||||
|
/* tslint:disable:max-classes-per-file */
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-component-with-content',
|
selector: 'ds-component-without-content',
|
||||||
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
|
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
|
||||||
' <div class="my-content">\n' +
|
|
||||||
' <span></span>\n' +
|
|
||||||
' </div>\n' +
|
|
||||||
'</ds-metadata-field-wrapper>'
|
'</ds-metadata-field-wrapper>'
|
||||||
})
|
})
|
||||||
class ContentComponent {}
|
class NoContentComponent {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-component-with-empty-spans',
|
||||||
|
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
|
||||||
|
' <span></span>\n' +
|
||||||
|
' <span></span>\n' +
|
||||||
|
'</ds-metadata-field-wrapper>'
|
||||||
|
})
|
||||||
|
class SpanContentComponent {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-component-with-text',
|
||||||
|
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
|
||||||
|
' <span>The quick brown fox jumps over the lazy dog</span>\n' +
|
||||||
|
'</ds-metadata-field-wrapper>'
|
||||||
|
})
|
||||||
|
class TextContentComponent {}
|
||||||
|
|
||||||
|
@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', () => {
|
describe('MetadataFieldWrapperComponent', () => {
|
||||||
let component: MetadataFieldWrapperComponent;
|
let component: MetadataFieldWrapperComponent;
|
||||||
@@ -20,7 +43,7 @@ describe('MetadataFieldWrapperComponent', () => {
|
|||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [MetadataFieldWrapperComponent, ContentComponent]
|
declarations: [MetadataFieldWrapperComponent, NoContentComponent, SpanContentComponent, TextContentComponent, ImgContentComponent]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -30,23 +53,21 @@ describe('MetadataFieldWrapperComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const wrapperSelector = '.simple-view-element';
|
const wrapperSelector = '.simple-view-element';
|
||||||
const labelSelector = '.simple-view-element-header';
|
|
||||||
const contentSelector = '.my-content';
|
|
||||||
|
|
||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
expect(component).toBeDefined();
|
expect(component).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show the component when there is no content', () => {
|
it('should not show the component when there is no content', () => {
|
||||||
component.label = 'test label';
|
const parentFixture = TestBed.createComponent(NoContentComponent);
|
||||||
fixture.detectChanges();
|
parentFixture.detectChanges();
|
||||||
const parentNative = fixture.nativeElement;
|
const parentNative = parentFixture.nativeElement;
|
||||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
||||||
expect(nativeWrapper.classList.contains('d-none')).toBe(true);
|
expect(nativeWrapper.classList.contains('d-none')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show the component when there is DOM content but no text', () => {
|
it('should not show the component when there is DOM content but not text or an image', () => {
|
||||||
const parentFixture = TestBed.createComponent(ContentComponent);
|
const parentFixture = TestBed.createComponent(SpanContentComponent);
|
||||||
parentFixture.detectChanges();
|
parentFixture.detectChanges();
|
||||||
const parentNative = parentFixture.nativeElement;
|
const parentNative = parentFixture.nativeElement;
|
||||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
||||||
@@ -54,11 +75,18 @@ describe('MetadataFieldWrapperComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should show the component when there is text content', () => {
|
it('should show the component when there is text content', () => {
|
||||||
const parentFixture = TestBed.createComponent(ContentComponent);
|
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 img content', () => {
|
||||||
|
const parentFixture = TestBed.createComponent(ImgContentComponent);
|
||||||
parentFixture.detectChanges();
|
parentFixture.detectChanges();
|
||||||
const parentNative = parentFixture.nativeElement;
|
const parentNative = parentFixture.nativeElement;
|
||||||
const nativeContent = parentNative.querySelector(contentSelector);
|
|
||||||
nativeContent.textContent = 'lorem ipsum';
|
|
||||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
||||||
parentFixture.detectChanges();
|
parentFixture.detectChanges();
|
||||||
expect(nativeWrapper.classList.contains('d-none')).toBe(false);
|
expect(nativeWrapper.classList.contains('d-none')).toBe(false);
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { hasNoValue } from '../../../shared/empty.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders any content inside this wrapper.
|
* This component renders any content inside this wrapper.
|
||||||
@@ -11,6 +12,15 @@ import { Component, Input } from '@angular/core';
|
|||||||
})
|
})
|
||||||
export class MetadataFieldWrapperComponent {
|
export class MetadataFieldWrapperComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The label (title) for the content
|
||||||
|
*/
|
||||||
@Input() label: string;
|
@Input() label: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make hasNoValue() available in the template
|
||||||
|
*/
|
||||||
|
hasNoValue(o: any): boolean {
|
||||||
|
return hasNoValue(o);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,97 @@
|
|||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { ChangeDetectionStrategy, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { MockTranslateLoader } from '../../../shared/mocks/mock-translate-loader';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { MetadataUriValuesComponent } from './metadata-uri-values.component';
|
||||||
|
import { isNotEmpty } from '../../../shared/empty.util';
|
||||||
|
|
||||||
|
let comp: MetadataUriValuesComponent;
|
||||||
|
let fixture: ComponentFixture<MetadataUriValuesComponent>;
|
||||||
|
|
||||||
|
const mockMetadata = [
|
||||||
|
{
|
||||||
|
key: 'dc.identifier.uri',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'http://fakelink.org'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'dc.identifier.uri',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'http://another.fakelink.org'
|
||||||
|
}];
|
||||||
|
const mockSeperator = '<br/>';
|
||||||
|
const mockLabel = 'fake.message';
|
||||||
|
const mockLinkText = 'fake link text';
|
||||||
|
|
||||||
|
describe('MetadataUriValuesComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [MetadataUriValuesComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(MetadataUriValuesComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
fixture = TestBed.createComponent(MetadataUriValuesComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.values = mockMetadata;
|
||||||
|
comp.separator = mockSeperator;
|
||||||
|
comp.label = mockLabel;
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should display all metadata values', () => {
|
||||||
|
const innerHTML = fixture.nativeElement.innerHTML;
|
||||||
|
for (const metadatum of mockMetadata) {
|
||||||
|
expect(innerHTML).toContain(metadatum.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain the correct hrefs', () => {
|
||||||
|
const links = fixture.debugElement.queryAll(By.css('a'));
|
||||||
|
for (const metadatum of mockMetadata) {
|
||||||
|
expect(containsHref(links, metadatum.value)).toBeTruthy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain separators equal to the amount of metadata values minus one', () => {
|
||||||
|
const separators = fixture.debugElement.queryAll(By.css('a span'));
|
||||||
|
expect(separators.length).toBe(mockMetadata.length - 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when linktext is defined', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.linktext = mockLinkText;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace the metadata value with the linktext', () => {
|
||||||
|
const link = fixture.debugElement.query(By.css('a'));
|
||||||
|
expect(link.nativeElement.textContent).toContain(mockLinkText);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
function containsHref(links: DebugElement[], href: string): boolean {
|
||||||
|
for (const link of links) {
|
||||||
|
const hrefAtt = link.properties.href;
|
||||||
|
if (isNotEmpty(hrefAtt)) {
|
||||||
|
if (hrefAtt === href) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
@@ -17,11 +17,24 @@ import { MetadataValue } from '../../../core/shared/metadata.models';
|
|||||||
})
|
})
|
||||||
export class MetadataUriValuesComponent extends MetadataValuesComponent {
|
export class MetadataUriValuesComponent extends MetadataValuesComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional text to replace the links with
|
||||||
|
* If undefined, the metadata value (uri) is displayed
|
||||||
|
*/
|
||||||
@Input() linktext: any;
|
@Input() linktext: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The metadata values to display
|
||||||
|
*/
|
||||||
@Input() mdValues: MetadataValue[];
|
@Input() mdValues: MetadataValue[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The seperator used to split the metadata values (can contain HTML)
|
||||||
|
*/
|
||||||
@Input() separator: string;
|
@Input() separator: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The label for this iteration of metadata values
|
||||||
|
*/
|
||||||
@Input() label: string;
|
@Input() label: string;
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,68 @@
|
|||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { MockTranslateLoader } from '../../../shared/mocks/mock-translate-loader';
|
||||||
|
import { MetadataValuesComponent } from './metadata-values.component';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { Metadatum } from '../../../core/shared/metadatum.model';
|
||||||
|
|
||||||
|
let comp: MetadataValuesComponent;
|
||||||
|
let fixture: ComponentFixture<MetadataValuesComponent>;
|
||||||
|
|
||||||
|
const mockMetadata = [
|
||||||
|
{
|
||||||
|
key: 'journal.identifier.issn',
|
||||||
|
language: 'en_US',
|
||||||
|
value: '1234'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'journal.publisher',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'a publisher'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'journal.identifier.description',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'desc'
|
||||||
|
}] as Metadatum[];
|
||||||
|
const mockSeperator = '<br/>';
|
||||||
|
const mockLabel = 'fake.message';
|
||||||
|
|
||||||
|
describe('MetadataValuesComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [MetadataValuesComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(MetadataValuesComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
fixture = TestBed.createComponent(MetadataValuesComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.values = mockMetadata;
|
||||||
|
comp.separator = mockSeperator;
|
||||||
|
comp.label = mockLabel;
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should display all metadata values', () => {
|
||||||
|
const innerHTML = fixture.nativeElement.innerHTML;
|
||||||
|
for (const metadatum of mockMetadata) {
|
||||||
|
expect(innerHTML).toContain(metadatum.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain separators equal to the amount of metadata values minus one', () => {
|
||||||
|
const separators = fixture.debugElement.queryAll(By.css('span>span'));
|
||||||
|
expect(separators.length).toBe(mockMetadata.length - 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -12,10 +12,19 @@ import { MetadataValue } from '../../../core/shared/metadata.models';
|
|||||||
})
|
})
|
||||||
export class MetadataValuesComponent {
|
export class MetadataValuesComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The metadata values to display
|
||||||
|
*/
|
||||||
@Input() mdValues: MetadataValue[];
|
@Input() mdValues: MetadataValue[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The seperator used to split the metadata values (can contain HTML)
|
||||||
|
*/
|
||||||
@Input() separator: string;
|
@Input() separator: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The label for this iteration of metadata values
|
||||||
|
*/
|
||||||
@Input() label: string;
|
@Input() label: string;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
76
src/app/+item-page/full/full-item-page.component.spec.ts
Normal file
76
src/app/+item-page/full/full-item-page.component.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader';
|
||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { TruncatePipe } from '../../shared/utils/truncate.pipe';
|
||||||
|
import { FullItemPageComponent } from './full-item-page.component';
|
||||||
|
import { MetadataService } from '../../core/metadata/metadata.service';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
||||||
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
const mockItem: Item = Object.assign(new Item(), {
|
||||||
|
bitstreams: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))),
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'dc.title',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'test item'
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
const routeStub = Object.assign(new ActivatedRouteStub(), {
|
||||||
|
data: observableOf({ item: new RemoteData(false, false, true, null, mockItem) })
|
||||||
|
});
|
||||||
|
const metadataServiceStub = {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
processRemoteData: () => {}
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('FullItemPageComponent', () => {
|
||||||
|
let comp: FullItemPageComponent;
|
||||||
|
let fixture: ComponentFixture<FullItemPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
}), RouterTestingModule.withRoutes([]), BrowserAnimationsModule],
|
||||||
|
declarations: [FullItemPageComponent, TruncatePipe, VarDirective],
|
||||||
|
providers: [
|
||||||
|
{provide: ActivatedRoute, useValue: routeStub},
|
||||||
|
{provide: ItemDataService, useValue: {}},
|
||||||
|
{provide: MetadataService, useValue: metadataServiceStub}
|
||||||
|
],
|
||||||
|
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(FullItemPageComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
fixture = TestBed.createComponent(FullItemPageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should display the item\'s metadata', () => {
|
||||||
|
const table = fixture.debugElement.query(By.css('table'));
|
||||||
|
for (const metadatum of mockItem.metadata) {
|
||||||
|
expect(table.nativeElement.innerHTML).toContain(metadatum.value);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
@@ -1,9 +1,8 @@
|
|||||||
|
|
||||||
import {filter, map} from 'rxjs/operators';
|
import {filter, map} from 'rxjs/operators';
|
||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable , BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
import { ItemPageComponent } from '../simple/item-page.component';
|
import { ItemPageComponent } from '../simple/item-page.component';
|
||||||
import { MetadataMap } from '../../core/shared/metadata.models';
|
import { MetadataMap } from '../../core/shared/metadata.models';
|
||||||
@@ -32,7 +31,7 @@ import { hasValue } from '../../shared/empty.util';
|
|||||||
})
|
})
|
||||||
export class FullItemPageComponent extends ItemPageComponent implements OnInit {
|
export class FullItemPageComponent extends ItemPageComponent implements OnInit {
|
||||||
|
|
||||||
itemRD$: Observable<RemoteData<Item>>;
|
itemRD$: BehaviorSubject<RemoteData<Item>>;
|
||||||
|
|
||||||
metadata$: Observable<MetadataMap>;
|
metadata$: Observable<MetadataMap>;
|
||||||
|
|
||||||
|
@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
import { SharedModule } from './../shared/shared.module';
|
import { SharedModule } from './../shared/shared.module';
|
||||||
|
import { GenericItemPageFieldComponent } from './simple/field-components/specific-field/generic/generic-item-page-field.component';
|
||||||
|
|
||||||
import { ItemPageComponent } from './simple/item-page.component';
|
import { ItemPageComponent } from './simple/item-page.component';
|
||||||
import { ItemPageRoutingModule } from './item-page-routing.module';
|
import { ItemPageRoutingModule } from './item-page-routing.module';
|
||||||
@@ -13,19 +14,32 @@ import { ItemPageDateFieldComponent } from './simple/field-components/specific-f
|
|||||||
import { ItemPageAbstractFieldComponent } from './simple/field-components/specific-field/abstract/item-page-abstract-field.component';
|
import { ItemPageAbstractFieldComponent } from './simple/field-components/specific-field/abstract/item-page-abstract-field.component';
|
||||||
import { ItemPageUriFieldComponent } from './simple/field-components/specific-field/uri/item-page-uri-field.component';
|
import { ItemPageUriFieldComponent } from './simple/field-components/specific-field/uri/item-page-uri-field.component';
|
||||||
import { ItemPageTitleFieldComponent } from './simple/field-components/specific-field/title/item-page-title-field.component';
|
import { ItemPageTitleFieldComponent } from './simple/field-components/specific-field/title/item-page-title-field.component';
|
||||||
import { ItemPageSpecificFieldComponent } from './simple/field-components/specific-field/item-page-specific-field.component';
|
import { ItemPageFieldComponent } from './simple/field-components/specific-field/item-page-field.component';
|
||||||
import { FileSectionComponent } from './simple/field-components/file-section/file-section.component';
|
import { FileSectionComponent } from './simple/field-components/file-section/file-section.component';
|
||||||
import { CollectionsComponent } from './field-components/collections/collections.component';
|
import { CollectionsComponent } from './field-components/collections/collections.component';
|
||||||
import { FullItemPageComponent } from './full/full-item-page.component';
|
import { FullItemPageComponent } from './full/full-item-page.component';
|
||||||
import { FullFileSectionComponent } from './full/field-components/file-section/full-file-section.component';
|
import { FullFileSectionComponent } from './full/field-components/file-section/full-file-section.component';
|
||||||
|
import { RelatedItemsComponent } from './simple/related-items/related-items-component';
|
||||||
|
import { SearchPageModule } from '../+search-page/search-page.module';
|
||||||
|
import { PublicationComponent } from './simple/item-types/publication/publication.component';
|
||||||
|
import { PersonComponent } from './simple/item-types/person/person.component';
|
||||||
|
import { OrgunitComponent } from './simple/item-types/orgunit/orgunit.component';
|
||||||
|
import { ProjectComponent } from './simple/item-types/project/project.component';
|
||||||
|
import { JournalComponent } from './simple/item-types/journal/journal.component';
|
||||||
|
import { JournalVolumeComponent } from './simple/item-types/journal-volume/journal-volume.component';
|
||||||
|
import { JournalIssueComponent } from './simple/item-types/journal-issue/journal-issue.component';
|
||||||
|
import { ItemComponent } from './simple/item-types/shared/item.component';
|
||||||
import { EditItemPageModule } from './edit-item-page/edit-item-page.module';
|
import { EditItemPageModule } from './edit-item-page/edit-item-page.module';
|
||||||
|
import { MetadataRepresentationListComponent } from './simple/metadata-representation-list/metadata-representation-list.component';
|
||||||
|
import { RelatedEntitiesSearchComponent } from './simple/related-entities/related-entities-search/related-entities-search.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
EditItemPageModule,
|
EditItemPageModule,
|
||||||
ItemPageRoutingModule
|
ItemPageRoutingModule,
|
||||||
|
SearchPageModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
ItemPageComponent,
|
ItemPageComponent,
|
||||||
@@ -38,10 +52,31 @@ import { EditItemPageModule } from './edit-item-page/edit-item-page.module';
|
|||||||
ItemPageAbstractFieldComponent,
|
ItemPageAbstractFieldComponent,
|
||||||
ItemPageUriFieldComponent,
|
ItemPageUriFieldComponent,
|
||||||
ItemPageTitleFieldComponent,
|
ItemPageTitleFieldComponent,
|
||||||
ItemPageSpecificFieldComponent,
|
ItemPageFieldComponent,
|
||||||
FileSectionComponent,
|
FileSectionComponent,
|
||||||
CollectionsComponent,
|
CollectionsComponent,
|
||||||
FullFileSectionComponent
|
FullFileSectionComponent,
|
||||||
|
PublicationComponent,
|
||||||
|
ProjectComponent,
|
||||||
|
OrgunitComponent,
|
||||||
|
PersonComponent,
|
||||||
|
RelatedItemsComponent,
|
||||||
|
ItemComponent,
|
||||||
|
GenericItemPageFieldComponent,
|
||||||
|
JournalComponent,
|
||||||
|
JournalIssueComponent,
|
||||||
|
JournalVolumeComponent,
|
||||||
|
MetadataRepresentationListComponent,
|
||||||
|
RelatedEntitiesSearchComponent
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
PublicationComponent,
|
||||||
|
ProjectComponent,
|
||||||
|
OrgunitComponent,
|
||||||
|
PersonComponent,
|
||||||
|
JournalComponent,
|
||||||
|
JournalIssueComponent,
|
||||||
|
JournalVolumeComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class ItemPageModule {
|
export class ItemPageModule {
|
||||||
|
@@ -5,6 +5,7 @@ import { RemoteData } from '../core/data/remote-data';
|
|||||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||||
import { ItemDataService } from '../core/data/item-data.service';
|
import { ItemDataService } from '../core/data/item-data.service';
|
||||||
import { Item } from '../core/shared/item.model';
|
import { Item } from '../core/shared/item.model';
|
||||||
|
import { tap } from 'rxjs/operators';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class represents a resolver that requests a specific item before the route is activated
|
* This class represents a resolver that requests a specific item before the route is activated
|
||||||
|
@@ -0,0 +1,41 @@
|
|||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ItemPageAbstractFieldComponent } from './item-page-abstract-field.component';
|
||||||
|
import { MockTranslateLoader } from '../../../../../shared/testing/mock-translate-loader';
|
||||||
|
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
|
||||||
|
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
|
||||||
|
|
||||||
|
let comp: ItemPageAbstractFieldComponent;
|
||||||
|
let fixture: ComponentFixture<ItemPageAbstractFieldComponent>;
|
||||||
|
|
||||||
|
const mockField = 'dc.description.abstract';
|
||||||
|
const mockValue = 'test value';
|
||||||
|
|
||||||
|
describe('ItemPageAbstractFieldComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [ItemPageAbstractFieldComponent, MetadataValuesComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(ItemPageAbstractFieldComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
fixture = TestBed.createComponent(ItemPageAbstractFieldComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should display display the correct metadata value', () => {
|
||||||
|
expect(fixture.nativeElement.innerHTML).toContain(mockValue);
|
||||||
|
});
|
||||||
|
});
|
@@ -1,22 +1,39 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
import { Item } from '../../../../../core/shared/item.model';
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
import { ItemPageSpecificFieldComponent } from '../item-page-specific-field.component';
|
import { ItemPageFieldComponent } from '../item-page-field.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-page-abstract-field',
|
selector: 'ds-item-page-abstract-field',
|
||||||
templateUrl: './../item-page-specific-field.component.html'
|
templateUrl: '../item-page-field.component.html'
|
||||||
})
|
})
|
||||||
export class ItemPageAbstractFieldComponent extends ItemPageSpecificFieldComponent {
|
/**
|
||||||
|
* This component is used for displaying the abstract (dc.description.abstract) of an item
|
||||||
|
*/
|
||||||
|
export class ItemPageAbstractFieldComponent extends ItemPageFieldComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item to display metadata for
|
||||||
|
*/
|
||||||
@Input() item: Item;
|
@Input() item: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Separator string between multiple values of the metadata fields defined
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
separator: string;
|
separator: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fields (schema.element.qualifier) used to render their values.
|
||||||
|
* In this component, we want to display values for metadata 'dc.description.abstract'
|
||||||
|
*/
|
||||||
fields: string[] = [
|
fields: string[] = [
|
||||||
'dc.description.abstract'
|
'dc.description.abstract'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Label i18n key for the rendered metadata
|
||||||
|
*/
|
||||||
label = 'item.page.abstract';
|
label = 'item.page.abstract';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,45 @@
|
|||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { MockTranslateLoader } from '../../../../../shared/testing/mock-translate-loader';
|
||||||
|
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
|
||||||
|
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
|
||||||
|
import { ItemPageAuthorFieldComponent } from './item-page-author-field.component';
|
||||||
|
|
||||||
|
let comp: ItemPageAuthorFieldComponent;
|
||||||
|
let fixture: ComponentFixture<ItemPageAuthorFieldComponent>;
|
||||||
|
|
||||||
|
const mockFields = ['dc.contributor.author', 'dc.creator', 'dc.contributor'];
|
||||||
|
const mockValue = 'test value';
|
||||||
|
|
||||||
|
describe('ItemPageAuthorFieldComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [ItemPageAuthorFieldComponent, MetadataValuesComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(ItemPageAuthorFieldComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (const field of mockFields) {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
fixture = TestBed.createComponent(ItemPageAuthorFieldComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.item = mockItemWithMetadataFieldAndValue(field, mockValue);
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe(`when the item contains metadata for ${field}`, () => {
|
||||||
|
it('should display display the correct metadata value', () => {
|
||||||
|
expect(fixture.nativeElement.innerHTML).toContain(mockValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@@ -1,24 +1,41 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
import { Item } from '../../../../../core/shared/item.model';
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
import { ItemPageSpecificFieldComponent } from '../item-page-specific-field.component';
|
import { ItemPageFieldComponent } from '../item-page-field.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-page-author-field',
|
selector: 'ds-item-page-author-field',
|
||||||
templateUrl: './../item-page-specific-field.component.html'
|
templateUrl: '../item-page-field.component.html'
|
||||||
})
|
})
|
||||||
export class ItemPageAuthorFieldComponent extends ItemPageSpecificFieldComponent {
|
/**
|
||||||
|
* This component is used for displaying the author (dc.contributor.author, dc.creator and dc.contributor) metadata of an item
|
||||||
|
*/
|
||||||
|
export class ItemPageAuthorFieldComponent extends ItemPageFieldComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item to display metadata for
|
||||||
|
*/
|
||||||
@Input() item: Item;
|
@Input() item: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Separator string between multiple values of the metadata fields defined
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
separator: string;
|
separator: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fields (schema.element.qualifier) used to render their values.
|
||||||
|
* In this component, we want to display values for metadata 'dc.contributor.author', 'dc.creator' and 'dc.contributor'
|
||||||
|
*/
|
||||||
fields: string[] = [
|
fields: string[] = [
|
||||||
'dc.contributor.author',
|
'dc.contributor.author',
|
||||||
'dc.creator',
|
'dc.creator',
|
||||||
'dc.contributor'
|
'dc.contributor'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Label i18n key for the rendered metadata
|
||||||
|
*/
|
||||||
label = 'item.page.author';
|
label = 'item.page.author';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,41 @@
|
|||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { MockTranslateLoader } from '../../../../../shared/testing/mock-translate-loader';
|
||||||
|
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
|
||||||
|
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
|
||||||
|
import { ItemPageDateFieldComponent } from './item-page-date-field.component';
|
||||||
|
|
||||||
|
let comp: ItemPageDateFieldComponent;
|
||||||
|
let fixture: ComponentFixture<ItemPageDateFieldComponent>;
|
||||||
|
|
||||||
|
const mockField = 'dc.date.issued';
|
||||||
|
const mockValue = 'test value';
|
||||||
|
|
||||||
|
describe('ItemPageDateFieldComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [ItemPageDateFieldComponent, MetadataValuesComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(ItemPageDateFieldComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
fixture = TestBed.createComponent(ItemPageDateFieldComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should display display the correct metadata value', () => {
|
||||||
|
expect(fixture.nativeElement.innerHTML).toContain(mockValue);
|
||||||
|
});
|
||||||
|
});
|
@@ -1,22 +1,39 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
import { Item } from '../../../../../core/shared/item.model';
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
import { ItemPageSpecificFieldComponent } from '../item-page-specific-field.component';
|
import { ItemPageFieldComponent } from '../item-page-field.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-page-date-field',
|
selector: 'ds-item-page-date-field',
|
||||||
templateUrl: './../item-page-specific-field.component.html'
|
templateUrl: '../item-page-field.component.html'
|
||||||
})
|
})
|
||||||
export class ItemPageDateFieldComponent extends ItemPageSpecificFieldComponent {
|
/**
|
||||||
|
* This component is used for displaying the issue date (dc.date.issued) metadata of an item
|
||||||
|
*/
|
||||||
|
export class ItemPageDateFieldComponent extends ItemPageFieldComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item to display metadata for
|
||||||
|
*/
|
||||||
@Input() item: Item;
|
@Input() item: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Separator string between multiple values of the metadata fields defined
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
separator = ', ';
|
separator = ', ';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fields (schema.element.qualifier) used to render their values.
|
||||||
|
* In this component, we want to display values for metadata 'dc.date.issued'
|
||||||
|
*/
|
||||||
fields: string[] = [
|
fields: string[] = [
|
||||||
'dc.date.issued'
|
'dc.date.issued'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Label i18n key for the rendered metadata
|
||||||
|
*/
|
||||||
label = 'item.page.date';
|
label = 'item.page.date';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,45 @@
|
|||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { MockTranslateLoader } from '../../../../../shared/testing/mock-translate-loader';
|
||||||
|
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
|
||||||
|
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
|
||||||
|
import { GenericItemPageFieldComponent } from './generic-item-page-field.component';
|
||||||
|
|
||||||
|
let comp: GenericItemPageFieldComponent;
|
||||||
|
let fixture: ComponentFixture<GenericItemPageFieldComponent>;
|
||||||
|
|
||||||
|
const mockValue = 'test value';
|
||||||
|
const mockField = 'dc.test';
|
||||||
|
const mockLabel = 'test label';
|
||||||
|
const mockFields = [mockField];
|
||||||
|
|
||||||
|
describe('GenericItemPageFieldComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [GenericItemPageFieldComponent, MetadataValuesComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(GenericItemPageFieldComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
fixture = TestBed.createComponent(GenericItemPageFieldComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
|
||||||
|
comp.fields = mockFields;
|
||||||
|
comp.label = mockLabel;
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should display display the correct metadata value', () => {
|
||||||
|
expect(fixture.nativeElement.innerHTML).toContain(mockValue);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,38 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
import { ItemPageFieldComponent } from '../item-page-field.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-generic-item-page-field',
|
||||||
|
templateUrl: '../item-page-field.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* This component can be used to represent metadata on a simple item page.
|
||||||
|
* It is the most generic way of displaying metadata values
|
||||||
|
* It expects 4 parameters: The item, a seperator, the metadata keys and an i18n key
|
||||||
|
*/
|
||||||
|
export class GenericItemPageFieldComponent extends ItemPageFieldComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item to display metadata for
|
||||||
|
*/
|
||||||
|
@Input() item: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Separator string between multiple values of the metadata fields defined
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
@Input() separator: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fields (schema.element.qualifier) used to render their values.
|
||||||
|
*/
|
||||||
|
@Input() fields: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Label i18n key for the rendered metadata
|
||||||
|
*/
|
||||||
|
@Input() label: string;
|
||||||
|
|
||||||
|
}
|
@@ -1,3 +1,3 @@
|
|||||||
<div class="item-page-specific-field">
|
<div class="item-page-field">
|
||||||
<ds-metadata-values [mdValues]="item?.allMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-values>
|
<ds-metadata-values [mdValues]="item?.allMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-values>
|
||||||
</div>
|
</div>
|
@@ -0,0 +1,62 @@
|
|||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { MockTranslateLoader } from '../../../../shared/mocks/mock-translate-loader';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { ItemPageFieldComponent } from './item-page-field.component';
|
||||||
|
import { MetadataValuesComponent } from '../../../field-components/metadata-values/metadata-values.component';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
let comp: ItemPageFieldComponent;
|
||||||
|
let fixture: ComponentFixture<ItemPageFieldComponent>;
|
||||||
|
|
||||||
|
const mockValue = 'test value';
|
||||||
|
const mockField = 'dc.test';
|
||||||
|
const mockLabel = 'test label';
|
||||||
|
const mockFields = [mockField];
|
||||||
|
|
||||||
|
describe('ItemPageFieldComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [ItemPageFieldComponent, MetadataValuesComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(ItemPageFieldComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
fixture = TestBed.createComponent(ItemPageFieldComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
|
||||||
|
comp.fields = mockFields;
|
||||||
|
comp.label = mockLabel;
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should display display the correct metadata value', () => {
|
||||||
|
expect(fixture.nativeElement.innerHTML).toContain(mockValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export function mockItemWithMetadataFieldAndValue(field: string, value: string): Item {
|
||||||
|
return Object.assign(new Item(), {
|
||||||
|
bitstreams: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))),
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: field,
|
||||||
|
language: 'en_US',
|
||||||
|
value: value
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
}
|
@@ -9,10 +9,13 @@ import { Item } from '../../../../core/shared/item.model';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: './item-page-specific-field.component.html'
|
templateUrl: './item-page-field.component.html'
|
||||||
})
|
})
|
||||||
export class ItemPageSpecificFieldComponent {
|
export class ItemPageFieldComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item to display metadata for
|
||||||
|
*/
|
||||||
@Input() item: Item;
|
@Input() item: Item;
|
||||||
|
|
||||||
/**
|
/**
|
@@ -0,0 +1,41 @@
|
|||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { MockTranslateLoader } from '../../../../../shared/testing/mock-translate-loader';
|
||||||
|
import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component';
|
||||||
|
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
|
||||||
|
import { ItemPageTitleFieldComponent } from './item-page-title-field.component';
|
||||||
|
|
||||||
|
let comp: ItemPageTitleFieldComponent;
|
||||||
|
let fixture: ComponentFixture<ItemPageTitleFieldComponent>;
|
||||||
|
|
||||||
|
const mockField = 'dc.title';
|
||||||
|
const mockValue = 'test value';
|
||||||
|
|
||||||
|
describe('ItemPageTitleFieldComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [ItemPageTitleFieldComponent, MetadataValuesComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(ItemPageTitleFieldComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
fixture = TestBed.createComponent(ItemPageTitleFieldComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should display display the correct metadata value', () => {
|
||||||
|
expect(fixture.nativeElement.innerHTML).toContain(mockValue);
|
||||||
|
});
|
||||||
|
});
|
@@ -1,18 +1,32 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
import { Item } from '../../../../../core/shared/item.model';
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
import { ItemPageSpecificFieldComponent } from '../item-page-specific-field.component';
|
import { ItemPageFieldComponent } from '../item-page-field.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-page-title-field',
|
selector: 'ds-item-page-title-field',
|
||||||
templateUrl: './item-page-title-field.component.html'
|
templateUrl: './item-page-title-field.component.html'
|
||||||
})
|
})
|
||||||
export class ItemPageTitleFieldComponent extends ItemPageSpecificFieldComponent {
|
/**
|
||||||
|
* This component is used for displaying the title (dc.title) of an item
|
||||||
|
*/
|
||||||
|
export class ItemPageTitleFieldComponent extends ItemPageFieldComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item to display metadata for
|
||||||
|
*/
|
||||||
@Input() item: Item;
|
@Input() item: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Separator string between multiple values of the metadata fields defined
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
separator: string;
|
separator: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fields (schema.element.qualifier) used to render their values.
|
||||||
|
* In this component, we want to display values for metadata 'dc.title'
|
||||||
|
*/
|
||||||
fields: string[] = [
|
fields: string[] = [
|
||||||
'dc.title'
|
'dc.title'
|
||||||
];
|
];
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
<div class="item-page-specific-field">
|
<div class="item-page-field">
|
||||||
<ds-metadata-uri-values [mdValues]="item?.allMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-uri-values>
|
<ds-metadata-uri-values [mdValues]="item?.allMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-uri-values>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -0,0 +1,41 @@
|
|||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { MockTranslateLoader } from '../../../../../shared/testing/mock-translate-loader';
|
||||||
|
import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec';
|
||||||
|
import { ItemPageUriFieldComponent } from './item-page-uri-field.component';
|
||||||
|
import { MetadataUriValuesComponent } from '../../../../field-components/metadata-uri-values/metadata-uri-values.component';
|
||||||
|
|
||||||
|
let comp: ItemPageUriFieldComponent;
|
||||||
|
let fixture: ComponentFixture<ItemPageUriFieldComponent>;
|
||||||
|
|
||||||
|
const mockField = 'dc.identifier.uri';
|
||||||
|
const mockValue = 'test value';
|
||||||
|
|
||||||
|
describe('ItemPageUriFieldComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [ItemPageUriFieldComponent, MetadataUriValuesComponent],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(ItemPageUriFieldComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
fixture = TestBed.createComponent(ItemPageUriFieldComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should display display the correct metadata value', () => {
|
||||||
|
expect(fixture.nativeElement.innerHTML).toContain(mockValue);
|
||||||
|
});
|
||||||
|
});
|
@@ -1,22 +1,39 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
import { Item } from '../../../../../core/shared/item.model';
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
import { ItemPageSpecificFieldComponent } from '../item-page-specific-field.component';
|
import { ItemPageFieldComponent } from '../item-page-field.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-page-uri-field',
|
selector: 'ds-item-page-uri-field',
|
||||||
templateUrl: './item-page-uri-field.component.html'
|
templateUrl: './item-page-uri-field.component.html'
|
||||||
})
|
})
|
||||||
export class ItemPageUriFieldComponent extends ItemPageSpecificFieldComponent {
|
/**
|
||||||
|
* This component is used for displaying the uri (dc.identifier.uri) metadata of an item
|
||||||
|
*/
|
||||||
|
export class ItemPageUriFieldComponent extends ItemPageFieldComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item to display metadata for
|
||||||
|
*/
|
||||||
@Input() item: Item;
|
@Input() item: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Separator string between multiple values of the metadata fields defined
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
separator: string;
|
separator: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fields (schema.element.qualifier) used to render their values.
|
||||||
|
* In this component, we want to display values for metadata 'dc.identifier.uri'
|
||||||
|
*/
|
||||||
fields: string[] = [
|
fields: string[] = [
|
||||||
'dc.identifier.uri'
|
'dc.identifier.uri'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Label i18n key for the rendered metadata
|
||||||
|
*/
|
||||||
label = 'item.page.uri';
|
label = 'item.page.uri';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,27 +1,7 @@
|
|||||||
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
||||||
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||||
<div *ngIf="itemRD?.payload as item">
|
<div *ngIf="itemRD?.payload as item">
|
||||||
<ds-item-page-title-field [item]="item"></ds-item-page-title-field>
|
<ds-item-type-switcher [object]="item" [viewMode]="viewMode"></ds-item-type-switcher>
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-12 col-md-4">
|
|
||||||
<ds-metadata-field-wrapper>
|
|
||||||
<ds-thumbnail [thumbnail]="thumbnail$ | async"></ds-thumbnail>
|
|
||||||
</ds-metadata-field-wrapper>
|
|
||||||
<ds-item-page-file-section [item]="item"></ds-item-page-file-section>
|
|
||||||
<ds-item-page-date-field [item]="item"></ds-item-page-date-field>
|
|
||||||
<ds-item-page-author-field [item]="item"></ds-item-page-author-field>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-12 col-md-6">
|
|
||||||
<ds-item-page-abstract-field [item]="item"></ds-item-page-abstract-field>
|
|
||||||
<ds-item-page-uri-field [item]="item"></ds-item-page-uri-field>
|
|
||||||
<ds-item-page-collections [item]="item"></ds-item-page-collections>
|
|
||||||
<div>
|
|
||||||
<a class="btn btn-outline-primary" [routerLink]="['/items/' + item.id + '/full']">
|
|
||||||
{{"item.page.link.full" | translate}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
||||||
|
@@ -1 +1,9 @@
|
|||||||
@import '../../../styles/variables.scss';
|
@import '../../../styles/variables.scss';
|
||||||
|
@import '../../../styles/mixins.scss';
|
||||||
|
|
||||||
|
@include media-breakpoint-down(md) {
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
90
src/app/+item-page/simple/item-page.component.spec.ts
Normal file
90
src/app/+item-page/simple/item-page.component.spec.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader';
|
||||||
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { ItemPageComponent } from './item-page.component';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
||||||
|
import { MetadataService } from '../../core/metadata/metadata.service';
|
||||||
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { createRelationshipsObservable } from './item-types/shared/item.component.spec';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
const mockItem: Item = Object.assign(new Item(), {
|
||||||
|
bitstreams: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))),
|
||||||
|
metadata: [],
|
||||||
|
relationships: createRelationshipsObservable()
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ItemPageComponent', () => {
|
||||||
|
let comp: ItemPageComponent;
|
||||||
|
let fixture: ComponentFixture<ItemPageComponent>;
|
||||||
|
|
||||||
|
const mockMetadataService = {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
processRemoteData: () => {}
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
};
|
||||||
|
const mockRoute = Object.assign(new ActivatedRouteStub(), {
|
||||||
|
data: observableOf({ item: new RemoteData(false, false, true, null, mockItem) })
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
}), BrowserAnimationsModule],
|
||||||
|
declarations: [ItemPageComponent, VarDirective],
|
||||||
|
providers: [
|
||||||
|
{provide: ActivatedRoute, useValue: mockRoute},
|
||||||
|
{provide: ItemDataService, useValue: {}},
|
||||||
|
{provide: MetadataService, useValue: mockMetadataService}
|
||||||
|
],
|
||||||
|
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(ItemPageComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
fixture = TestBed.createComponent(ItemPageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('when the item is loading', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.itemRD$ = observableOf(new RemoteData(true, true, true, null, undefined));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display a loading component', () => {
|
||||||
|
const loading = fixture.debugElement.query(By.css('ds-loading'));
|
||||||
|
expect(loading.nativeElement).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the item failed loading', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.itemRD$ = observableOf(new RemoteData(false, false, false, null, undefined));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display an error component', () => {
|
||||||
|
const error = fixture.debugElement.query(By.css('ds-error'));
|
||||||
|
expect(error.nativeElement).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -1,5 +1,4 @@
|
|||||||
|
import { filter, map, mergeMap } from 'rxjs/operators';
|
||||||
import {mergeMap, filter, map} from 'rxjs/operators';
|
|
||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
@@ -14,6 +13,9 @@ import { MetadataService } from '../../core/metadata/metadata.service';
|
|||||||
|
|
||||||
import { fadeInOut } from '../../shared/animations/fade';
|
import { fadeInOut } from '../../shared/animations/fade';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import * as viewMode from '../../shared/view-mode';
|
||||||
|
|
||||||
|
export const VIEW_MODE_FULL = 'full';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a simple item page.
|
* This component renders a simple item page.
|
||||||
@@ -29,21 +31,31 @@ import { hasValue } from '../../shared/empty.util';
|
|||||||
})
|
})
|
||||||
export class ItemPageComponent implements OnInit {
|
export class ItemPageComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item's id
|
||||||
|
*/
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
private sub: any;
|
/**
|
||||||
|
* The item wrapped in a remote-data object
|
||||||
|
*/
|
||||||
itemRD$: Observable<RemoteData<Item>>;
|
itemRD$: Observable<RemoteData<Item>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item's thumbnail
|
||||||
|
*/
|
||||||
thumbnail$: Observable<Bitstream>;
|
thumbnail$: Observable<Bitstream>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view-mode we're currently on
|
||||||
|
*/
|
||||||
|
viewMode = VIEW_MODE_FULL;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private items: ItemDataService,
|
private items: ItemDataService,
|
||||||
private metadataService: MetadataService
|
private metadataService: MetadataService,
|
||||||
) {
|
) { }
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.itemRD$ = this.route.data.pipe(map((data) => data.item));
|
this.itemRD$ = this.route.data.pipe(map((data) => data.item));
|
||||||
|
@@ -0,0 +1,50 @@
|
|||||||
|
<h2 class="item-page-title-field">
|
||||||
|
<ds-metadata-values [values]="item?.filterMetadata(['dc.title'])"></ds-metadata-values>
|
||||||
|
</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-md-4">
|
||||||
|
<ds-metadata-field-wrapper>
|
||||||
|
<ds-thumbnail [thumbnail]="this.item.getThumbnail() | async"></ds-thumbnail>
|
||||||
|
</ds-metadata-field-wrapper>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['journalissue.identifier.number']"
|
||||||
|
[label]="'journalissue.page.number'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['journalissue.issuedate']"
|
||||||
|
[label]="'journalissue.page.issuedate'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['journal.title']"
|
||||||
|
[label]="'journalissue.page.journal-title'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['journal.identifier.issn']"
|
||||||
|
[label]="'journalissue.page.journal-issn'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-6">
|
||||||
|
<ds-related-items
|
||||||
|
[items]="volumes$ | async"
|
||||||
|
[label]="'relationships.isSingleVolumeOf' | translate">
|
||||||
|
</ds-related-items>
|
||||||
|
<ds-related-items
|
||||||
|
class="mb-1 mt-1"
|
||||||
|
[items]="publications$ | async"
|
||||||
|
[label]="'relationships.isPublicationOfJournalIssue' | translate">
|
||||||
|
</ds-related-items>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['journalissue.identifier.description']"
|
||||||
|
[label]="'journalissue.page.description'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['journalissue.identifier.keyword']"
|
||||||
|
[label]="'journalissue.page.keyword'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<div>
|
||||||
|
<a class="btn btn-outline-primary" [routerLink]="['/items/' + item.id + '/full']">
|
||||||
|
{{"item.page.link.full" | translate}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
@import '../../../../../styles/variables.scss';
|
@@ -0,0 +1,35 @@
|
|||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
|
import { createRelationshipsObservable, getItemPageFieldsTest } from '../shared/item.component.spec';
|
||||||
|
import { JournalIssueComponent } from './journal-issue.component';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
const mockItem: Item = Object.assign(new Item(), {
|
||||||
|
bitstreams: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))),
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'journalissue.identifier.number',
|
||||||
|
language: 'en_US',
|
||||||
|
value: '1234'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'journalissue.issuedate',
|
||||||
|
language: 'en_US',
|
||||||
|
value: '2018'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'journalissue.identifier.description',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'desc'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'journalissue.identifier.keyword',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'keyword'
|
||||||
|
}],
|
||||||
|
relationships: createRelationshipsObservable()
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('JournalIssueComponent', getItemPageFieldsTest(mockItem, JournalIssueComponent));
|
@@ -0,0 +1,51 @@
|
|||||||
|
import { Component, Inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { rendersItemType } from '../../../../shared/items/item-type-decorator';
|
||||||
|
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
|
||||||
|
import { isNotEmpty } from '../../../../shared/empty.util';
|
||||||
|
import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component';
|
||||||
|
import { VIEW_MODE_FULL } from '../../item-page.component';
|
||||||
|
|
||||||
|
@rendersItemType('JournalIssue', VIEW_MODE_FULL)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-journal-issue',
|
||||||
|
styleUrls: ['./journal-issue.component.scss'],
|
||||||
|
templateUrl: './journal-issue.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying metadata and relations of an item of the type Journal Issue
|
||||||
|
*/
|
||||||
|
export class JournalIssueComponent extends ItemComponent {
|
||||||
|
/**
|
||||||
|
* The volumes related to this journal issue
|
||||||
|
*/
|
||||||
|
volumes$: Observable<Item[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The publications related to this journal issue
|
||||||
|
*/
|
||||||
|
publications$: Observable<Item[]>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(ITEM) public item: Item,
|
||||||
|
private ids: ItemDataService
|
||||||
|
) {
|
||||||
|
super(item);
|
||||||
|
}
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
|
||||||
|
if (isNotEmpty(this.resolvedRelsAndTypes$)) {
|
||||||
|
this.volumes$ = this.resolvedRelsAndTypes$.pipe(
|
||||||
|
filterRelationsByTypeLabel('isJournalVolumeOfIssue'),
|
||||||
|
relationsToItems(this.item.id, this.ids)
|
||||||
|
);
|
||||||
|
this.publications$ = this.resolvedRelsAndTypes$.pipe(
|
||||||
|
filterRelationsByTypeLabel('isPublicationOfJournalIssue'),
|
||||||
|
relationsToItems(this.item.id, this.ids)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,37 @@
|
|||||||
|
<h2 class="item-page-title-field">
|
||||||
|
<ds-metadata-values [values]="item?.filterMetadata(['dc.title'])"></ds-metadata-values>
|
||||||
|
</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-md-4">
|
||||||
|
<ds-metadata-field-wrapper>
|
||||||
|
<ds-thumbnail [thumbnail]="this.item.getThumbnail() | async"></ds-thumbnail>
|
||||||
|
</ds-metadata-field-wrapper>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['journalvolume.identifier.volume']"
|
||||||
|
[label]="'journalvolume.page.volume'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['journalvolume.issuedate']"
|
||||||
|
[label]="'journalvolume.page.issuedate'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-6">
|
||||||
|
<ds-related-items
|
||||||
|
[items]="journals$ | async"
|
||||||
|
[label]="'relationships.isSingleJournalOf' | translate">
|
||||||
|
</ds-related-items>
|
||||||
|
<ds-related-items
|
||||||
|
[items]="issues$ | async"
|
||||||
|
[label]="'relationships.isIssueOf' | translate">
|
||||||
|
</ds-related-items>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['journalvolume.identifier.description']"
|
||||||
|
[label]="'journalvolume.page.description'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<div>
|
||||||
|
<a class="btn btn-outline-primary" [routerLink]="['/items/' + item.id + '/full']">
|
||||||
|
{{"item.page.link.full" | translate}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
@import '../../../../../styles/variables.scss';
|
@@ -0,0 +1,30 @@
|
|||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
|
import { createRelationshipsObservable, getItemPageFieldsTest } from '../shared/item.component.spec';
|
||||||
|
import { JournalVolumeComponent } from './journal-volume.component';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
const mockItem: Item = Object.assign(new Item(), {
|
||||||
|
bitstreams: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))),
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'journalvolume.identifier.volume',
|
||||||
|
language: 'en_US',
|
||||||
|
value: '1234'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'journalvolume.issuedate',
|
||||||
|
language: 'en_US',
|
||||||
|
value: '2018'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'journalvolume.identifier.description',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'desc'
|
||||||
|
}],
|
||||||
|
relationships: createRelationshipsObservable()
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('JournalVolumeComponent', getItemPageFieldsTest(mockItem, JournalVolumeComponent));
|
@@ -0,0 +1,51 @@
|
|||||||
|
import { Component, Inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { rendersItemType } from '../../../../shared/items/item-type-decorator';
|
||||||
|
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
|
||||||
|
import { isNotEmpty } from '../../../../shared/empty.util';
|
||||||
|
import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component';
|
||||||
|
import { VIEW_MODE_FULL } from '../../item-page.component';
|
||||||
|
|
||||||
|
@rendersItemType('JournalVolume', VIEW_MODE_FULL)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-journal-volume',
|
||||||
|
styleUrls: ['./journal-volume.component.scss'],
|
||||||
|
templateUrl: './journal-volume.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying metadata and relations of an item of the type Journal Volume
|
||||||
|
*/
|
||||||
|
export class JournalVolumeComponent extends ItemComponent {
|
||||||
|
/**
|
||||||
|
* The journals related to this journal volume
|
||||||
|
*/
|
||||||
|
journals$: Observable<Item[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The journal issues related to this journal volume
|
||||||
|
*/
|
||||||
|
issues$: Observable<Item[]>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(ITEM) public item: Item,
|
||||||
|
private ids: ItemDataService
|
||||||
|
) {
|
||||||
|
super(item);
|
||||||
|
}
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
|
||||||
|
if (isNotEmpty(this.resolvedRelsAndTypes$)) {
|
||||||
|
this.journals$ = this.resolvedRelsAndTypes$.pipe(
|
||||||
|
filterRelationsByTypeLabel('isJournalOfVolume'),
|
||||||
|
relationsToItems(this.item.id, this.ids)
|
||||||
|
);
|
||||||
|
this.issues$ = this.resolvedRelsAndTypes$.pipe(
|
||||||
|
filterRelationsByTypeLabel('isIssueOfJournalVolume'),
|
||||||
|
relationsToItems(this.item.id, this.ids)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,42 @@
|
|||||||
|
<h2 class="item-page-title-field">
|
||||||
|
<ds-metadata-values [values]="item?.filterMetadata(['dc.title'])"></ds-metadata-values>
|
||||||
|
</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-md-4">
|
||||||
|
<ds-metadata-field-wrapper>
|
||||||
|
<ds-thumbnail [thumbnail]="this.item.getThumbnail() | async"></ds-thumbnail>
|
||||||
|
</ds-metadata-field-wrapper>
|
||||||
|
<ds-generic-item-page-field class="item-page-fields" [item]="item"
|
||||||
|
[fields]="['journal.identifier.issn']"
|
||||||
|
[label]="'journal.page.issn'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field class="item-page-fields" [item]="item"
|
||||||
|
[fields]="['journal.publisher']"
|
||||||
|
[label]="'journal.page.publisher'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['journal.contributor.editor']"
|
||||||
|
[label]="'journal.page.editor'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-6">
|
||||||
|
<ds-related-items
|
||||||
|
[items]="volumes$ | async"
|
||||||
|
[label]="'relationships.isVolumeOf' | translate">
|
||||||
|
</ds-related-items>
|
||||||
|
<ds-generic-item-page-field class="item-page-fields" [item]="item"
|
||||||
|
[fields]="['journal.identifier.description']"
|
||||||
|
[label]="'journal.page.description'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<div>
|
||||||
|
<a class="btn btn-outline-primary" [routerLink]="['/items/' + item.id + '/full']">
|
||||||
|
{{"item.page.link.full" | translate}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 w-100">
|
||||||
|
<ds-related-entities-search [item]="item"
|
||||||
|
[relationType]="'isJournalOfPublication'">
|
||||||
|
</ds-related-entities-search>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
@import '../../../../../styles/variables.scss';
|
@@ -0,0 +1,88 @@
|
|||||||
|
import { ChangeDetectionStrategy, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
|
||||||
|
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
|
||||||
|
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
||||||
|
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { MockTranslateLoader } from '../../../../shared/mocks/mock-translate-loader';
|
||||||
|
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
|
import { isNotEmpty } from '../../../../shared/empty.util';
|
||||||
|
import { JournalComponent } from './journal.component';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
let comp: JournalComponent;
|
||||||
|
let fixture: ComponentFixture<JournalComponent>;
|
||||||
|
|
||||||
|
const mockItem: Item = Object.assign(new Item(), {
|
||||||
|
bitstreams: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))),
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'journal.identifier.issn',
|
||||||
|
language: 'en_US',
|
||||||
|
value: '1234'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'journal.publisher',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'a publisher'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'journal.identifier.description',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'desc'
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('JournalComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [JournalComponent, GenericItemPageFieldComponent, TruncatePipe],
|
||||||
|
providers: [
|
||||||
|
{provide: ITEM, useValue: mockItem},
|
||||||
|
{provide: ItemDataService, useValue: {}},
|
||||||
|
{provide: TruncatableService, useValue: {}}
|
||||||
|
],
|
||||||
|
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(JournalComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
fixture = TestBed.createComponent(JournalComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (const metadata of mockItem.metadata) {
|
||||||
|
it(`should be calling a component with metadata field ${metadata.key}`, () => {
|
||||||
|
const fields = fixture.debugElement.queryAll(By.css('.item-page-fields'));
|
||||||
|
expect(containsFieldInput(fields, metadata.key)).toBeTruthy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function containsFieldInput(fields: DebugElement[], metadataKey: string): boolean {
|
||||||
|
for (const field of fields) {
|
||||||
|
const fieldComp = field.componentInstance;
|
||||||
|
if (isNotEmpty(fieldComp.fields)) {
|
||||||
|
if (fieldComp.fields.indexOf(metadataKey) > -1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
@@ -0,0 +1,42 @@
|
|||||||
|
import { Component, Inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { rendersItemType } from '../../../../shared/items/item-type-decorator';
|
||||||
|
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
|
||||||
|
import { isNotEmpty } from '../../../../shared/empty.util';
|
||||||
|
import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component';
|
||||||
|
import { VIEW_MODE_FULL } from '../../item-page.component';
|
||||||
|
|
||||||
|
@rendersItemType('Journal', VIEW_MODE_FULL)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-journal',
|
||||||
|
styleUrls: ['./journal.component.scss'],
|
||||||
|
templateUrl: './journal.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying metadata and relations of an item of the type Journal
|
||||||
|
*/
|
||||||
|
export class JournalComponent extends ItemComponent {
|
||||||
|
/**
|
||||||
|
* The volumes related to this journal
|
||||||
|
*/
|
||||||
|
volumes$: Observable<Item[]>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(ITEM) public item: Item,
|
||||||
|
private ids: ItemDataService
|
||||||
|
) {
|
||||||
|
super(item);
|
||||||
|
}
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
|
||||||
|
if (isNotEmpty(this.resolvedRelsAndTypes$)) {
|
||||||
|
this.volumes$ = this.resolvedRelsAndTypes$.pipe(
|
||||||
|
filterRelationsByTypeLabel('isVolumeOfJournal'),
|
||||||
|
relationsToItems(this.item.id, this.ids)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,49 @@
|
|||||||
|
<h2 class="item-page-title-field">
|
||||||
|
<ds-metadata-values [values]="item?.filterMetadata(['orgunit.identifier.name'])"></ds-metadata-values>
|
||||||
|
</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-md-4">
|
||||||
|
<ds-metadata-field-wrapper>
|
||||||
|
<ds-thumbnail [thumbnail]="this.item.getThumbnail() | async" [defaultImage]="'assets/images/orgunit-placeholder.jpg'"></ds-thumbnail>
|
||||||
|
</ds-metadata-field-wrapper>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['orgunit.identifier.dateestablished']"
|
||||||
|
[label]="'orgunit.page.dateestablished'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['orgunit.identifier.city']"
|
||||||
|
[label]="'orgunit.page.city'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['orgunit.identifier.country']"
|
||||||
|
[label]="'orgunit.page.country'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['orgunit.identifier.id']"
|
||||||
|
[label]="'orgunit.page.id'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-6">
|
||||||
|
<ds-related-items
|
||||||
|
[items]="people$ | async"
|
||||||
|
[label]="'relationships.isPersonOf' | translate">
|
||||||
|
</ds-related-items>
|
||||||
|
<ds-related-items
|
||||||
|
[items]="projects$ | async"
|
||||||
|
[label]="'relationships.isProjectOf' | translate">
|
||||||
|
</ds-related-items>
|
||||||
|
<ds-related-items
|
||||||
|
[items]="publications$ | async"
|
||||||
|
[label]="'relationships.isPublicationOf' | translate">
|
||||||
|
</ds-related-items>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['orgunit.identifier.description']"
|
||||||
|
[label]="'orgunit.page.description'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<div>
|
||||||
|
<a class="btn btn-outline-primary" [routerLink]="['/items/' + item.id + '/full']">
|
||||||
|
{{"item.page.link.full" | translate}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
@import '../../../../../styles/variables.scss';
|
@@ -0,0 +1,40 @@
|
|||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
|
import { createRelationshipsObservable, getItemPageFieldsTest } from '../shared/item.component.spec';
|
||||||
|
import { OrgunitComponent } from './orgunit.component';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
const mockItem: Item = Object.assign(new Item(), {
|
||||||
|
bitstreams: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))),
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'orgunit.identifier.dateestablished',
|
||||||
|
language: 'en_US',
|
||||||
|
value: '2018'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'orgunit.identifier.city',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'New York'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'orgunit.identifier.country',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'USA'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'orgunit.identifier.id',
|
||||||
|
language: 'en_US',
|
||||||
|
value: '1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'orgunit.identifier.description',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'desc'
|
||||||
|
}],
|
||||||
|
relationships: createRelationshipsObservable()
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('OrgUnitComponent', getItemPageFieldsTest(mockItem, OrgunitComponent));
|
@@ -0,0 +1,62 @@
|
|||||||
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { rendersItemType } from '../../../../shared/items/item-type-decorator';
|
||||||
|
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
|
||||||
|
import { isNotEmpty } from '../../../../shared/empty.util';
|
||||||
|
import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component';
|
||||||
|
import { VIEW_MODE_FULL } from '../../item-page.component';
|
||||||
|
|
||||||
|
@rendersItemType('OrgUnit', VIEW_MODE_FULL)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-orgunit',
|
||||||
|
styleUrls: ['./orgunit.component.scss'],
|
||||||
|
templateUrl: './orgunit.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying metadata and relations of an item of the type Organisation Unit
|
||||||
|
*/
|
||||||
|
export class OrgunitComponent extends ItemComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* The people related to this organisation unit
|
||||||
|
*/
|
||||||
|
people$: Observable<Item[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The projects related to this organisation unit
|
||||||
|
*/
|
||||||
|
projects$: Observable<Item[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The publications related to this organisation unit
|
||||||
|
*/
|
||||||
|
publications$: Observable<Item[]>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(ITEM) public item: Item,
|
||||||
|
private ids: ItemDataService
|
||||||
|
) {
|
||||||
|
super(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
|
||||||
|
if (isNotEmpty(this.resolvedRelsAndTypes$)) {
|
||||||
|
this.people$ = this.resolvedRelsAndTypes$.pipe(
|
||||||
|
filterRelationsByTypeLabel('isPersonOfOrgUnit'),
|
||||||
|
relationsToItems(this.item.id, this.ids)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.projects$ = this.resolvedRelsAndTypes$.pipe(
|
||||||
|
filterRelationsByTypeLabel('isProjectOfOrgUnit'),
|
||||||
|
relationsToItems(this.item.id, this.ids)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.publications$ = this.resolvedRelsAndTypes$.pipe(
|
||||||
|
filterRelationsByTypeLabel('isPublicationOfOrgUnit'),
|
||||||
|
relationsToItems(this.item.id, this.ids)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
@@ -0,0 +1,58 @@
|
|||||||
|
<h2 class="item-page-title-field">
|
||||||
|
<ds-metadata-values [values]="item?.filterMetadata(['dc.contributor.author'])"></ds-metadata-values>
|
||||||
|
</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-md-4">
|
||||||
|
<ds-metadata-field-wrapper>
|
||||||
|
<ds-thumbnail [thumbnail]="this.item.getThumbnail() | async" [defaultImage]="'assets/images/person-placeholder.png'"></ds-thumbnail>
|
||||||
|
</ds-metadata-field-wrapper>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['person.identifier.email']"
|
||||||
|
[label]="'person.page.email'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['person.identifier.orcid']"
|
||||||
|
[label]="'person.page.orcid'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['person.identifier.birthdate']"
|
||||||
|
[label]="'person.page.birthdate'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['person.identifier.staffid']"
|
||||||
|
[label]="'person.page.staffid'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-6">
|
||||||
|
<ds-related-items
|
||||||
|
[items]="projects$ | async"
|
||||||
|
[label]="'relationships.isProjectOf' | translate">
|
||||||
|
</ds-related-items>
|
||||||
|
<ds-related-items
|
||||||
|
[items]="orgUnits$ | async"
|
||||||
|
[label]="'relationships.isOrgUnitOf' | translate">
|
||||||
|
</ds-related-items>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['person.identifier.jobtitle']"
|
||||||
|
[label]="'person.page.jobtitle'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['person.identifier.lastname']"
|
||||||
|
[label]="'person.page.lastname'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['person.identifier.firstname']"
|
||||||
|
[label]="'person.page.firstname'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<div>
|
||||||
|
<a class="btn btn-outline-primary" [routerLink]="['/items/' + item.id + '/full']">
|
||||||
|
{{"item.page.link.full" | translate}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 w-100">
|
||||||
|
<ds-related-entities-search [item]="item"
|
||||||
|
[relationType]="'isAuthorOfPublication'">
|
||||||
|
</ds-related-entities-search>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
@import '../../../../../styles/variables.scss';
|
@@ -0,0 +1,50 @@
|
|||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
|
import { createRelationshipsObservable, getItemPageFieldsTest } from '../shared/item.component.spec';
|
||||||
|
import { PersonComponent } from './person.component';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
const mockItem: Item = Object.assign(new Item(), {
|
||||||
|
bitstreams: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))),
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'person.identifier.email',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'fake@email.com'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'person.identifier.orcid',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'ORCID-1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'person.identifier.birthdate',
|
||||||
|
language: 'en_US',
|
||||||
|
value: '1993'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'person.identifier.staffid',
|
||||||
|
language: 'en_US',
|
||||||
|
value: '1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'person.identifier.jobtitle',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'Developer'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'person.identifier.lastname',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'Doe'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'person.identifier.firstname',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'John'
|
||||||
|
}],
|
||||||
|
relationships: createRelationshipsObservable()
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PersonComponent', getItemPageFieldsTest(mockItem, PersonComponent));
|
@@ -0,0 +1,77 @@
|
|||||||
|
import { Component, Inject } from '@angular/core';
|
||||||
|
import { Observable , of as observableOf } from 'rxjs';
|
||||||
|
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { rendersItemType } from '../../../../shared/items/item-type-decorator';
|
||||||
|
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
|
||||||
|
import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service';
|
||||||
|
import { isNotEmpty } from '../../../../shared/empty.util';
|
||||||
|
import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component';
|
||||||
|
import { VIEW_MODE_FULL } from '../../item-page.component';
|
||||||
|
|
||||||
|
@rendersItemType('Person', VIEW_MODE_FULL)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-person',
|
||||||
|
styleUrls: ['./person.component.scss'],
|
||||||
|
templateUrl: './person.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying metadata and relations of an item of the type Person
|
||||||
|
*/
|
||||||
|
export class PersonComponent extends ItemComponent {
|
||||||
|
/**
|
||||||
|
* The publications related to this person
|
||||||
|
*/
|
||||||
|
publications$: Observable<Item[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The projects related to this person
|
||||||
|
*/
|
||||||
|
projects$: Observable<Item[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The organisation units related to this person
|
||||||
|
*/
|
||||||
|
orgUnits$: Observable<Item[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The applied fixed filter
|
||||||
|
*/
|
||||||
|
fixedFilter$: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The query used for applying the fixed filter
|
||||||
|
*/
|
||||||
|
fixedFilterQuery: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(ITEM) public item: Item,
|
||||||
|
private ids: ItemDataService,
|
||||||
|
private fixedFilterService: SearchFixedFilterService
|
||||||
|
) {
|
||||||
|
super(item);
|
||||||
|
}
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
|
||||||
|
if (isNotEmpty(this.resolvedRelsAndTypes$)) {
|
||||||
|
this.publications$ = this.resolvedRelsAndTypes$.pipe(
|
||||||
|
filterRelationsByTypeLabel('isPublicationOfAuthor'),
|
||||||
|
relationsToItems(this.item.id, this.ids)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.projects$ = this.resolvedRelsAndTypes$.pipe(
|
||||||
|
filterRelationsByTypeLabel('isProjectOfPerson'),
|
||||||
|
relationsToItems(this.item.id, this.ids)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.orgUnits$ = this.resolvedRelsAndTypes$.pipe(
|
||||||
|
filterRelationsByTypeLabel('isOrgUnitOfPerson'),
|
||||||
|
relationsToItems(this.item.id, this.ids)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.fixedFilterQuery = this.fixedFilterService.getQueryByRelations('isAuthorOfPublication', this.item.id);
|
||||||
|
this.fixedFilter$ = observableOf('publication');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,49 @@
|
|||||||
|
<h2 class="item-page-title-field">
|
||||||
|
<ds-metadata-values [values]="item?.filterMetadata(['project.identifier.name'])"></ds-metadata-values>
|
||||||
|
</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-md-4">
|
||||||
|
<ds-metadata-field-wrapper>
|
||||||
|
<ds-thumbnail [thumbnail]="this.item.getThumbnail() | async" [defaultImage]="'assets/images/project-placeholder.png'"></ds-thumbnail>
|
||||||
|
</ds-metadata-field-wrapper>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['project.identifier.status']"
|
||||||
|
[label]="'project.page.status'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['project.identifier.id']"
|
||||||
|
[label]="'project.page.id'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['project.identifier.expectedcompletion']"
|
||||||
|
[label]="'project.page.expectedcompletion'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-6">
|
||||||
|
<ds-related-items
|
||||||
|
[items]="people$ | async"
|
||||||
|
[label]="'relationships.isPersonOf' | translate">
|
||||||
|
</ds-related-items>
|
||||||
|
<ds-related-items
|
||||||
|
[items]="publications$ | async"
|
||||||
|
[label]="'relationships.isPublicationOf' | translate">
|
||||||
|
</ds-related-items>
|
||||||
|
<ds-related-items
|
||||||
|
[items]="orgUnits$ | async"
|
||||||
|
[label]="'relationships.isOrgUnitOf' | translate">
|
||||||
|
</ds-related-items>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['project.identifier.description']"
|
||||||
|
[label]="'project.page.description'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['project.identifier.keyword']"
|
||||||
|
[label]="'project.page.keyword'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<div>
|
||||||
|
<a class="btn btn-outline-primary" [routerLink]="['/items/' + item.id + '/full']">
|
||||||
|
{{"item.page.link.full" | translate}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
@import '../../../../../styles/variables.scss';
|
@@ -0,0 +1,40 @@
|
|||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
|
import { createRelationshipsObservable, getItemPageFieldsTest } from '../shared/item.component.spec';
|
||||||
|
import { ProjectComponent } from './project.component';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
const mockItem: Item = Object.assign(new Item(), {
|
||||||
|
bitstreams: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))),
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'project.identifier.status',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'published'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'project.identifier.id',
|
||||||
|
language: 'en_US',
|
||||||
|
value: '1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'project.identifier.expectedcompletion',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'exp comp'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'project.identifier.description',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'keyword'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'project.identifier.keyword',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'keyword'
|
||||||
|
}],
|
||||||
|
relationships: createRelationshipsObservable()
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ProjectComponent', getItemPageFieldsTest(mockItem, ProjectComponent));
|
@@ -0,0 +1,63 @@
|
|||||||
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { rendersItemType } from '../../../../shared/items/item-type-decorator';
|
||||||
|
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
|
||||||
|
import { isNotEmpty } from '../../../../shared/empty.util';
|
||||||
|
import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component';
|
||||||
|
import { VIEW_MODE_FULL } from '../../item-page.component';
|
||||||
|
|
||||||
|
@rendersItemType('Project', VIEW_MODE_FULL)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-project',
|
||||||
|
styleUrls: ['./project.component.scss'],
|
||||||
|
templateUrl: './project.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying metadata and relations of an item of the type Project
|
||||||
|
*/
|
||||||
|
export class ProjectComponent extends ItemComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* The people related to this project
|
||||||
|
*/
|
||||||
|
people$: Observable<Item[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The publications related to this project
|
||||||
|
*/
|
||||||
|
publications$: Observable<Item[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The organisation units related to this project
|
||||||
|
*/
|
||||||
|
orgUnits$: Observable<Item[]>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(ITEM) public item: Item,
|
||||||
|
private ids: ItemDataService
|
||||||
|
) {
|
||||||
|
super(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
|
||||||
|
if (isNotEmpty(this.resolvedRelsAndTypes$)) {
|
||||||
|
this.people$ = this.resolvedRelsAndTypes$.pipe(
|
||||||
|
filterRelationsByTypeLabel('isPersonOfProject'),
|
||||||
|
relationsToItems(this.item.id, this.ids)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.publications$ = this.resolvedRelsAndTypes$.pipe(
|
||||||
|
filterRelationsByTypeLabel('isPublicationOfProject'),
|
||||||
|
relationsToItems(this.item.id, this.ids)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.orgUnits$ = this.resolvedRelsAndTypes$.pipe(
|
||||||
|
filterRelationsByTypeLabel('isOrgUnitOfProject'),
|
||||||
|
relationsToItems(this.item.id, this.ids)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,58 @@
|
|||||||
|
<ds-item-page-title-field [item]="item"></ds-item-page-title-field>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-md-4">
|
||||||
|
<ds-metadata-field-wrapper>
|
||||||
|
<ds-thumbnail [thumbnail]="this.item.getThumbnail() | async"></ds-thumbnail>
|
||||||
|
</ds-metadata-field-wrapper>
|
||||||
|
<ds-item-page-file-section [item]="item"></ds-item-page-file-section>
|
||||||
|
<ds-item-page-date-field [item]="item"></ds-item-page-date-field>
|
||||||
|
<ds-item-page-author-field *ngIf="!(authors$ | async)" [item]="item"></ds-item-page-author-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['journal.title']"
|
||||||
|
[label]="'publication.page.journal-title'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['journal.identifier.issn']"
|
||||||
|
[label]="'publication.page.journal-issn'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['journalvolume.identifier.name']"
|
||||||
|
[label]="'publication.page.volume-title'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-6">
|
||||||
|
<ds-metadata-representation-list
|
||||||
|
[label]="'relationships.isAuthorOf' | translate"
|
||||||
|
[representations]="authors$ | async">
|
||||||
|
</ds-metadata-representation-list>
|
||||||
|
<ds-related-items
|
||||||
|
[items]="projects$ | async"
|
||||||
|
[label]="'relationships.isProjectOf' | translate">
|
||||||
|
</ds-related-items>
|
||||||
|
<ds-related-items
|
||||||
|
[items]="orgUnits$ | async"
|
||||||
|
[label]="'relationships.isOrgUnitOf' | translate">
|
||||||
|
</ds-related-items>
|
||||||
|
<ds-related-items
|
||||||
|
[items]="journalIssues$ | async"
|
||||||
|
[label]="'relationships.isJournalIssueOf' | translate">
|
||||||
|
</ds-related-items>
|
||||||
|
<ds-item-page-abstract-field [item]="item"></ds-item-page-abstract-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['dc.subject']"
|
||||||
|
[separator]="','"
|
||||||
|
[label]="'item.page.subject'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-generic-item-page-field [item]="item"
|
||||||
|
[fields]="['dc.identifier.citation']"
|
||||||
|
[label]="'item.page.citation'">
|
||||||
|
</ds-generic-item-page-field>
|
||||||
|
<ds-item-page-uri-field [item]="item"></ds-item-page-uri-field>
|
||||||
|
<ds-item-page-collections [item]="item"></ds-item-page-collections>
|
||||||
|
<div>
|
||||||
|
<a class="btn btn-outline-primary" [routerLink]="['/items/' + item.id + '/full']">
|
||||||
|
{{"item.page.link.full" | translate}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
@import '../../../../../styles/variables.scss';
|
@@ -0,0 +1,89 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { MockTranslateLoader } from '../../../../shared/mocks/mock-translate-loader';
|
||||||
|
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
|
||||||
|
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
||||||
|
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
|
||||||
|
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||||
|
import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service';
|
||||||
|
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
|
||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { createRelationshipsObservable } from '../shared/item.component.spec';
|
||||||
|
import { PublicationComponent } from './publication.component';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
const mockItem: Item = Object.assign(new Item(), {
|
||||||
|
bitstreams: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))),
|
||||||
|
metadata: [],
|
||||||
|
relationships: createRelationshipsObservable()
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PublicationComponent', () => {
|
||||||
|
let comp: PublicationComponent;
|
||||||
|
let fixture: ComponentFixture<PublicationComponent>;
|
||||||
|
|
||||||
|
const searchFixedFilterServiceStub = {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
getQueryByRelations: () => {}
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [PublicationComponent, GenericItemPageFieldComponent, TruncatePipe],
|
||||||
|
providers: [
|
||||||
|
{provide: ITEM, useValue: mockItem},
|
||||||
|
{provide: ItemDataService, useValue: {}},
|
||||||
|
{provide: SearchFixedFilterService, useValue: searchFixedFilterServiceStub},
|
||||||
|
{provide: TruncatableService, useValue: {}}
|
||||||
|
],
|
||||||
|
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(PublicationComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
fixture = TestBed.createComponent(PublicationComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should contain a component to display the date', () => {
|
||||||
|
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-date-field'));
|
||||||
|
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a component to display the author', () => {
|
||||||
|
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-author-field'));
|
||||||
|
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a component to display the abstract', () => {
|
||||||
|
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-abstract-field'));
|
||||||
|
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a component to display the uri', () => {
|
||||||
|
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-uri-field'));
|
||||||
|
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a component to display the collections', () => {
|
||||||
|
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-collections'));
|
||||||
|
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,74 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import {
|
||||||
|
DEFAULT_ITEM_TYPE,
|
||||||
|
rendersItemType
|
||||||
|
} from '../../../../shared/items/item-type-decorator';
|
||||||
|
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
|
||||||
|
import { ItemComponent, filterRelationsByTypeLabel, relationsToItems } from '../shared/item.component';
|
||||||
|
import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model';
|
||||||
|
import { VIEW_MODE_FULL } from '../../item-page.component';
|
||||||
|
|
||||||
|
@rendersItemType('Publication', VIEW_MODE_FULL)
|
||||||
|
@rendersItemType(DEFAULT_ITEM_TYPE, VIEW_MODE_FULL)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-publication',
|
||||||
|
styleUrls: ['./publication.component.scss'],
|
||||||
|
templateUrl: './publication.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class PublicationComponent extends ItemComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* The authors related to this publication
|
||||||
|
*/
|
||||||
|
authors$: Observable<MetadataRepresentation[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The projects related to this publication
|
||||||
|
*/
|
||||||
|
projects$: Observable<Item[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The organisation units related to this publication
|
||||||
|
*/
|
||||||
|
orgUnits$: Observable<Item[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The journal issues related to this publication
|
||||||
|
*/
|
||||||
|
journalIssues$: Observable<Item[]>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(ITEM) public item: Item,
|
||||||
|
private ids: ItemDataService
|
||||||
|
) {
|
||||||
|
super(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
|
||||||
|
if (this.resolvedRelsAndTypes$) {
|
||||||
|
|
||||||
|
this.authors$ = this.buildRepresentations('Person', 'dc.contributor.author', this.ids);
|
||||||
|
|
||||||
|
this.projects$ = this.resolvedRelsAndTypes$.pipe(
|
||||||
|
filterRelationsByTypeLabel('isProjectOfPublication'),
|
||||||
|
relationsToItems(this.item.id, this.ids)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.orgUnits$ = this.resolvedRelsAndTypes$.pipe(
|
||||||
|
filterRelationsByTypeLabel('isOrgUnitOfPublication'),
|
||||||
|
relationsToItems(this.item.id, this.ids)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.journalIssues$ = this.resolvedRelsAndTypes$.pipe(
|
||||||
|
filterRelationsByTypeLabel('isJournalIssueOfPublication'),
|
||||||
|
relationsToItems(this.item.id, this.ids)
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,431 @@
|
|||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
|
||||||
|
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
|
||||||
|
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { MockTranslateLoader } from '../../../../shared/mocks/mock-translate-loader';
|
||||||
|
import { ChangeDetectionStrategy, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
|
||||||
|
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
||||||
|
import { isNotEmpty } from '../../../../shared/empty.util';
|
||||||
|
import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service';
|
||||||
|
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
||||||
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
|
import { compareArraysUsing, compareArraysUsingIds, ItemComponent } from './item.component';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { ItemPageComponent } from '../../item-page.component';
|
||||||
|
import { VarDirective } from '../../../../shared/utils/var.directive';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { MetadataService } from '../../../../core/metadata/metadata.service';
|
||||||
|
import { of } from 'rxjs/internal/observable/of';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model';
|
||||||
|
import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
|
||||||
|
import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a generic test for an item-page-fields component using a mockItem and the type of component
|
||||||
|
* @param {Item} mockItem The item to use for testing. The item needs to contain just the metadata necessary to
|
||||||
|
* execute the tests for it's component.
|
||||||
|
* @param component The type of component to create test cases for.
|
||||||
|
* @returns {() => void} Returns a specDefinition for the test.
|
||||||
|
*/
|
||||||
|
export function getItemPageFieldsTest(mockItem: Item, component) {
|
||||||
|
return () => {
|
||||||
|
let comp: any;
|
||||||
|
let fixture: ComponentFixture<any>;
|
||||||
|
|
||||||
|
const searchFixedFilterServiceStub = {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
getQueryByRelations: () => {}
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
})],
|
||||||
|
declarations: [component, GenericItemPageFieldComponent, TruncatePipe],
|
||||||
|
providers: [
|
||||||
|
{provide: ITEM, useValue: mockItem},
|
||||||
|
{provide: ItemDataService, useValue: {}},
|
||||||
|
{provide: SearchFixedFilterService, useValue: searchFixedFilterServiceStub},
|
||||||
|
{provide: TruncatableService, useValue: {}}
|
||||||
|
],
|
||||||
|
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(component, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
fixture = TestBed.createComponent(component);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (const metadata of mockItem.metadata) {
|
||||||
|
it(`should be calling a component with metadata field ${metadata.key}`, () => {
|
||||||
|
const fields = fixture.debugElement.queryAll(By.css('ds-generic-item-page-field'));
|
||||||
|
expect(containsFieldInput(fields, metadata.key)).toBeTruthy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether in a list of debug elements, at least one of them contains a specific metadata key in their
|
||||||
|
* fields property.
|
||||||
|
* @param {DebugElement[]} fields List of debug elements to check
|
||||||
|
* @param {string} metadataKey A metadata key to look for
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function containsFieldInput(fields: DebugElement[], metadataKey: string): boolean {
|
||||||
|
for (const field of fields) {
|
||||||
|
const fieldComp = field.componentInstance;
|
||||||
|
if (isNotEmpty(fieldComp.fields)) {
|
||||||
|
if (fieldComp.fields.indexOf(metadataKey) > -1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRelationshipsObservable() {
|
||||||
|
return observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), [
|
||||||
|
Object.assign(new Relationship(), {
|
||||||
|
relationshipType: observableOf(new RemoteData(false, false, true, null, new RelationshipType()))
|
||||||
|
})
|
||||||
|
])));
|
||||||
|
}
|
||||||
|
describe('ItemComponent', () => {
|
||||||
|
const arr1 = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'test'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'another test'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'one last test'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const arrWithWrongId = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'test'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5, // Wrong id on purpose
|
||||||
|
name: 'another test'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'one last test'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const arrWithWrongName = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'test'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'wrong test' // Wrong name on purpose
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'one last test'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const arrWithDifferentOrder = [arr1[0], arr1[2], arr1[1]];
|
||||||
|
const arrWithOneMore = [...arr1, {
|
||||||
|
id: 4,
|
||||||
|
name: 'fourth test'
|
||||||
|
}];
|
||||||
|
const arrWithAddedProperties = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'test',
|
||||||
|
extra: 'extra property'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'another test',
|
||||||
|
extra: 'extra property'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'one last test',
|
||||||
|
extra: 'extra property'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const arrOfPrimitiveTypes = [1, 2, 3, 4];
|
||||||
|
const arrOfPrimitiveTypesWithOneWrong = [1, 5, 3, 4];
|
||||||
|
const arrOfPrimitiveTypesWithDifferentOrder = [1, 3, 2, 4];
|
||||||
|
const arrOfPrimitiveTypesWithOneMore = [1, 2, 3, 4, 5];
|
||||||
|
|
||||||
|
describe('when calling compareArraysUsing', () => {
|
||||||
|
|
||||||
|
describe('and comparing by id', () => {
|
||||||
|
const compare = compareArraysUsing<any>((o) => o.id);
|
||||||
|
|
||||||
|
it('should return true when comparing the same array', () => {
|
||||||
|
expect(compare(arr1, arr1)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true regardless of the order', () => {
|
||||||
|
expect(compare(arr1, arrWithDifferentOrder)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true regardless of other properties being different', () => {
|
||||||
|
expect(compare(arr1, arrWithWrongName)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true regardless of extra properties', () => {
|
||||||
|
expect(compare(arr1, arrWithAddedProperties)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the ids don\'t match', () => {
|
||||||
|
expect(compare(arr1, arrWithWrongId)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the sizes don\'t match', () => {
|
||||||
|
expect(compare(arr1, arrWithOneMore)).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and comparing by name', () => {
|
||||||
|
const compare = compareArraysUsing<any>((o) => o.name);
|
||||||
|
|
||||||
|
it('should return true when comparing the same array', () => {
|
||||||
|
expect(compare(arr1, arr1)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true regardless of the order', () => {
|
||||||
|
expect(compare(arr1, arrWithDifferentOrder)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true regardless of other properties being different', () => {
|
||||||
|
expect(compare(arr1, arrWithWrongId)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true regardless of extra properties', () => {
|
||||||
|
expect(compare(arr1, arrWithAddedProperties)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the names don\'t match', () => {
|
||||||
|
expect(compare(arr1, arrWithWrongName)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the sizes don\'t match', () => {
|
||||||
|
expect(compare(arr1, arrWithOneMore)).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and comparing by full objects', () => {
|
||||||
|
const compare = compareArraysUsing<any>((o) => o);
|
||||||
|
|
||||||
|
it('should return true when comparing the same array', () => {
|
||||||
|
expect(compare(arr1, arr1)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true regardless of the order', () => {
|
||||||
|
expect(compare(arr1, arrWithDifferentOrder)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when extra properties are added', () => {
|
||||||
|
expect(compare(arr1, arrWithAddedProperties)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the ids don\'t match', () => {
|
||||||
|
expect(compare(arr1, arrWithWrongId)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the names don\'t match', () => {
|
||||||
|
expect(compare(arr1, arrWithWrongName)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the sizes don\'t match', () => {
|
||||||
|
expect(compare(arr1, arrWithOneMore)).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and comparing with primitive objects as source', () => {
|
||||||
|
const compare = compareArraysUsing<any>((o) => o);
|
||||||
|
|
||||||
|
it('should return true when comparing the same array', () => {
|
||||||
|
expect(compare(arrOfPrimitiveTypes, arrOfPrimitiveTypes)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true regardless of the order', () => {
|
||||||
|
expect(compare(arrOfPrimitiveTypes, arrOfPrimitiveTypesWithDifferentOrder)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when at least one is wrong', () => {
|
||||||
|
expect(compare(arrOfPrimitiveTypes, arrOfPrimitiveTypesWithOneWrong)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the sizes don\'t match', () => {
|
||||||
|
expect(compare(arrOfPrimitiveTypes, arrOfPrimitiveTypesWithOneMore)).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when calling compareArraysUsingIds', () => {
|
||||||
|
const compare = compareArraysUsingIds();
|
||||||
|
|
||||||
|
it('should return true when comparing the same array', () => {
|
||||||
|
expect(compare(arr1 as any, arr1 as any)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true regardless of the order', () => {
|
||||||
|
expect(compare(arr1 as any, arrWithDifferentOrder as any)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true regardless of other properties being different', () => {
|
||||||
|
expect(compare(arr1 as any, arrWithWrongName as any)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true regardless of extra properties', () => {
|
||||||
|
expect(compare(arr1 as any, arrWithAddedProperties as any)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the ids don\'t match', () => {
|
||||||
|
expect(compare(arr1 as any, arrWithWrongId as any)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the sizes don\'t match', () => {
|
||||||
|
expect(compare(arr1 as any, arrWithOneMore as any)).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when calling buildRepresentations', () => {
|
||||||
|
let comp: ItemComponent;
|
||||||
|
let fixture: ComponentFixture<ItemComponent>;
|
||||||
|
|
||||||
|
const metadataField = 'dc.contributor.author';
|
||||||
|
const mockItem = Object.assign(new Item(), {
|
||||||
|
id: '1',
|
||||||
|
uuid: '1',
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: metadataField,
|
||||||
|
value: 'Second value',
|
||||||
|
place: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: metadataField,
|
||||||
|
value: 'Third value',
|
||||||
|
place: 2,
|
||||||
|
authority: 'virtual::123'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: metadataField,
|
||||||
|
value: 'First value',
|
||||||
|
place: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: metadataField,
|
||||||
|
value: 'Fourth value',
|
||||||
|
place: 3,
|
||||||
|
authority: '123'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
relationships: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), [
|
||||||
|
Object.assign(new Relationship(), {
|
||||||
|
uuid: '123',
|
||||||
|
id: '123',
|
||||||
|
leftId: '1',
|
||||||
|
rightId: '2',
|
||||||
|
relationshipType: observableOf(new RemoteData(false, false, true, null, new RelationshipType()))
|
||||||
|
})
|
||||||
|
])))
|
||||||
|
});
|
||||||
|
const relatedItem = Object.assign(new Item(), {
|
||||||
|
id: '2',
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'dc.title',
|
||||||
|
value: 'related item'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
const mockItemDataService = {
|
||||||
|
findById: (id) => {
|
||||||
|
if (id === relatedItem.id) {
|
||||||
|
return observableOf(new RemoteData(false, false, true, null, relatedItem))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as ItemDataService;
|
||||||
|
|
||||||
|
let representations: Observable<MetadataRepresentation[]>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: MockTranslateLoader
|
||||||
|
}
|
||||||
|
}), BrowserAnimationsModule],
|
||||||
|
declarations: [ItemComponent, VarDirective],
|
||||||
|
providers: [
|
||||||
|
{provide: ITEM, useValue: mockItem}
|
||||||
|
],
|
||||||
|
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(ItemComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
fixture = TestBed.createComponent(ItemComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
representations = comp.buildRepresentations('bogus', metadataField, mockItemDataService);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should contain exactly 4 metadata-representations', () => {
|
||||||
|
representations.subscribe((reps: MetadataRepresentation[]) => {
|
||||||
|
expect(reps.length).toEqual(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have all the representations in the correct order', () => {
|
||||||
|
representations.subscribe((reps: MetadataRepresentation[]) => {
|
||||||
|
expect(reps[0].getValue()).toEqual('First value');
|
||||||
|
expect(reps[1].getValue()).toEqual('Second value');
|
||||||
|
expect(reps[2].getValue()).toEqual('related item');
|
||||||
|
expect(reps[3].getValue()).toEqual('Fourth value');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have created the correct MetadatumRepresentation and ItemMetadataRepresentation objects for the correct Metadata', () => {
|
||||||
|
representations.subscribe((reps: MetadataRepresentation[]) => {
|
||||||
|
expect(reps[0] instanceof MetadatumRepresentation).toEqual(true);
|
||||||
|
expect(reps[1] instanceof MetadatumRepresentation).toEqual(true);
|
||||||
|
expect(reps[2] instanceof ItemMetadataRepresentation).toEqual(true);
|
||||||
|
expect(reps[3] instanceof MetadatumRepresentation).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
190
src/app/+item-page/simple/item-types/shared/item.component.ts
Normal file
190
src/app/+item-page/simple/item-types/shared/item.component.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
|
import { Observable , zip as observableZip, combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
|
import { distinctUntilChanged, filter, flatMap, map } from 'rxjs/operators';
|
||||||
|
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
|
||||||
|
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||||
|
import { hasNoValue, hasValue } from '../../../../shared/empty.util';
|
||||||
|
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
|
||||||
|
import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model';
|
||||||
|
import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
||||||
|
import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
|
||||||
|
import { of } from 'rxjs/internal/observable/of';
|
||||||
|
import { MetadataValue } from '../../../../core/shared/metadata.models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator for comparing arrays using a mapping function
|
||||||
|
* The mapping function should turn the source array into an array of basic types, so that the array can
|
||||||
|
* be compared using these basic types.
|
||||||
|
* For example: "(o) => o.id" will compare the two arrays by comparing their content by id.
|
||||||
|
* @param mapFn Function for mapping the arrays
|
||||||
|
*/
|
||||||
|
export const compareArraysUsing = <T>(mapFn: (t: T) => any) =>
|
||||||
|
(a: T[], b: T[]): boolean => {
|
||||||
|
if (!Array.isArray(a) || ! Array.isArray(b)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const aIds = a.map(mapFn);
|
||||||
|
const bIds = b.map(mapFn);
|
||||||
|
|
||||||
|
return aIds.length === bIds.length &&
|
||||||
|
aIds.every((e) => bIds.includes(e)) &&
|
||||||
|
bIds.every((e) => aIds.includes(e));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator for comparing arrays using the object's ids
|
||||||
|
*/
|
||||||
|
export const compareArraysUsingIds = <T extends { id: string }>() =>
|
||||||
|
compareArraysUsing((t: T) => hasValue(t) ? t.id : undefined);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the relationships which match the type label given
|
||||||
|
* @param {string} label Type label
|
||||||
|
* @returns {(source: Observable<[Relationship[] , RelationshipType[]]>) => Observable<Relationship[]>}
|
||||||
|
*/
|
||||||
|
export const filterRelationsByTypeLabel = (label: string) =>
|
||||||
|
(source: Observable<[Relationship[], RelationshipType[]]>): Observable<Relationship[]> =>
|
||||||
|
source.pipe(
|
||||||
|
map(([relsCurrentPage, relTypesCurrentPage]) =>
|
||||||
|
relsCurrentPage.filter((rel: Relationship, idx: number) =>
|
||||||
|
hasValue(relTypesCurrentPage[idx]) && (relTypesCurrentPage[idx].leftLabel === label ||
|
||||||
|
relTypesCurrentPage[idx].rightLabel === label)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
distinctUntilChanged(compareArraysUsingIds())
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator for turning a list of relationships into a list of the relevant items
|
||||||
|
* @param {string} thisId The item's id of which the relations belong to
|
||||||
|
* @param {ItemDataService} ids The ItemDataService to fetch items from the REST API
|
||||||
|
* @returns {(source: Observable<Relationship[]>) => Observable<Item[]>}
|
||||||
|
*/
|
||||||
|
export const relationsToItems = (thisId: string, ids: ItemDataService) =>
|
||||||
|
(source: Observable<Relationship[]>): Observable<Item[]> =>
|
||||||
|
source.pipe(
|
||||||
|
flatMap((rels: Relationship[]) =>
|
||||||
|
observableZip(
|
||||||
|
...rels.map((rel: Relationship) => {
|
||||||
|
let queryId = rel.leftId;
|
||||||
|
if (rel.leftId === thisId) {
|
||||||
|
queryId = rel.rightId;
|
||||||
|
}
|
||||||
|
return ids.findById(queryId);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
map((arr: Array<RemoteData<Item>>) =>
|
||||||
|
arr
|
||||||
|
.filter((d: RemoteData<Item>) => d.hasSucceeded)
|
||||||
|
.map((d: RemoteData<Item>) => d.payload)),
|
||||||
|
distinctUntilChanged(compareArraysUsingIds()),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator for turning a list of relationships into a list of metadatarepresentations given the original metadata
|
||||||
|
* @param thisId The id of the parent item
|
||||||
|
* @param itemType The type of relation this list resembles (for creating representations)
|
||||||
|
* @param metadata The list of original Metadatum objects
|
||||||
|
* @param ids The ItemDataService to use for fetching Items from the Rest API
|
||||||
|
*/
|
||||||
|
export const relationsToRepresentations = (thisId: string, itemType: string, metadata: MetadataValue[], ids: ItemDataService) =>
|
||||||
|
(source: Observable<Relationship[]>): Observable<MetadataRepresentation[]> =>
|
||||||
|
source.pipe(
|
||||||
|
flatMap((rels: Relationship[]) =>
|
||||||
|
observableZip(
|
||||||
|
...metadata
|
||||||
|
.map((metadatum: any) => Object.assign(new MetadataValue(), metadatum))
|
||||||
|
.map((metadatum: MetadataValue) => {
|
||||||
|
if (metadatum.isVirtual) {
|
||||||
|
const matchingRels = rels.filter((rel: Relationship) => ('' + rel.id) === metadatum.virtualValue);
|
||||||
|
if (matchingRels.length > 0) {
|
||||||
|
const matchingRel = matchingRels[0];
|
||||||
|
let queryId = matchingRel.leftId;
|
||||||
|
if (matchingRel.leftId === thisId) {
|
||||||
|
queryId = matchingRel.rightId;
|
||||||
|
}
|
||||||
|
return ids.findById(queryId).pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
map((d: RemoteData<Item>) => Object.assign(new ItemMetadataRepresentation(itemType), d.payload))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return of(Object.assign(new MetadatumRepresentation(itemType), metadatum));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item',
|
||||||
|
template: ''
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* A generic component for displaying metadata and relations of an item
|
||||||
|
*/
|
||||||
|
export class ItemComponent implements OnInit {
|
||||||
|
/**
|
||||||
|
* Resolved relationships and types together in one observable
|
||||||
|
*/
|
||||||
|
resolvedRelsAndTypes$: Observable<[Relationship[], RelationshipType[]]>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(ITEM) public item: Item
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const relationships$ = this.item.relationships;
|
||||||
|
if (relationships$) {
|
||||||
|
const relsCurrentPage$ = relationships$.pipe(
|
||||||
|
filter((rd: RemoteData<PaginatedList<Relationship>>) => rd.hasSucceeded),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((pl: PaginatedList<Relationship>) => pl.page),
|
||||||
|
distinctUntilChanged(compareArraysUsingIds())
|
||||||
|
);
|
||||||
|
|
||||||
|
const relTypesCurrentPage$ = relsCurrentPage$.pipe(
|
||||||
|
flatMap((rels: Relationship[]) =>
|
||||||
|
observableZip(...rels.map((rel: Relationship) => rel.relationshipType)).pipe(
|
||||||
|
map(([...arr]: Array<RemoteData<RelationshipType>>) => arr.map((d: RemoteData<RelationshipType>) => d.payload))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
distinctUntilChanged(compareArraysUsingIds())
|
||||||
|
);
|
||||||
|
|
||||||
|
this.resolvedRelsAndTypes$ = observableCombineLatest(
|
||||||
|
relsCurrentPage$,
|
||||||
|
relTypesCurrentPage$
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a list of MetadataRepresentations for the current item. This combines all metadata and relationships of a
|
||||||
|
* certain type.
|
||||||
|
* @param itemType The type of item we're building representations of. Used for matching templates.
|
||||||
|
* @param metadataField The metadata field that resembles the item type.
|
||||||
|
* @param itemDataService ItemDataService to turn relations into items.
|
||||||
|
*/
|
||||||
|
buildRepresentations(itemType: string, metadataField: string, itemDataService: ItemDataService): Observable<MetadataRepresentation[]> {
|
||||||
|
const metadata = this.item.findMetadataSortedByPlace(metadataField);
|
||||||
|
const relsCurrentPage$ = this.item.relationships.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((pl: PaginatedList<Relationship>) => pl.page),
|
||||||
|
distinctUntilChanged(compareArraysUsingIds())
|
||||||
|
);
|
||||||
|
|
||||||
|
return relsCurrentPage$.pipe(
|
||||||
|
relationsToRepresentations(this.item.id, itemType, metadata, itemDataService)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,5 @@
|
|||||||
|
<ds-metadata-field-wrapper *ngIf="representations && representations.length > 0" [label]="label">
|
||||||
|
<ds-item-type-switcher *ngFor="let rep of representations"
|
||||||
|
[object]="rep" [viewMode]="viewMode">
|
||||||
|
</ds-item-type-switcher>
|
||||||
|
</ds-metadata-field-wrapper>
|
@@ -0,0 +1,40 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { MetadataRepresentationListComponent } from './metadata-representation-list.component';
|
||||||
|
import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
|
||||||
|
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
||||||
|
|
||||||
|
const itemType = 'type';
|
||||||
|
const metadataRepresentation1 = new MetadatumRepresentation(itemType);
|
||||||
|
const metadataRepresentation2 = new ItemMetadataRepresentation(itemType);
|
||||||
|
const representations = [metadataRepresentation1, metadataRepresentation2];
|
||||||
|
|
||||||
|
describe('MetadataRepresentationListComponent', () => {
|
||||||
|
let comp: MetadataRepresentationListComponent;
|
||||||
|
let fixture: ComponentFixture<MetadataRepresentationListComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [],
|
||||||
|
declarations: [MetadataRepresentationListComponent],
|
||||||
|
providers: [],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(MetadataRepresentationListComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
fixture = TestBed.createComponent(MetadataRepresentationListComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.representations = representations;
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it(`should load ${representations.length} item-type-switcher components`, () => {
|
||||||
|
const fields = fixture.debugElement.queryAll(By.css('ds-item-type-switcher'));
|
||||||
|
expect(fields.length).toBe(representations.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,31 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import * as viewMode from '../../../shared/view-mode';
|
||||||
|
import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model';
|
||||||
|
|
||||||
|
export const VIEW_MODE_METADATA = 'metadata';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-metadata-representation-list',
|
||||||
|
templateUrl: './metadata-representation-list.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* This component is used for displaying metadata
|
||||||
|
* It expects a list of MetadataRepresentation objects and a label to put on top of the list
|
||||||
|
*/
|
||||||
|
export class MetadataRepresentationListComponent {
|
||||||
|
/**
|
||||||
|
* A list of metadata-representations to display
|
||||||
|
*/
|
||||||
|
@Input() representations: MetadataRepresentation[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An i18n label to use as a title for the list
|
||||||
|
*/
|
||||||
|
@Input() label: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view-mode we're currently on
|
||||||
|
* @type {ElementViewMode}
|
||||||
|
*/
|
||||||
|
viewMode = VIEW_MODE_METADATA;
|
||||||
|
}
|
@@ -0,0 +1,6 @@
|
|||||||
|
<ds-filtered-search-page
|
||||||
|
[fixedFilterQuery]="fixedFilter"
|
||||||
|
[fixedFilter$]="fixedFilter$"
|
||||||
|
[searchEnabled]="searchEnabled"
|
||||||
|
[sideBarWidth]="sideBarWidth">
|
||||||
|
</ds-filtered-search-page>
|
@@ -0,0 +1,56 @@
|
|||||||
|
import { RelatedEntitiesSearchComponent } from './related-entities-search.component';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
|
||||||
|
describe('RelatedEntitiesSearchComponent', () => {
|
||||||
|
let comp: RelatedEntitiesSearchComponent;
|
||||||
|
let fixture: ComponentFixture<RelatedEntitiesSearchComponent>;
|
||||||
|
let fixedFilterService: SearchFixedFilterService;
|
||||||
|
|
||||||
|
const mockItem = Object.assign(new Item(), {
|
||||||
|
id: 'id1'
|
||||||
|
});
|
||||||
|
const mockRelationType = 'publicationsOfAuthor';
|
||||||
|
const mockRelationEntityType = 'publication';
|
||||||
|
const mockFilter= `f.${mockRelationType}=${mockItem.id}`;
|
||||||
|
const fixedFilterServiceStub = {
|
||||||
|
getFilterByRelation: () => mockFilter
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule],
|
||||||
|
declarations: [RelatedEntitiesSearchComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: SearchFixedFilterService, useValue: fixedFilterServiceStub }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(RelatedEntitiesSearchComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixedFilterService = (comp as any).fixedFilterService;
|
||||||
|
comp.relationType = mockRelationType;
|
||||||
|
comp.item = mockItem;
|
||||||
|
comp.relationEntityType = mockRelationEntityType;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a fixedFilter', () => {
|
||||||
|
expect(comp.fixedFilter).toEqual(mockFilter);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a fixedFilter$', () => {
|
||||||
|
comp.fixedFilter$.subscribe((fixedFilter) => {
|
||||||
|
expect(fixedFilter).toEqual(mockRelationEntityType);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,64 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { SearchFixedFilterService } from '../../../../+search-page/search-filters/search-filter/search-fixed-filter.service';
|
||||||
|
import { isNotEmpty } from '../../../../shared/empty.util';
|
||||||
|
import { of } from 'rxjs/internal/observable/of';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-related-entities-search',
|
||||||
|
templateUrl: './related-entities-search.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* A component to show related items as search results.
|
||||||
|
* Related items can be facetted, or queried using an
|
||||||
|
* optional search box.
|
||||||
|
*/
|
||||||
|
export class RelatedEntitiesSearchComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of relationship to fetch items for
|
||||||
|
* e.g. 'isAuthorOfPublication'
|
||||||
|
*/
|
||||||
|
@Input() relationType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item to render relationships for
|
||||||
|
*/
|
||||||
|
@Input() item: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The entity type of the relationship items to be displayed
|
||||||
|
* e.g. 'publication'
|
||||||
|
* This determines the title of the search results (if search is enabled)
|
||||||
|
*/
|
||||||
|
@Input() relationEntityType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the search bar and title should be displayed (defaults to true)
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
@Input() searchEnabled = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ratio of the sidebar's width compared to the search results (1-12) (defaults to 4)
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
@Input() sideBarWidth = 4;
|
||||||
|
|
||||||
|
fixedFilter: string;
|
||||||
|
fixedFilter$: Observable<string>;
|
||||||
|
|
||||||
|
constructor(private fixedFilterService: SearchFixedFilterService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (isNotEmpty(this.relationType) && isNotEmpty(this.item)) {
|
||||||
|
this.fixedFilter = this.fixedFilterService.getFilterByRelation(this.relationType, this.item.id);
|
||||||
|
}
|
||||||
|
if (isNotEmpty(this.relationEntityType)) {
|
||||||
|
this.fixedFilter$ = of(this.relationEntityType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,31 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
|
||||||
|
export const VIEW_MODE_ELEMENT = 'element';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-related-items',
|
||||||
|
styleUrls: ['./related-items.component.scss'],
|
||||||
|
templateUrl: './related-items.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* This component is used for displaying relations between items
|
||||||
|
* It expects a list of items to display and a label to put on top
|
||||||
|
*/
|
||||||
|
export class RelatedItemsComponent {
|
||||||
|
/**
|
||||||
|
* A list of items to display
|
||||||
|
*/
|
||||||
|
@Input() items: Item[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An i18n label to use as a title for the list (usually describes the relation)
|
||||||
|
*/
|
||||||
|
@Input() label: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view-mode we're currently on
|
||||||
|
* @type {ElementViewMode}
|
||||||
|
*/
|
||||||
|
viewMode = VIEW_MODE_ELEMENT;
|
||||||
|
}
|
@@ -0,0 +1,5 @@
|
|||||||
|
<ds-metadata-field-wrapper *ngIf="items && items.length > 0" [label]="label">
|
||||||
|
<ds-item-type-switcher *ngFor="let item of items"
|
||||||
|
[object]="item" [viewMode]="viewMode">
|
||||||
|
</ds-item-type-switcher>
|
||||||
|
</ds-metadata-field-wrapper>
|
@@ -0,0 +1,51 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { RelatedItemsComponent } from './related-items-component';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { createRelationshipsObservable } from '../item-types/shared/item.component.spec';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
const mockItem1: Item = Object.assign(new Item(), {
|
||||||
|
bitstreams: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))),
|
||||||
|
metadata: [],
|
||||||
|
relationships: createRelationshipsObservable()
|
||||||
|
});
|
||||||
|
const mockItem2: Item = Object.assign(new Item(), {
|
||||||
|
bitstreams: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []))),
|
||||||
|
metadata: [],
|
||||||
|
relationships: createRelationshipsObservable()
|
||||||
|
});
|
||||||
|
const mockItems = [mockItem1, mockItem2];
|
||||||
|
|
||||||
|
describe('RelatedItemsComponent', () => {
|
||||||
|
let comp: RelatedItemsComponent;
|
||||||
|
let fixture: ComponentFixture<RelatedItemsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [],
|
||||||
|
declarations: [RelatedItemsComponent],
|
||||||
|
providers: [],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(RelatedItemsComponent, {
|
||||||
|
set: {changeDetection: ChangeDetectionStrategy.Default}
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
fixture = TestBed.createComponent(RelatedItemsComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.items = mockItems;
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it(`should load ${mockItems.length} item-type-switcher components`, () => {
|
||||||
|
const fields = fixture.debugElement.queryAll(By.css('ds-item-type-switcher'));
|
||||||
|
expect(fields.length).toBe(mockItems.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
37
src/app/+search-page/filtered-search-page.component.spec.ts
Normal file
37
src/app/+search-page/filtered-search-page.component.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { FilteredSearchPageComponent } from './filtered-search-page.component';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { configureSearchComponentTestingModule } from './search-page.component.spec';
|
||||||
|
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||||
|
|
||||||
|
describe('FilteredSearchPageComponent', () => {
|
||||||
|
let comp: FilteredSearchPageComponent;
|
||||||
|
let fixture: ComponentFixture<FilteredSearchPageComponent>;
|
||||||
|
let searchConfigService: SearchConfigurationService;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
configureSearchComponentTestingModule(FilteredSearchPageComponent);
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(FilteredSearchPageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
searchConfigService = (comp as any).searchConfigService;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when fixedFilterQuery is defined', () => {
|
||||||
|
const fixedFilterQuery = 'fixedFilterQuery';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(searchConfigService, 'updateFixedFilter').and.callThrough();
|
||||||
|
comp.fixedFilterQuery = fixedFilterQuery;
|
||||||
|
comp.ngOnInit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the paginated search options', () => {
|
||||||
|
expect(searchConfigService.updateFixedFilter).toHaveBeenCalledWith(fixedFilterQuery);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
53
src/app/+search-page/filtered-search-page.component.ts
Normal file
53
src/app/+search-page/filtered-search-page.component.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
|
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
|
||||||
|
import { SearchService } from './search-service/search.service';
|
||||||
|
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||||
|
import { SearchPageComponent } from './search-page.component';
|
||||||
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||||
|
import { pushInOut } from '../shared/animations/push';
|
||||||
|
import { RouteService } from '../shared/services/route.service';
|
||||||
|
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { PaginatedSearchOptions } from './paginated-search-options.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component renders a simple item page.
|
||||||
|
* The route parameter 'id' is used to request the item it represents.
|
||||||
|
* All fields of the item that should be displayed, are defined in its template.
|
||||||
|
*/
|
||||||
|
@Component({selector: 'ds-filtered-search-page',
|
||||||
|
styleUrls: ['./search-page.component.scss'],
|
||||||
|
templateUrl: './search-page.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
animations: [pushInOut]
|
||||||
|
})
|
||||||
|
|
||||||
|
export class FilteredSearchPageComponent extends SearchPageComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The actual query for the fixed filter.
|
||||||
|
* If empty, the query will be determined by the route parameter called 'filter'
|
||||||
|
*/
|
||||||
|
@Input() fixedFilterQuery: string;
|
||||||
|
|
||||||
|
constructor(protected service: SearchService,
|
||||||
|
protected sidebarService: SearchSidebarService,
|
||||||
|
protected windowService: HostWindowService,
|
||||||
|
protected filterService: SearchFilterService,
|
||||||
|
protected searchConfigService: SearchConfigurationService,
|
||||||
|
protected routeService: RouteService) {
|
||||||
|
super(service, sidebarService, windowService, filterService, searchConfigService, routeService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current paginated search options after updating the fixed filter using the fixedFilterQuery input
|
||||||
|
* This is to make sure the fixed filter is included in the paginated search options, as it is not part of any
|
||||||
|
* query or route parameters
|
||||||
|
* @returns {Observable<PaginatedSearchOptions>}
|
||||||
|
*/
|
||||||
|
protected getSearchOptions(): Observable<PaginatedSearchOptions> {
|
||||||
|
this.searchConfigService.updateFixedFilter(this.fixedFilterQuery);
|
||||||
|
return this.searchConfigService.paginatedSearchOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
18
src/app/+search-page/filtered-search-page.guard.ts
Normal file
18
src/app/+search-page/filtered-search-page.guard.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
|
||||||
|
export class FilteredSearchPageGuard implements CanActivate {
|
||||||
|
canActivate(
|
||||||
|
route: ActivatedRouteSnapshot,
|
||||||
|
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
|
||||||
|
const filter = route.params.filter;
|
||||||
|
|
||||||
|
const newTitle = route.data.title + filter + '.title';
|
||||||
|
|
||||||
|
route.data = { title: newTitle };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@@ -12,7 +12,7 @@ export class PaginatedSearchOptions extends SearchOptions {
|
|||||||
pagination?: PaginationComponentOptions;
|
pagination?: PaginationComponentOptions;
|
||||||
sort?: SortOptions;
|
sort?: SortOptions;
|
||||||
|
|
||||||
constructor(options: {scope?: string, query?: string, dsoType?: DSpaceObjectType, filters?: SearchFilter[], pagination?: PaginationComponentOptions, sort?: SortOptions}) {
|
constructor(options: {scope?: string, query?: string, dsoType?: DSpaceObjectType, filters?: SearchFilter[], fixedFilter?: any, pagination?: PaginationComponentOptions, sort?: SortOptions}) {
|
||||||
super(options);
|
super(options);
|
||||||
this.pagination = options.pagination;
|
this.pagination = options.pagination;
|
||||||
this.sort = options.sort;
|
this.sort = options.sort;
|
||||||
|
@@ -13,8 +13,10 @@ import {
|
|||||||
import { SearchFiltersState } from './search-filter.reducer';
|
import { SearchFiltersState } from './search-filter.reducer';
|
||||||
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||||
import { FilterType } from '../../search-service/filter-type.model';
|
import { FilterType } from '../../search-service/filter-type.model';
|
||||||
|
import { SearchFixedFilterService } from './search-fixed-filter.service';
|
||||||
import { ActivatedRouteStub } from '../../../shared/testing/active-router-stub';
|
import { ActivatedRouteStub } from '../../../shared/testing/active-router-stub';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||||
|
|
||||||
describe('SearchFilterService', () => {
|
describe('SearchFilterService', () => {
|
||||||
let service: SearchFilterService;
|
let service: SearchFilterService;
|
||||||
@@ -26,6 +28,12 @@ describe('SearchFilterService', () => {
|
|||||||
isOpenByDefault: false,
|
isOpenByDefault: false,
|
||||||
pageSize: 2
|
pageSize: 2
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockFixedFilterService: SearchFixedFilterService = {
|
||||||
|
getQueryByFilterName: (filter: string) => {
|
||||||
|
return observableOf(undefined)
|
||||||
|
}
|
||||||
|
} as SearchFixedFilterService
|
||||||
const value1 = 'random value';
|
const value1 = 'random value';
|
||||||
// const value2 = 'another value';
|
// const value2 = 'another value';
|
||||||
const store: Store<SearchFiltersState> = jasmine.createSpyObj('store', {
|
const store: Store<SearchFiltersState> = jasmine.createSpyObj('store', {
|
||||||
@@ -45,11 +53,15 @@ describe('SearchFilterService', () => {
|
|||||||
},
|
},
|
||||||
addQueryParameterValue: (param: string, value: string) => {
|
addQueryParameterValue: (param: string, value: string) => {
|
||||||
},
|
},
|
||||||
|
getQueryParameterValue: (param: string) => {
|
||||||
|
},
|
||||||
getQueryParameterValues: (param: string) => {
|
getQueryParameterValues: (param: string) => {
|
||||||
return observableOf({});
|
return observableOf({});
|
||||||
},
|
},
|
||||||
getQueryParamsWithPrefix: (param: string) => {
|
getQueryParamsWithPrefix: (param: string) => {
|
||||||
return observableOf({});
|
return observableOf({});
|
||||||
|
},
|
||||||
|
getRouteParameterValue: (param: string) => {
|
||||||
}
|
}
|
||||||
/* tslint:enable:no-empty */
|
/* tslint:enable:no-empty */
|
||||||
};
|
};
|
||||||
@@ -59,7 +71,7 @@ describe('SearchFilterService', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service = new SearchFilterService(store, routeServiceStub);
|
service = new SearchFilterService(store, routeServiceStub, mockFixedFilterService);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the initialCollapse method is triggered', () => {
|
describe('when the initialCollapse method is triggered', () => {
|
||||||
@@ -179,4 +191,185 @@ describe('SearchFilterService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when the getCurrentScope method is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(routeServiceStub, 'getQueryParameterValue');
|
||||||
|
service.getCurrentScope();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getQueryParameterValue on the route service with scope', () => {
|
||||||
|
expect(routeServiceStub.getQueryParameterValue).toHaveBeenCalledWith('scope');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the getCurrentQuery method is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(routeServiceStub, 'getQueryParameterValue');
|
||||||
|
service.getCurrentQuery();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getQueryParameterValue on the route service with query', () => {
|
||||||
|
expect(routeServiceStub.getQueryParameterValue).toHaveBeenCalledWith('query');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the getCurrentPagination method is called', () => {
|
||||||
|
let result;
|
||||||
|
const mockReturn = 5;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(routeServiceStub, 'getQueryParameterValue').and.returnValue(observableOf(mockReturn));
|
||||||
|
result = service.getCurrentPagination();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getQueryParameterValue on the route service with page', () => {
|
||||||
|
expect(routeServiceStub.getQueryParameterValue).toHaveBeenCalledWith('page');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getQueryParameterValue on the route service with pageSize', () => {
|
||||||
|
expect(routeServiceStub.getQueryParameterValue).toHaveBeenCalledWith('pageSize');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an observable containing the correct pagination', () => {
|
||||||
|
result.subscribe((pagination) => {
|
||||||
|
expect(pagination.currentPage).toBe(mockReturn);
|
||||||
|
expect(pagination.pageSize).toBe(mockReturn);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the getCurrentSort method is called', () => {
|
||||||
|
let result;
|
||||||
|
const field = 'author';
|
||||||
|
const direction = SortDirection.ASC;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(routeServiceStub, 'getQueryParameterValue').and.returnValue(observableOf(undefined));
|
||||||
|
result = service.getCurrentSort(new SortOptions(field, direction));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getQueryParameterValue on the route service with sortDirection', () => {
|
||||||
|
expect(routeServiceStub.getQueryParameterValue).toHaveBeenCalledWith('sortDirection');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getQueryParameterValue on the route service with sortField', () => {
|
||||||
|
expect(routeServiceStub.getQueryParameterValue).toHaveBeenCalledWith('sortField');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an observable containing the correct sortOptions', () => {
|
||||||
|
result.subscribe((sort) => {
|
||||||
|
expect(sort.field).toBe(field);
|
||||||
|
expect(sort.direction).toBe(direction);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the getCurrentFilters method is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(routeServiceStub, 'getQueryParamsWithPrefix');
|
||||||
|
service.getCurrentFilters();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getQueryParamsWithPrefix on the route service with prefix \'f.\'', () => {
|
||||||
|
expect(routeServiceStub.getQueryParamsWithPrefix).toHaveBeenCalledWith('f.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the getCurrentFixedFilter method is called', () => {
|
||||||
|
const filter = 'filter';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(routeServiceStub, 'getRouteParameterValue').and.returnValue(observableOf(filter));
|
||||||
|
spyOn(mockFixedFilterService, 'getQueryByFilterName').and.returnValue(observableOf(filter));
|
||||||
|
service.getCurrentFixedFilter().subscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getQueryByFilterName on the fixed-filter service with the correct filter', () => {
|
||||||
|
expect(mockFixedFilterService.getQueryByFilterName).toHaveBeenCalledWith(filter);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the getCurrentView method is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(routeServiceStub, 'getQueryParameterValue');
|
||||||
|
service.getCurrentView();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getQueryParameterValue on the route service with view', () => {
|
||||||
|
expect(routeServiceStub.getQueryParameterValue).toHaveBeenCalledWith('view');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the getPaginatedSearchOptions method is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(service, 'getCurrentPagination');
|
||||||
|
spyOn(service, 'getCurrentSort');
|
||||||
|
spyOn(service, 'getCurrentView');
|
||||||
|
spyOn(service, 'getCurrentScope');
|
||||||
|
spyOn(service, 'getCurrentQuery');
|
||||||
|
spyOn(service, 'getCurrentFilters');
|
||||||
|
spyOn(service, 'getCurrentFixedFilter');
|
||||||
|
service.getPaginatedSearchOptions();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getCurrentPagination to build the paginated search options', () => {
|
||||||
|
expect(service.getCurrentPagination).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getCurrentSort to build the paginated search options', () => {
|
||||||
|
expect(service.getCurrentSort).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getCurrentView to build the paginated search options', () => {
|
||||||
|
expect(service.getCurrentView).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getCurrentScope to build the paginated search options', () => {
|
||||||
|
expect(service.getCurrentScope).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getCurrentQuery to build the paginated search options', () => {
|
||||||
|
expect(service.getCurrentQuery).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getCurrentFilters to build the paginated search options', () => {
|
||||||
|
expect(service.getCurrentFilters).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getCurrentFixedFilter to build the paginated search options', () => {
|
||||||
|
expect(service.getCurrentFixedFilter).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the getSearchOptions method is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(service, 'getCurrentView');
|
||||||
|
spyOn(service, 'getCurrentScope');
|
||||||
|
spyOn(service, 'getCurrentQuery');
|
||||||
|
spyOn(service, 'getCurrentFilters');
|
||||||
|
spyOn(service, 'getCurrentFixedFilter');
|
||||||
|
service.getPaginatedSearchOptions();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getCurrentView to build the search options', () => {
|
||||||
|
expect(service.getCurrentView).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getCurrentScope to build the search options', () => {
|
||||||
|
expect(service.getCurrentScope).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getCurrentQuery to build the search options', () => {
|
||||||
|
expect(service.getCurrentQuery).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getCurrentFilters to build the search options', () => {
|
||||||
|
expect(service.getCurrentFilters).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getCurrentFixedFilter to build the search options', () => {
|
||||||
|
expect(service.getCurrentFixedFilter).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
|
import { mergeMap, map, distinctUntilChanged } from 'rxjs/operators';
|
||||||
import { Injectable, InjectionToken } from '@angular/core';
|
import { Injectable, InjectionToken } from '@angular/core';
|
||||||
import { map } from 'rxjs/operators';
|
|
||||||
import { SearchFiltersState, SearchFilterState } from './search-filter.reducer';
|
import { SearchFiltersState, SearchFilterState } from './search-filter.reducer';
|
||||||
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||||
import {
|
import {
|
||||||
@@ -16,6 +16,11 @@ import {
|
|||||||
import { hasValue, isNotEmpty, } from '../../../shared/empty.util';
|
import { hasValue, isNotEmpty, } from '../../../shared/empty.util';
|
||||||
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||||
import { RouteService } from '../../../shared/services/route.service';
|
import { RouteService } from '../../../shared/services/route.service';
|
||||||
|
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||||
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { SearchOptions } from '../../search-options.model';
|
||||||
|
import { PaginatedSearchOptions } from '../../paginated-search-options.model';
|
||||||
|
import { SearchFixedFilterService } from './search-fixed-filter.service';
|
||||||
import { Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
|
|
||||||
const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
|
const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
|
||||||
@@ -29,8 +34,8 @@ export const FILTER_CONFIG: InjectionToken<SearchFilterConfig> = new InjectionTo
|
|||||||
export class SearchFilterService {
|
export class SearchFilterService {
|
||||||
|
|
||||||
constructor(private store: Store<SearchFiltersState>,
|
constructor(private store: Store<SearchFiltersState>,
|
||||||
private routeService: RouteService
|
private routeService: RouteService,
|
||||||
) {
|
private fixedFilterService: SearchFixedFilterService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,6 +57,138 @@ export class SearchFilterService {
|
|||||||
return this.routeService.hasQueryParam(paramName);
|
return this.routeService.hasQueryParam(paramName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the current active scope from the query parameters
|
||||||
|
* @returns {Observable<string>}
|
||||||
|
*/
|
||||||
|
getCurrentScope() {
|
||||||
|
return this.routeService.getQueryParameterValue('scope');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the current query from the query parameters
|
||||||
|
* @returns {Observable<string>}
|
||||||
|
*/
|
||||||
|
getCurrentQuery() {
|
||||||
|
return this.routeService.getQueryParameterValue('query');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the current pagination from query parameters 'page' and 'pageSize'
|
||||||
|
* and combine them with a given pagination
|
||||||
|
* @param pagination Pagination options to combine the query parameters with
|
||||||
|
* @returns {Observable<PaginationComponentOptions>}
|
||||||
|
*/
|
||||||
|
getCurrentPagination(pagination: any = {}): Observable<PaginationComponentOptions> {
|
||||||
|
const page$ = this.routeService.getQueryParameterValue('page');
|
||||||
|
const size$ = this.routeService.getQueryParameterValue('pageSize');
|
||||||
|
return observableCombineLatest(page$, size$).pipe(map(([page, size]) => {
|
||||||
|
return Object.assign(new PaginationComponentOptions(), pagination, {
|
||||||
|
currentPage: page || 1,
|
||||||
|
pageSize: size || pagination.pageSize
|
||||||
|
});
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the current sorting options from query parameters 'sortDirection' and 'sortField'
|
||||||
|
* and combine them with given sorting options
|
||||||
|
* @param {SortOptions} defaultSort Sorting options to combine the query parameters with
|
||||||
|
* @returns {Observable<SortOptions>}
|
||||||
|
*/
|
||||||
|
getCurrentSort(defaultSort: SortOptions): Observable<SortOptions> {
|
||||||
|
const sortDirection$ = this.routeService.getQueryParameterValue('sortDirection');
|
||||||
|
const sortField$ = this.routeService.getQueryParameterValue('sortField');
|
||||||
|
return observableCombineLatest(sortDirection$, sortField$).pipe(map(([sortDirection, sortField]) => {
|
||||||
|
const field = sortField || defaultSort.field;
|
||||||
|
const direction = SortDirection[sortDirection] || defaultSort.direction;
|
||||||
|
return new SortOptions(field, direction)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the current active filters from the query parameters
|
||||||
|
* @returns {Observable<Params>}
|
||||||
|
*/
|
||||||
|
getCurrentFilters() {
|
||||||
|
return this.routeService.getQueryParamsWithPrefix('f.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the current active fixed filter from the route parameters and return the query by filter name
|
||||||
|
* @returns {Observable<string>}
|
||||||
|
*/
|
||||||
|
getCurrentFixedFilter(): Observable<string> {
|
||||||
|
const filter: Observable<string> = this.routeService.getRouteParameterValue('filter');
|
||||||
|
return filter.pipe(mergeMap((f) => this.fixedFilterService.getQueryByFilterName(f)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the current view from the query parameters
|
||||||
|
* @returns {Observable<string>}
|
||||||
|
*/
|
||||||
|
getCurrentView() {
|
||||||
|
return this.routeService.getQueryParameterValue('view');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the current paginated search options using the getters from above
|
||||||
|
* and combining them with given defaults
|
||||||
|
* @param defaults Default paginated search options
|
||||||
|
* @returns {Observable<PaginatedSearchOptions>}
|
||||||
|
*/
|
||||||
|
getPaginatedSearchOptions(defaults: any = {}): Observable<PaginatedSearchOptions> {
|
||||||
|
return observableCombineLatest(
|
||||||
|
this.getCurrentPagination(defaults.pagination),
|
||||||
|
this.getCurrentSort(defaults.sort),
|
||||||
|
this.getCurrentView(),
|
||||||
|
this.getCurrentScope(),
|
||||||
|
this.getCurrentQuery(),
|
||||||
|
this.getCurrentFilters(),
|
||||||
|
this.getCurrentFixedFilter()).pipe(
|
||||||
|
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
|
||||||
|
map(([pagination, sort, view, scope, query, filters, fixedFilter]) => {
|
||||||
|
return Object.assign(new PaginatedSearchOptions(defaults),
|
||||||
|
{
|
||||||
|
pagination: pagination,
|
||||||
|
sort: sort,
|
||||||
|
view: view,
|
||||||
|
scope: scope || defaults.scope,
|
||||||
|
query: query,
|
||||||
|
filters: filters,
|
||||||
|
fixedFilter: fixedFilter
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the current search options (not paginated) using the getters from above
|
||||||
|
* and combining them with given defaults
|
||||||
|
* @param defaults Default search options
|
||||||
|
* @returns {Observable<SearchOptions>}
|
||||||
|
*/
|
||||||
|
getSearchOptions(defaults: any = {}): Observable<SearchOptions> {
|
||||||
|
return observableCombineLatest(
|
||||||
|
this.getCurrentView(),
|
||||||
|
this.getCurrentScope(),
|
||||||
|
this.getCurrentQuery(),
|
||||||
|
this.getCurrentFilters(),
|
||||||
|
this.getCurrentFixedFilter(),
|
||||||
|
(view, scope, query, filters, fixedFilter) => {
|
||||||
|
return Object.assign(new SearchOptions(defaults),
|
||||||
|
{
|
||||||
|
view: view,
|
||||||
|
scope: scope || defaults.scope,
|
||||||
|
query: query,
|
||||||
|
filters: filters,
|
||||||
|
fixedFilter: fixedFilter
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests the active filter values set for a given filter
|
* Requests the active filter values set for a given filter
|
||||||
* @param {SearchFilterConfig} filterConfig The configuration for which the filters are active
|
* @param {SearchFilterConfig} filterConfig The configuration for which the filters are active
|
||||||
|
@@ -0,0 +1,64 @@
|
|||||||
|
import { SearchFixedFilterService } from './search-fixed-filter.service';
|
||||||
|
import { ResponseCacheEntry } from '../../../core/cache/response-cache.reducer';
|
||||||
|
import { RouteService } from '../../../shared/services/route.service';
|
||||||
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
|
import { ResponseCacheService } from '../../../core/cache/response-cache.service';
|
||||||
|
import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
|
||||||
|
import { FilteredDiscoveryQueryResponse } from '../../../core/cache/response-cache.models';
|
||||||
|
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
describe('SearchFixedFilterService', () => {
|
||||||
|
let service: SearchFixedFilterService;
|
||||||
|
|
||||||
|
const filterQuery = 'filter:query';
|
||||||
|
|
||||||
|
const routeServiceStub = {} as RouteService;
|
||||||
|
const requestServiceStub = Object.assign({
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
configure: () => {},
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
generateRequestId: () => 'fake-id'
|
||||||
|
}) as RequestService;
|
||||||
|
const responseCacheStub = Object.assign(new ResponseCacheService(undefined), {
|
||||||
|
get: () => observableOf(Object.assign(new ResponseCacheEntry(), {
|
||||||
|
response: new FilteredDiscoveryQueryResponse(filterQuery, '200', new PageInfo())
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
const halServiceStub = Object.assign(new HALEndpointService(responseCacheStub, requestServiceStub, undefined), {
|
||||||
|
getEndpoint: () => observableOf('fake-url')
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new SearchFixedFilterService(routeServiceStub, requestServiceStub, responseCacheStub, halServiceStub);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getQueryByFilterName is called with a filterName', () => {
|
||||||
|
it('should return the filter query', () => {
|
||||||
|
service.getQueryByFilterName('filter').subscribe((query) => {
|
||||||
|
expect(query).toBe(filterQuery);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getQueryByFilterName is called without a filterName', () => {
|
||||||
|
it('should return undefined', () => {
|
||||||
|
service.getQueryByFilterName(undefined).subscribe((query) => {
|
||||||
|
expect(query).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getQueryByRelations is called', () => {
|
||||||
|
const relationType = 'isRelationOf';
|
||||||
|
const itemUUID = 'c5b277e6-2477-48bb-8993-356710c285f3';
|
||||||
|
|
||||||
|
it('should contain the relationType and itemUUID', () => {
|
||||||
|
const query = service.getQueryByRelations(relationType, itemUUID);
|
||||||
|
expect(query.length).toBeGreaterThan(relationType.length + itemUUID.length);
|
||||||
|
expect(query).toContain(relationType);
|
||||||
|
expect(query).toContain(itemUUID);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,78 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { flatMap, map } from 'rxjs/operators';
|
||||||
|
import { Observable , of as observableOf } from 'rxjs';
|
||||||
|
import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
|
||||||
|
import { GetRequest, RestRequest } from '../../../core/data/request.models';
|
||||||
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
|
import { ResponseParsingService } from '../../../core/data/parsing.service';
|
||||||
|
import { GenericConstructor } from '../../../core/shared/generic-constructor';
|
||||||
|
import { FilteredDiscoveryPageResponseParsingService } from '../../../core/data/filtered-discovery-page-response-parsing.service';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
import { configureRequest } from '../../../core/shared/operators';
|
||||||
|
import { RouteService } from '../../../shared/services/route.service';
|
||||||
|
import { FilteredDiscoveryQueryResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for performing actions on the filtered-discovery-pages REST endpoint
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class SearchFixedFilterService {
|
||||||
|
private queryByFilterPath = 'filtered-discovery-pages';
|
||||||
|
|
||||||
|
constructor(private routeService: RouteService,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
private halService: HALEndpointService) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filter query for a certain filter by name
|
||||||
|
* @param {string} filterName Name of the filter
|
||||||
|
* @returns {Observable<string>} Filter query
|
||||||
|
*/
|
||||||
|
getQueryByFilterName(filterName: string): Observable<string> {
|
||||||
|
if (hasValue(filterName)) {
|
||||||
|
const requestUuid = this.requestService.generateRequestId();
|
||||||
|
const requestObs = this.halService.getEndpoint(this.queryByFilterPath).pipe(
|
||||||
|
map((url: string) => {
|
||||||
|
url += ('/' + filterName);
|
||||||
|
const request = new GetRequest(requestUuid, url);
|
||||||
|
return Object.assign(request, {
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return FilteredDiscoveryPageResponseParsingService;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
configureRequest(this.requestService)
|
||||||
|
);
|
||||||
|
|
||||||
|
// get search results from response cache
|
||||||
|
const filterQuery: Observable<string> = this.requestService.getByUUID(requestUuid).pipe(
|
||||||
|
map((response: FilteredDiscoveryQueryResponse) =>
|
||||||
|
response.filterQuery
|
||||||
|
));
|
||||||
|
return filterQuery;
|
||||||
|
}
|
||||||
|
return observableOf(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the query for looking up items by relation type
|
||||||
|
* @param {string} relationType Relation type
|
||||||
|
* @param {string} itemUUID Item UUID
|
||||||
|
* @returns {string} Query
|
||||||
|
*/
|
||||||
|
getQueryByRelations(relationType: string, itemUUID: string): string {
|
||||||
|
return `query=relation.${relationType}:${itemUUID}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filter for a relation with the item's UUID
|
||||||
|
* @param relationType The type of relation e.g. 'isAuthorOfPublication'
|
||||||
|
* @param itemUUID The item's UUID
|
||||||
|
*/
|
||||||
|
getFilterByRelation(relationType: string, itemUUID: string): string {
|
||||||
|
return `f.${relationType}=${itemUUID}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -3,21 +3,25 @@ import { URLCombiner } from '../core/url-combiner/url-combiner';
|
|||||||
import 'core-js/library/fn/object/entries';
|
import 'core-js/library/fn/object/entries';
|
||||||
import { SearchFilter } from './search-filter.model';
|
import { SearchFilter } from './search-filter.model';
|
||||||
import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
|
import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
|
||||||
|
import { SetViewMode } from '../shared/view-mode';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This model class represents all parameters needed to request information about a certain search request
|
* This model class represents all parameters needed to request information about a certain search request
|
||||||
*/
|
*/
|
||||||
export class SearchOptions {
|
export class SearchOptions {
|
||||||
|
view?: SetViewMode = SetViewMode.List;
|
||||||
scope?: string;
|
scope?: string;
|
||||||
query?: string;
|
query?: string;
|
||||||
dsoType?: DSpaceObjectType;
|
dsoType?: DSpaceObjectType;
|
||||||
filters?: SearchFilter[];
|
filters?: any;
|
||||||
|
fixedFilter?: any;
|
||||||
|
|
||||||
constructor(options: {scope?: string, query?: string, dsoType?: DSpaceObjectType, filters?: SearchFilter[]}) {
|
constructor(options: {scope?: string, query?: string, dsoType?: DSpaceObjectType, filters?: SearchFilter[], fixedFilter?: any}) {
|
||||||
this.scope = options.scope;
|
this.scope = options.scope;
|
||||||
this.query = options.query;
|
this.query = options.query;
|
||||||
this.dsoType = options.dsoType;
|
this.dsoType = options.dsoType;
|
||||||
this.filters = options.filters;
|
this.filters = options.filters;
|
||||||
|
this.fixedFilter = options.fixedFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,7 +31,9 @@ export class SearchOptions {
|
|||||||
* @returns {string} URL with all search options and passed arguments as query parameters
|
* @returns {string} URL with all search options and passed arguments as query parameters
|
||||||
*/
|
*/
|
||||||
toRestUrl(url: string, args: string[] = []): string {
|
toRestUrl(url: string, args: string[] = []): string {
|
||||||
|
if (isNotEmpty(this.fixedFilter)) {
|
||||||
|
args.push(this.fixedFilter);
|
||||||
|
}
|
||||||
if (isNotEmpty(this.query)) {
|
if (isNotEmpty(this.query)) {
|
||||||
args.push(`query=${this.query}`);
|
args.push(`query=${this.query}`);
|
||||||
}
|
}
|
||||||
|
@@ -2,11 +2,14 @@ import { NgModule } from '@angular/core';
|
|||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
import { SearchPageComponent } from './search-page.component';
|
import { SearchPageComponent } from './search-page.component';
|
||||||
|
import { FilteredSearchPageComponent } from './filtered-search-page.component';
|
||||||
|
import { FilteredSearchPageGuard } from './filtered-search-page.guard';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{ path: '', component: SearchPageComponent, data: { title: 'search.title' } }
|
{ path: '', component: SearchPageComponent, data: { title: 'search.title' } },
|
||||||
|
{ path: ':filter', component: FilteredSearchPageComponent, canActivate: [FilteredSearchPageGuard], data: { title: 'search.' }}
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@@ -1,16 +1,16 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="search-page row">
|
<div class="search-page row">
|
||||||
<ds-search-sidebar *ngIf="!(isXsOrSm$ | async)" class="col-3 sidebar-md-sticky"
|
<ds-search-sidebar *ngIf="!(isXsOrSm$ | async)" class="col-{{sideBarWidth}} sidebar-md-sticky"
|
||||||
id="search-sidebar"
|
id="search-sidebar"
|
||||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"></ds-search-sidebar>
|
[resultCount]="(resultsRD$ | async)?.payload.totalElements"></ds-search-sidebar>
|
||||||
<div class="col-12 col-md-9">
|
<div class="col-12 col-md-{{12 - sideBarWidth}}">
|
||||||
<ds-search-form id="search-form"
|
<ds-search-form *ngIf="searchEnabled" id="search-form"
|
||||||
[query]="(searchOptions$ | async)?.query"
|
[query]="(searchOptions$ | async)?.query"
|
||||||
[scope]="(searchOptions$ | async)?.scope"
|
[scope]="(searchOptions$ | async)?.scope"
|
||||||
[currentUrl]="getSearchLink()"
|
[currentUrl]="getSearchLink()"
|
||||||
[scopes]="(scopeListRD$ | async)">
|
[scopes]="(scopeListRD$ | async)">
|
||||||
</ds-search-form>
|
</ds-search-form>
|
||||||
<ds-search-labels></ds-search-labels>
|
<ds-search-labels *ngIf="searchEnabled"></ds-search-labels>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div id="search-body"
|
<div id="search-body"
|
||||||
class="row-offcanvas row-offcanvas-left"
|
class="row-offcanvas row-offcanvas-left"
|
||||||
@@ -31,7 +31,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ds-search-results [searchResults]="resultsRD$ | async"
|
<ds-search-results [searchResults]="resultsRD$ | async"
|
||||||
[searchConfig]="searchOptions$ | async"></ds-search-results>
|
[searchConfig]="searchOptions$ | async"
|
||||||
|
[fixedFilter]="fixedFilter$ | async"
|
||||||
|
[disableHeader]="!searchEnabled"></ds-search-results>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -20,52 +20,60 @@ import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
|||||||
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
|
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
|
||||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { RouteService } from '../shared/services/route.service';
|
||||||
|
|
||||||
describe('SearchPageComponent', () => {
|
let comp: SearchPageComponent;
|
||||||
let comp: SearchPageComponent;
|
let fixture: ComponentFixture<SearchPageComponent>;
|
||||||
let fixture: ComponentFixture<SearchPageComponent>;
|
let searchServiceObject: SearchService;
|
||||||
let searchServiceObject: SearchService;
|
const store: Store<SearchPageComponent> = jasmine.createSpyObj('store', {
|
||||||
const store: Store<SearchPageComponent> = jasmine.createSpyObj('store', {
|
|
||||||
/* tslint:disable:no-empty */
|
/* tslint:disable:no-empty */
|
||||||
dispatch: {},
|
dispatch: {},
|
||||||
/* tslint:enable:no-empty */
|
/* tslint:enable:no-empty */
|
||||||
select: observableOf(true)
|
select: observableOf(true)
|
||||||
});
|
});
|
||||||
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
||||||
pagination.id = 'search-results-pagination';
|
pagination.id = 'search-results-pagination';
|
||||||
pagination.currentPage = 1;
|
pagination.currentPage = 1;
|
||||||
pagination.pageSize = 10;
|
pagination.pageSize = 10;
|
||||||
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
|
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
|
||||||
const mockResults = observableOf(new RemoteData(false, false, true, null, ['test', 'data']));
|
const mockResults = observableOf(new RemoteData(false, false, true, null, ['test', 'data']));
|
||||||
const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
||||||
search: mockResults,
|
search: mockResults,
|
||||||
getSearchLink: '/search',
|
getSearchLink: '/search',
|
||||||
getScopes: observableOf(['test-scope'])
|
getScopes: observableOf(['test-scope'])
|
||||||
});
|
});
|
||||||
const queryParam = 'test query';
|
const queryParam = 'test query';
|
||||||
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
||||||
const paginatedSearchOptions = {
|
const fixedFilter = 'fixed filter';
|
||||||
|
const paginatedSearchOptions = {
|
||||||
query: queryParam,
|
query: queryParam,
|
||||||
scope: scopeParam,
|
scope: scopeParam,
|
||||||
|
fixedFilter: fixedFilter,
|
||||||
pagination,
|
pagination,
|
||||||
sort
|
sort
|
||||||
};
|
};
|
||||||
const activatedRouteStub = {
|
const activatedRouteStub = {
|
||||||
queryParams: observableOf({
|
queryParams: observableOf({
|
||||||
query: queryParam,
|
query: queryParam,
|
||||||
scope: scopeParam
|
scope: scopeParam
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
const sidebarService = {
|
const sidebarService = {
|
||||||
isCollapsed: observableOf(true),
|
isCollapsed: observableOf(true),
|
||||||
collapse: () => this.isCollapsed = observableOf(true),
|
collapse: () => this.isCollapsed = observableOf(true),
|
||||||
expand: () => this.isCollapsed = observableOf(false)
|
expand: () => this.isCollapsed = observableOf(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async(() => {
|
const routeServiceStub = {
|
||||||
|
getRouteParameterValue: () => {
|
||||||
|
return observableOf('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function configureSearchComponentTestingModule(compType) {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, NgbCollapseModule.forRoot()],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, NgbCollapseModule.forRoot()],
|
||||||
declarations: [SearchPageComponent],
|
declarations: [compType],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: SearchService, useValue: searchServiceStub },
|
{ provide: SearchService, useValue: searchServiceStub },
|
||||||
{
|
{
|
||||||
@@ -91,20 +99,35 @@ describe('SearchPageComponent', () => {
|
|||||||
{
|
{
|
||||||
provide: SearchFilterService,
|
provide: SearchFilterService,
|
||||||
useValue: {}
|
useValue: {}
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
provide: SearchConfigurationService,
|
provide: SearchConfigurationService,
|
||||||
useValue: {
|
useValue: {
|
||||||
paginatedSearchOptions: hot('a', {
|
paginatedSearchOptions: hot('a', {
|
||||||
a: paginatedSearchOptions
|
a: paginatedSearchOptions
|
||||||
}),
|
}),
|
||||||
getCurrentScope: (a) => observableOf('test-id')
|
getCurrentScope: (a) => observableOf('test-id'),
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
updateFixedFilter: (newFilter) => {
|
||||||
|
}
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: RouteService,
|
||||||
|
useValue: routeServiceStub
|
||||||
|
}
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).overrideComponent(SearchPageComponent, {
|
}).overrideComponent(compType, {
|
||||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SearchPageComponent', () => {
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
configureSearchComponentTestingModule(SearchPageComponent);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -177,4 +200,4 @@ describe('SearchPageComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
import { Observable , Subscription , BehaviorSubject } from 'rxjs';
|
||||||
import { switchMap, } from 'rxjs/operators';
|
import { switchMap, } from 'rxjs/operators';
|
||||||
import { PaginatedList } from '../core/data/paginated-list';
|
import { PaginatedList } from '../core/data/paginated-list';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
@@ -11,15 +11,10 @@ import { SearchFilterService } from './search-filters/search-filter/search-filte
|
|||||||
import { SearchResult } from './search-result.model';
|
import { SearchResult } from './search-result.model';
|
||||||
import { SearchService } from './search-service/search.service';
|
import { SearchService } from './search-service/search.service';
|
||||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||||
import { hasValue } from '../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||||
|
import { RouteService } from '../shared/services/route.service';
|
||||||
/**
|
|
||||||
* This component renders a simple item page.
|
|
||||||
* The route parameter 'id' is used to request the item it represents.
|
|
||||||
* All fields of the item that should be displayed, are defined in its template.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-page',
|
selector: 'ds-search-page',
|
||||||
@@ -31,6 +26,7 @@ import { getSucceededRemoteData } from '../core/shared/operators';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This component represents the whole search page
|
* This component represents the whole search page
|
||||||
|
* It renders search results depending on the current search options
|
||||||
*/
|
*/
|
||||||
export class SearchPageComponent implements OnInit {
|
export class SearchPageComponent implements OnInit {
|
||||||
|
|
||||||
@@ -59,11 +55,30 @@ export class SearchPageComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
sub: Subscription;
|
sub: Subscription;
|
||||||
|
|
||||||
constructor(private service: SearchService,
|
/**
|
||||||
private sidebarService: SearchSidebarService,
|
* Whether or not the search bar should be visible
|
||||||
private windowService: HostWindowService,
|
*/
|
||||||
private filterService: SearchFilterService,
|
@Input()
|
||||||
private searchConfigService: SearchConfigurationService) {
|
searchEnabled = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The width of the sidebar (bootstrap columns)
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
sideBarWidth = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently applied filter (determines title of search)
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
fixedFilter$: Observable<string>;
|
||||||
|
|
||||||
|
constructor(protected service: SearchService,
|
||||||
|
protected sidebarService: SearchSidebarService,
|
||||||
|
protected windowService: HostWindowService,
|
||||||
|
protected filterService: SearchFilterService,
|
||||||
|
protected searchConfigService: SearchConfigurationService,
|
||||||
|
protected routeService: RouteService) {
|
||||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +90,7 @@ export class SearchPageComponent implements OnInit {
|
|||||||
* If something changes, update the list of scopes for the dropdown
|
* If something changes, update the list of scopes for the dropdown
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
|
this.searchOptions$ = this.getSearchOptions();
|
||||||
this.sub = this.searchOptions$.pipe(
|
this.sub = this.searchOptions$.pipe(
|
||||||
switchMap((options) => this.service.search(options).pipe(getSucceededRemoteData())))
|
switchMap((options) => this.service.search(options).pipe(getSucceededRemoteData())))
|
||||||
.subscribe((results) => {
|
.subscribe((results) => {
|
||||||
@@ -84,6 +99,17 @@ export class SearchPageComponent implements OnInit {
|
|||||||
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
||||||
switchMap((scopeId) => this.service.getScopes(scopeId))
|
switchMap((scopeId) => this.service.getScopes(scopeId))
|
||||||
);
|
);
|
||||||
|
if (!isNotEmpty(this.fixedFilter$)) {
|
||||||
|
this.fixedFilter$ = this.routeService.getRouteParameterValue('filter');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current paginated search options
|
||||||
|
* @returns {Observable<PaginatedSearchOptions>}
|
||||||
|
*/
|
||||||
|
protected getSearchOptions(): Observable<PaginatedSearchOptions> {
|
||||||
|
return this.searchConfigService.paginatedSearchOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -21,6 +21,9 @@ import { SearchFiltersComponent } from './search-filters/search-filters.componen
|
|||||||
import { SearchFilterComponent } from './search-filters/search-filter/search-filter.component';
|
import { SearchFilterComponent } from './search-filters/search-filter/search-filter.component';
|
||||||
import { SearchFacetFilterComponent } from './search-filters/search-filter/search-facet-filter/search-facet-filter.component';
|
import { SearchFacetFilterComponent } from './search-filters/search-filter/search-facet-filter/search-facet-filter.component';
|
||||||
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
|
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
|
||||||
|
import { FilteredSearchPageComponent } from './filtered-search-page.component';
|
||||||
|
import { SearchFixedFilterService } from './search-filters/search-filter/search-fixed-filter.service';
|
||||||
|
import { FilteredSearchPageGuard } from './filtered-search-page.guard';
|
||||||
import { SearchLabelsComponent } from './search-labels/search-labels.component';
|
import { SearchLabelsComponent } from './search-labels/search-labels.component';
|
||||||
import { SearchRangeFilterComponent } from './search-filters/search-filter/search-range-filter/search-range-filter.component';
|
import { SearchRangeFilterComponent } from './search-filters/search-filter/search-range-filter/search-range-filter.component';
|
||||||
import { SearchTextFilterComponent } from './search-filters/search-filter/search-text-filter/search-text-filter.component';
|
import { SearchTextFilterComponent } from './search-filters/search-filter/search-text-filter/search-text-filter.component';
|
||||||
@@ -43,6 +46,7 @@ const effects = [
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
SearchPageComponent,
|
SearchPageComponent,
|
||||||
|
FilteredSearchPageComponent,
|
||||||
SearchResultsComponent,
|
SearchResultsComponent,
|
||||||
SearchSidebarComponent,
|
SearchSidebarComponent,
|
||||||
SearchSettingsComponent,
|
SearchSettingsComponent,
|
||||||
@@ -68,6 +72,9 @@ const effects = [
|
|||||||
SearchService,
|
SearchService,
|
||||||
SearchSidebarService,
|
SearchSidebarService,
|
||||||
SearchFilterService,
|
SearchFilterService,
|
||||||
|
SearchFixedFilterService,
|
||||||
|
FilteredSearchPageGuard,
|
||||||
|
SearchFilterService,
|
||||||
SearchConfigurationService
|
SearchConfigurationService
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
@@ -82,6 +89,9 @@ const effects = [
|
|||||||
SearchTextFilterComponent,
|
SearchTextFilterComponent,
|
||||||
SearchHierarchyFilterComponent,
|
SearchHierarchyFilterComponent,
|
||||||
SearchBooleanFilterComponent,
|
SearchBooleanFilterComponent,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
FilteredSearchPageComponent,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<h2>{{ 'search.results.head' | translate }}</h2>
|
<h2 *ngIf="!disableHeader">{{ getTitleKey() | translate }}</h2>
|
||||||
<div *ngIf="searchResults?.hasSucceeded && !searchResults?.isLoading && searchResults?.payload?.page.length > 0" @fadeIn>
|
<div *ngIf="searchResults?.hasSucceeded && !searchResults?.isLoading && searchResults?.payload?.page.length > 0" @fadeIn>
|
||||||
<ds-viewable-collection
|
<ds-viewable-collection
|
||||||
[config]="searchConfig.pagination"
|
[config]="searchConfig.pagination"
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { ComponentFixture, TestBed, async, tick, fakeAsync } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, async, tick, fakeAsync } from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ResourceType } from '../../core/shared/resource-type';
|
import { ResourceType } from '../../core/shared/resource-type';
|
||||||
import { Community } from '../../core/shared/community.model';
|
import { Community } from '../../core/shared/community.model';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
@@ -11,6 +11,8 @@ import { QueryParamsDirectiveStub } from '../../shared/testing/query-params-dire
|
|||||||
describe('SearchResultsComponent', () => {
|
describe('SearchResultsComponent', () => {
|
||||||
let comp: SearchResultsComponent;
|
let comp: SearchResultsComponent;
|
||||||
let fixture: ComponentFixture<SearchResultsComponent>;
|
let fixture: ComponentFixture<SearchResultsComponent>;
|
||||||
|
let heading: DebugElement;
|
||||||
|
let title: DebugElement;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -24,7 +26,9 @@ describe('SearchResultsComponent', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(SearchResultsComponent);
|
fixture = TestBed.createComponent(SearchResultsComponent);
|
||||||
comp = fixture.componentInstance; // SearchResultsComponent test instance
|
comp = fixture.componentInstance; // SearchFormComponent test instance
|
||||||
|
heading = fixture.debugElement.query(By.css('heading'));
|
||||||
|
title = fixture.debugElement.query(By.css('h2'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display results when results are not empty', () => {
|
it('should display results when results are not empty', () => {
|
||||||
|
@@ -2,11 +2,12 @@ import { Component, Input } from '@angular/core';
|
|||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
||||||
|
import { SetViewMode } from '../../shared/view-mode';
|
||||||
import { SearchOptions } from '../search-options.model';
|
import { SearchOptions } from '../search-options.model';
|
||||||
import { SearchResult } from '../search-result.model';
|
import { SearchResult } from '../search-result.model';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list';
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-results',
|
selector: 'ds-search-results',
|
||||||
@@ -30,11 +31,23 @@ export class SearchResultsComponent {
|
|||||||
* The current configuration of the search
|
* The current configuration of the search
|
||||||
*/
|
*/
|
||||||
@Input() searchConfig: SearchOptions;
|
@Input() searchConfig: SearchOptions;
|
||||||
|
@Input() sortConfig: SortOptions;
|
||||||
|
@Input() viewMode: SetViewMode;
|
||||||
|
@Input() fixedFilter: string;
|
||||||
|
@Input() disableHeader = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current view mode for the search results
|
* Get the i18n key for the title depending on the fixed filter
|
||||||
|
* Defaults to 'search.results.head' if there's no fixed filter found
|
||||||
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
@Input() viewMode: ViewMode;
|
getTitleKey() {
|
||||||
|
if (isNotEmpty(this.fixedFilter)) {
|
||||||
|
return 'search.' + this.fixedFilter + '.results.head'
|
||||||
|
} else {
|
||||||
|
return 'search.results.head';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to change the given string by surrounding it by quotes if not already present.
|
* Method to change the given string by surrounding it by quotes if not already present.
|
||||||
|
@@ -23,17 +23,21 @@ describe('SearchConfigurationService', () => {
|
|||||||
|
|
||||||
const backendFilters = [new SearchFilter('f.author', ['another value']), new SearchFilter('f.date', ['[2013 TO 2018]'])];
|
const backendFilters = [new SearchFilter('f.author', ['another value']), new SearchFilter('f.date', ['[2013 TO 2018]'])];
|
||||||
|
|
||||||
const spy = jasmine.createSpyObj('RouteService', {
|
const routeService = jasmine.createSpyObj('RouteService', {
|
||||||
getQueryParameterValue: observableOf(value1),
|
getQueryParameterValue: observableOf(value1),
|
||||||
getQueryParamsWithPrefix: observableOf(prefixFilter)
|
getQueryParamsWithPrefix: observableOf(prefixFilter),
|
||||||
|
getRouteParameterValue: observableOf('')
|
||||||
|
});
|
||||||
|
|
||||||
|
const fixedFilterService = jasmine.createSpyObj('SearchFixedFilterService', {
|
||||||
|
getQueryByFilterName: observableOf(''),
|
||||||
});
|
});
|
||||||
|
|
||||||
const activatedRoute: any = new ActivatedRouteStub();
|
const activatedRoute: any = new ActivatedRouteStub();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service = new SearchConfigurationService(spy, activatedRoute);
|
service = new SearchConfigurationService(routeService, fixedFilterService, activatedRoute);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the scope is called', () => {
|
describe('when the scope is called', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service.getCurrentScope('');
|
service.getCurrentScope('');
|
||||||
@@ -143,4 +147,29 @@ describe('SearchConfigurationService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when getCurrentFixedFilter is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.getCurrentFixedFilter();
|
||||||
|
});
|
||||||
|
it('should call getRouteParameterValue on the routeService with parameter name \'filter\'', () => {
|
||||||
|
expect((service as any).routeService.getRouteParameterValue).toHaveBeenCalledWith('filter');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when updateFixedFilter is called', () => {
|
||||||
|
const filter = 'filter';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service.updateFixedFilter(filter);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the paginated search options with the correct fixed filter', () => {
|
||||||
|
expect(service.paginatedSearchOptions.getValue().fixedFilter).toEqual(filter);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the search options with the correct fixed filter', () => {
|
||||||
|
expect(service.searchOptions.getValue().fixedFilter).toEqual(filter);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -6,7 +6,7 @@ import {
|
|||||||
of as observableOf,
|
of as observableOf,
|
||||||
Subscription
|
Subscription
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { filter, map } from 'rxjs/operators';
|
import { filter, flatMap, map } from 'rxjs/operators';
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
import { SearchOptions } from '../search-options.model';
|
import { SearchOptions } from '../search-options.model';
|
||||||
@@ -14,11 +14,12 @@ import { ActivatedRoute, Params } from '@angular/router';
|
|||||||
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
||||||
import { Injectable, OnDestroy } from '@angular/core';
|
import { Injectable, OnDestroy } from '@angular/core';
|
||||||
import { RouteService } from '../../shared/services/route.service';
|
import { RouteService } from '../../shared/services/route.service';
|
||||||
import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { getSucceededRemoteData } from '../../core/shared/operators';
|
import { getSucceededRemoteData } from '../../core/shared/operators';
|
||||||
import { SearchFilter } from '../search-filter.model';
|
import { SearchFilter } from '../search-filter.model';
|
||||||
import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model';
|
import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model';
|
||||||
|
import { SearchFixedFilterService } from '../search-filters/search-filter/search-fixed-filter.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service that performs all actions that have to do with the current search configuration
|
* Service that performs all actions that have to do with the current search configuration
|
||||||
@@ -75,6 +76,7 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
* @param {ActivatedRoute} route
|
* @param {ActivatedRoute} route
|
||||||
*/
|
*/
|
||||||
constructor(private routeService: RouteService,
|
constructor(private routeService: RouteService,
|
||||||
|
private fixedFilterService: SearchFixedFilterService,
|
||||||
private route: ActivatedRoute) {
|
private route: ActivatedRoute) {
|
||||||
this.defaults
|
this.defaults
|
||||||
.pipe(getSucceededRemoteData())
|
.pipe(getSucceededRemoteData())
|
||||||
@@ -174,6 +176,15 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Observable<string>} Emits the current fixed filter as a string
|
||||||
|
*/
|
||||||
|
getCurrentFixedFilter(): Observable<string> {
|
||||||
|
return this.routeService.getRouteParameterValue('filter').pipe(
|
||||||
|
flatMap((f) => this.fixedFilterService.getQueryByFilterName(f))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {Observable<Params>} Emits the current active filters with their values as they are displayed in the frontend URL
|
* @returns {Observable<Params>} Emits the current active filters with their values as they are displayed in the frontend URL
|
||||||
*/
|
*/
|
||||||
@@ -191,7 +202,8 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
this.getScopePart(defaults.scope),
|
this.getScopePart(defaults.scope),
|
||||||
this.getQueryPart(defaults.query),
|
this.getQueryPart(defaults.query),
|
||||||
this.getDSOTypePart(),
|
this.getDSOTypePart(),
|
||||||
this.getFiltersPart()
|
this.getFiltersPart(),
|
||||||
|
this.getFixedFilterPart()
|
||||||
).subscribe((update) => {
|
).subscribe((update) => {
|
||||||
const currentValue: SearchOptions = this.searchOptions.getValue();
|
const currentValue: SearchOptions = this.searchOptions.getValue();
|
||||||
const updatedValue: SearchOptions = Object.assign(currentValue, update);
|
const updatedValue: SearchOptions = Object.assign(currentValue, update);
|
||||||
@@ -211,7 +223,8 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
this.getScopePart(defaults.scope),
|
this.getScopePart(defaults.scope),
|
||||||
this.getQueryPart(defaults.query),
|
this.getQueryPart(defaults.query),
|
||||||
this.getDSOTypePart(),
|
this.getDSOTypePart(),
|
||||||
this.getFiltersPart()
|
this.getFiltersPart(),
|
||||||
|
this.getFixedFilterPart()
|
||||||
).subscribe((update) => {
|
).subscribe((update) => {
|
||||||
const currentValue: PaginatedSearchOptions = this.paginatedSearchOptions.getValue();
|
const currentValue: PaginatedSearchOptions = this.paginatedSearchOptions.getValue();
|
||||||
const updatedValue: PaginatedSearchOptions = Object.assign(currentValue, update);
|
const updatedValue: PaginatedSearchOptions = Object.assign(currentValue, update);
|
||||||
@@ -297,4 +310,30 @@ export class SearchConfigurationService implements OnDestroy {
|
|||||||
return { filters }
|
return { filters }
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Observable<string>} Emits the current fixed filter as a partial SearchOptions object
|
||||||
|
*/
|
||||||
|
private getFixedFilterPart(): Observable<any> {
|
||||||
|
return this.getCurrentFixedFilter().pipe(
|
||||||
|
isNotEmptyOperator(),
|
||||||
|
map((fixedFilter) => {
|
||||||
|
return { fixedFilter }
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the fixed filter in paginated and non-paginated search options with a given value
|
||||||
|
* @param {string} fixedFilter
|
||||||
|
*/
|
||||||
|
public updateFixedFilter(fixedFilter: string) {
|
||||||
|
const currentPaginatedValue: PaginatedSearchOptions = this.paginatedSearchOptions.getValue();
|
||||||
|
const updatedPaginatedValue: PaginatedSearchOptions = Object.assign(currentPaginatedValue, { fixedFilter: fixedFilter });
|
||||||
|
this.paginatedSearchOptions.next(updatedPaginatedValue);
|
||||||
|
|
||||||
|
const currentValue: SearchOptions = this.searchOptions.getValue();
|
||||||
|
const updatedValue: SearchOptions = Object.assign(currentValue, { fixedFilter: fixedFilter });
|
||||||
|
this.searchOptions.next(updatedValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,9 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
import { SearchService } from './search.service';
|
import { SearchService } from './search.service';
|
||||||
|
import { ItemDataService } from './../../core/data/item-data.service';
|
||||||
|
import { SetViewMode } from '../../shared/view-mode';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
||||||
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
|
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
|
||||||
import { RequestService } from '../../core/data/request.service';
|
import { RequestService } from '../../core/data/request.service';
|
||||||
@@ -64,7 +67,7 @@ describe('SearchService', () => {
|
|||||||
|
|
||||||
it('should return list view mode', () => {
|
it('should return list view mode', () => {
|
||||||
searchService.getViewMode().subscribe((viewMode) => {
|
searchService.getViewMode().subscribe((viewMode) => {
|
||||||
expect(viewMode).toBe(ViewMode.List);
|
expect(viewMode).toBe(SetViewMode.List);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -122,33 +125,33 @@ describe('SearchService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => {
|
it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => {
|
||||||
searchService.setViewMode(ViewMode.List);
|
searchService.setViewMode(SetViewMode.List);
|
||||||
expect(router.navigate).toHaveBeenCalledWith(['/search'], {
|
expect(router.navigate).toHaveBeenCalledWith(['/search'], {
|
||||||
queryParams: { view: ViewMode.List },
|
queryParams: { view: SetViewMode.List },
|
||||||
queryParamsHandling: 'merge'
|
queryParamsHandling: 'merge'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call the navigate method on the Router with view mode grid parameter as a parameter when setViewMode is called', () => {
|
it('should call the navigate method on the Router with view mode grid parameter as a parameter when setViewMode is called', () => {
|
||||||
searchService.setViewMode(ViewMode.Grid);
|
searchService.setViewMode(SetViewMode.Grid);
|
||||||
expect(router.navigate).toHaveBeenCalledWith(['/search'], {
|
expect(router.navigate).toHaveBeenCalledWith(['/search'], {
|
||||||
queryParams: { view: ViewMode.Grid },
|
queryParams: { view: SetViewMode.Grid },
|
||||||
queryParamsHandling: 'merge'
|
queryParamsHandling: 'merge'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return ViewMode.List when the viewMode is set to ViewMode.List in the ActivatedRoute', () => {
|
it('should return ViewMode.List when the viewMode is set to ViewMode.List in the ActivatedRoute', () => {
|
||||||
let viewMode = ViewMode.Grid;
|
let viewMode = SetViewMode.Grid;
|
||||||
route.testParams = { view: ViewMode.List };
|
route.testParams = { view: SetViewMode.List };
|
||||||
searchService.getViewMode().subscribe((mode) => viewMode = mode);
|
searchService.getViewMode().subscribe((mode) => viewMode = mode);
|
||||||
expect(viewMode).toEqual(ViewMode.List);
|
expect(viewMode).toEqual(SetViewMode.List);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return ViewMode.Grid when the viewMode is set to ViewMode.Grid in the ActivatedRoute', () => {
|
it('should return ViewMode.Grid when the viewMode is set to ViewMode.Grid in the ActivatedRoute', () => {
|
||||||
let viewMode = ViewMode.List;
|
let viewMode = SetViewMode.List;
|
||||||
route.testParams = { view: ViewMode.Grid };
|
route.testParams = { view: SetViewMode.Grid };
|
||||||
searchService.getViewMode().subscribe((mode) => viewMode = mode);
|
searchService.getViewMode().subscribe((mode) => viewMode = mode);
|
||||||
expect(viewMode).toEqual(ViewMode.Grid);
|
expect(viewMode).toEqual(SetViewMode.Grid);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when search is called', () => {
|
describe('when search is called', () => {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user