mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-13 04:53:06 +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.
|
||||
rest: {
|
||||
ssl: true,
|
||||
host: 'dspace7.4science.it',
|
||||
host: 'dspace7-entities.atmire.com',
|
||||
port: 443,
|
||||
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||
nameSpace: '/dspace-spring-rest/api'
|
||||
nameSpace: '/rest/api'
|
||||
},
|
||||
// Caching settings
|
||||
cache: {
|
||||
|
@@ -12,8 +12,8 @@ describe('protractor App', () => {
|
||||
expect<any>(page.getPageTitleText()).toEqual('DSpace Angular :: Home');
|
||||
});
|
||||
|
||||
it('should display header "Welcome to DSpace"', () => {
|
||||
it('should contain a news section', () => {
|
||||
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();
|
||||
}
|
||||
|
||||
getFirstPText() {
|
||||
return element(by.xpath('//p[1]')).getText();
|
||||
}
|
||||
|
||||
getFirstHeaderText() {
|
||||
return element(by.xpath('//h1[1]')).getText();
|
||||
getHomePageNewsText() {
|
||||
return element(by.xpath('//ds-home-news')).getText();
|
||||
}
|
||||
}
|
||||
|
@@ -91,12 +91,14 @@
|
||||
},
|
||||
"item": {
|
||||
"page": {
|
||||
"author": "Author",
|
||||
"author": "Authors",
|
||||
"abstract": "Abstract",
|
||||
"date": "Date",
|
||||
"uri": "URI",
|
||||
"files": "Files",
|
||||
"collections": "Collections",
|
||||
"subject": "Keywords",
|
||||
"citation": "Citation",
|
||||
"filesection": {
|
||||
"download": "Download",
|
||||
"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": {
|
||||
"browse": {
|
||||
"header": "All of DSpace"
|
||||
@@ -319,6 +399,24 @@
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"description": "",
|
||||
"form": {
|
||||
@@ -355,7 +453,8 @@
|
||||
"f.dateIssued.min": "Start date",
|
||||
"f.dateIssued.max": "End date",
|
||||
"f.subject": "Subject",
|
||||
"f.has_content_in_original_bundle": "Has files"
|
||||
"f.has_content_in_original_bundle": "Has files",
|
||||
"f.entityType": "Item Type"
|
||||
},
|
||||
"filter": {
|
||||
"show-more": "Show more",
|
||||
@@ -383,6 +482,10 @@
|
||||
},
|
||||
"has_content_in_original_bundle": {
|
||||
"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 { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||
import { CollectionDataService } from '../core/data/collection-data.service';
|
||||
import { ItemDataService } from '../core/data/item-data.service';
|
||||
import { PaginatedList } from '../core/data/paginated-list';
|
||||
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 { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||
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 { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
||||
import { toDSpaceObjectListRD } from '../core/shared/operators';
|
||||
@@ -42,7 +43,7 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(
|
||||
private collectionDataService: CollectionDataService,
|
||||
private searchService: SearchService,
|
||||
private itemDataService: ItemDataService,
|
||||
private metadata: MetadataService,
|
||||
private route: ActivatedRoute
|
||||
) {
|
||||
@@ -56,7 +57,7 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
||||
ngOnInit(): void {
|
||||
this.collectionRD$ = this.route.data.pipe(
|
||||
map((data) => data.collection),
|
||||
tap((data) => this.collectionId = data.payload.id)
|
||||
first()
|
||||
);
|
||||
this.logoRD$ = this.collectionRD$.pipe(
|
||||
map((rd: RemoteData<Collection>) => rd.payload),
|
||||
@@ -68,26 +69,33 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
||||
this.metadata.processRemoteData(this.collectionRD$);
|
||||
const page = +params.page || this.paginationConfig.currentPage;
|
||||
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
|
||||
const sortDirection = +params.page || this.sortConfig.direction;
|
||||
const pagination = Object.assign({},
|
||||
this.paginationConfig,
|
||||
{ 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({
|
||||
pagination: pagination,
|
||||
sort: this.sortConfig
|
||||
sort: sort
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
updatePage(searchOptions) {
|
||||
this.itemRD$ = this.searchService.search(
|
||||
new PaginatedSearchOptions({
|
||||
scope: this.collectionId,
|
||||
pagination: searchOptions.pagination,
|
||||
sort: searchOptions.sort,
|
||||
dsoType: DSpaceObjectType.ITEM
|
||||
})).pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>;
|
||||
this.itemRD$ = this.itemDataService.findAll({
|
||||
scopeID: this.collectionId,
|
||||
currentPage: searchOptions.pagination.currentPage,
|
||||
elementsPerPage: searchOptions.pagination.pageSize,
|
||||
sort: searchOptions.sort
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
@@ -7,15 +7,14 @@ import { CollectionPageComponent } from './collection-page.component';
|
||||
import { CollectionPageRoutingModule } from './collection-page-routing.module';
|
||||
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.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 { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||
import { SearchService } from '../+search-page/search-service/search.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
SearchPageModule,
|
||||
CollectionPageRoutingModule
|
||||
],
|
||||
declarations: [
|
||||
@@ -24,6 +23,9 @@ import { DeleteCollectionPageComponent } from './delete-collection-page/delete-c
|
||||
EditCollectionPageComponent,
|
||||
DeleteCollectionPageComponent,
|
||||
CollectionFormComponent
|
||||
],
|
||||
providers: [
|
||||
SearchService
|
||||
]
|
||||
})
|
||||
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>
|
||||
<div #content class="simple-view-element-body">
|
||||
<ng-content></ng-content>
|
||||
|
@@ -1,18 +1,41 @@
|
||||
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Component, DebugElement } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MetadataFieldWrapperComponent } from './metadata-field-wrapper.component';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
@Component({
|
||||
selector: 'ds-component-with-content',
|
||||
selector: 'ds-component-without-content',
|
||||
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
|
||||
' <div class="my-content">\n' +
|
||||
' <span></span>\n' +
|
||||
' </div>\n' +
|
||||
'</ds-metadata-field-wrapper>'
|
||||
})
|
||||
class ContentComponent {}
|
||||
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', () => {
|
||||
let component: MetadataFieldWrapperComponent;
|
||||
@@ -20,7 +43,7 @@ describe('MetadataFieldWrapperComponent', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [MetadataFieldWrapperComponent, ContentComponent]
|
||||
declarations: [MetadataFieldWrapperComponent, NoContentComponent, SpanContentComponent, TextContentComponent, ImgContentComponent]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
@@ -30,23 +53,21 @@ describe('MetadataFieldWrapperComponent', () => {
|
||||
});
|
||||
|
||||
const wrapperSelector = '.simple-view-element';
|
||||
const labelSelector = '.simple-view-element-header';
|
||||
const contentSelector = '.my-content';
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not show the component when there is no content', () => {
|
||||
component.label = 'test label';
|
||||
fixture.detectChanges();
|
||||
const parentNative = fixture.nativeElement;
|
||||
const parentFixture = TestBed.createComponent(NoContentComponent);
|
||||
parentFixture.detectChanges();
|
||||
const parentNative = parentFixture.nativeElement;
|
||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
||||
expect(nativeWrapper.classList.contains('d-none')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not show the component when there is DOM content but no text', () => {
|
||||
const parentFixture = TestBed.createComponent(ContentComponent);
|
||||
it('should not show the component when there is DOM content but not text or an image', () => {
|
||||
const parentFixture = TestBed.createComponent(SpanContentComponent);
|
||||
parentFixture.detectChanges();
|
||||
const parentNative = parentFixture.nativeElement;
|
||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
||||
@@ -54,11 +75,18 @@ describe('MetadataFieldWrapperComponent', () => {
|
||||
});
|
||||
|
||||
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();
|
||||
const parentNative = parentFixture.nativeElement;
|
||||
const nativeContent = parentNative.querySelector(contentSelector);
|
||||
nativeContent.textContent = 'lorem ipsum';
|
||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
||||
parentFixture.detectChanges();
|
||||
expect(nativeWrapper.classList.contains('d-none')).toBe(false);
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { hasNoValue } from '../../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* This component renders any content inside this wrapper.
|
||||
@@ -11,6 +12,15 @@ import { Component, Input } from '@angular/core';
|
||||
})
|
||||
export class MetadataFieldWrapperComponent {
|
||||
|
||||
/**
|
||||
* The label (title) for the content
|
||||
*/
|
||||
@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 {
|
||||
|
||||
/**
|
||||
* Optional text to replace the links with
|
||||
* If undefined, the metadata value (uri) is displayed
|
||||
*/
|
||||
@Input() linktext: any;
|
||||
|
||||
/**
|
||||
* The metadata values to display
|
||||
*/
|
||||
@Input() mdValues: MetadataValue[];
|
||||
|
||||
/**
|
||||
* The seperator used to split the metadata values (can contain HTML)
|
||||
*/
|
||||
@Input() separator: string;
|
||||
|
||||
/**
|
||||
* The label for this iteration of metadata values
|
||||
*/
|
||||
@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 {
|
||||
|
||||
/**
|
||||
* The metadata values to display
|
||||
*/
|
||||
@Input() mdValues: MetadataValue[];
|
||||
|
||||
/**
|
||||
* The seperator used to split the metadata values (can contain HTML)
|
||||
*/
|
||||
@Input() separator: string;
|
||||
|
||||
/**
|
||||
* The label for this iteration of metadata values
|
||||
*/
|
||||
@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 { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable , BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { ItemPageComponent } from '../simple/item-page.component';
|
||||
import { MetadataMap } from '../../core/shared/metadata.models';
|
||||
@@ -32,7 +31,7 @@ import { hasValue } from '../../shared/empty.util';
|
||||
})
|
||||
export class FullItemPageComponent extends ItemPageComponent implements OnInit {
|
||||
|
||||
itemRD$: Observable<RemoteData<Item>>;
|
||||
itemRD$: BehaviorSubject<RemoteData<Item>>;
|
||||
|
||||
metadata$: Observable<MetadataMap>;
|
||||
|
||||
|
@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
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 { 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 { 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 { 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 { CollectionsComponent } from './field-components/collections/collections.component';
|
||||
import { FullItemPageComponent } from './full/full-item-page.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 { MetadataRepresentationListComponent } from './simple/metadata-representation-list/metadata-representation-list.component';
|
||||
import { RelatedEntitiesSearchComponent } from './simple/related-entities/related-entities-search/related-entities-search.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
EditItemPageModule,
|
||||
ItemPageRoutingModule
|
||||
ItemPageRoutingModule,
|
||||
SearchPageModule
|
||||
],
|
||||
declarations: [
|
||||
ItemPageComponent,
|
||||
@@ -38,10 +52,31 @@ import { EditItemPageModule } from './edit-item-page/edit-item-page.module';
|
||||
ItemPageAbstractFieldComponent,
|
||||
ItemPageUriFieldComponent,
|
||||
ItemPageTitleFieldComponent,
|
||||
ItemPageSpecificFieldComponent,
|
||||
ItemPageFieldComponent,
|
||||
FileSectionComponent,
|
||||
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 {
|
||||
|
@@ -5,6 +5,7 @@ import { RemoteData } from '../core/data/remote-data';
|
||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||
import { ItemDataService } from '../core/data/item-data.service';
|
||||
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
|
||||
|
@@ -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 { Item } from '../../../../../core/shared/item.model';
|
||||
import { ItemPageSpecificFieldComponent } from '../item-page-specific-field.component';
|
||||
import { ItemPageFieldComponent } from '../item-page-field.component';
|
||||
|
||||
@Component({
|
||||
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;
|
||||
|
||||
/**
|
||||
* Separator string between multiple values of the metadata fields defined
|
||||
* @type {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[] = [
|
||||
'dc.description.abstract'
|
||||
];
|
||||
|
||||
/**
|
||||
* Label i18n key for the rendered metadata
|
||||
*/
|
||||
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 { Item } from '../../../../../core/shared/item.model';
|
||||
import { ItemPageSpecificFieldComponent } from '../item-page-specific-field.component';
|
||||
import { ItemPageFieldComponent } from '../item-page-field.component';
|
||||
|
||||
@Component({
|
||||
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;
|
||||
|
||||
/**
|
||||
* Separator string between multiple values of the metadata fields defined
|
||||
* @type {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[] = [
|
||||
'dc.contributor.author',
|
||||
'dc.creator',
|
||||
'dc.contributor'
|
||||
];
|
||||
|
||||
/**
|
||||
* Label i18n key for the rendered metadata
|
||||
*/
|
||||
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 { Item } from '../../../../../core/shared/item.model';
|
||||
import { ItemPageSpecificFieldComponent } from '../item-page-specific-field.component';
|
||||
import { ItemPageFieldComponent } from '../item-page-field.component';
|
||||
|
||||
@Component({
|
||||
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;
|
||||
|
||||
/**
|
||||
* Separator string between multiple values of the metadata fields defined
|
||||
* @type {string}
|
||||
*/
|
||||
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[] = [
|
||||
'dc.date.issued'
|
||||
];
|
||||
|
||||
/**
|
||||
* Label i18n key for the rendered metadata
|
||||
*/
|
||||
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>
|
||||
</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({
|
||||
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;
|
||||
|
||||
/**
|
@@ -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 { Item } from '../../../../../core/shared/item.model';
|
||||
import { ItemPageSpecificFieldComponent } from '../item-page-specific-field.component';
|
||||
import { ItemPageFieldComponent } from '../item-page-field.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-page-title-field',
|
||||
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;
|
||||
|
||||
/**
|
||||
* Separator string between multiple values of the metadata fields defined
|
||||
* @type {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[] = [
|
||||
'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>
|
||||
</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 { Item } from '../../../../../core/shared/item.model';
|
||||
import { ItemPageSpecificFieldComponent } from '../item-page-specific-field.component';
|
||||
import { ItemPageFieldComponent } from '../item-page-field.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-page-uri-field',
|
||||
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;
|
||||
|
||||
/**
|
||||
* Separator string between multiple values of the metadata fields defined
|
||||
* @type {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[] = [
|
||||
'dc.identifier.uri'
|
||||
];
|
||||
|
||||
/**
|
||||
* Label i18n key for the rendered metadata
|
||||
*/
|
||||
label = 'item.page.uri';
|
||||
|
||||
}
|
||||
|
@@ -1,27 +1,7 @@
|
||||
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
||||
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="itemRD?.payload as item">
|
||||
<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]="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>
|
||||
<ds-item-type-switcher [object]="item" [viewMode]="viewMode"></ds-item-type-switcher>
|
||||
</div>
|
||||
</div>
|
||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
||||
|
@@ -1 +1,9 @@
|
||||
@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 {mergeMap, filter, map} from 'rxjs/operators';
|
||||
import { filter, map, mergeMap } from 'rxjs/operators';
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@@ -14,6 +13,9 @@ import { MetadataService } from '../../core/metadata/metadata.service';
|
||||
|
||||
import { fadeInOut } from '../../shared/animations/fade';
|
||||
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.
|
||||
@@ -29,21 +31,31 @@ import { hasValue } from '../../shared/empty.util';
|
||||
})
|
||||
export class ItemPageComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The item's id
|
||||
*/
|
||||
id: number;
|
||||
|
||||
private sub: any;
|
||||
|
||||
/**
|
||||
* The item wrapped in a remote-data object
|
||||
*/
|
||||
itemRD$: Observable<RemoteData<Item>>;
|
||||
|
||||
/**
|
||||
* The item's thumbnail
|
||||
*/
|
||||
thumbnail$: Observable<Bitstream>;
|
||||
|
||||
/**
|
||||
* The view-mode we're currently on
|
||||
*/
|
||||
viewMode = VIEW_MODE_FULL;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private items: ItemDataService,
|
||||
private metadataService: MetadataService
|
||||
) {
|
||||
|
||||
}
|
||||
private metadataService: MetadataService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
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;
|
||||
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);
|
||||
this.pagination = options.pagination;
|
||||
this.sort = options.sort;
|
||||
|
@@ -13,8 +13,10 @@ import {
|
||||
import { SearchFiltersState } from './search-filter.reducer';
|
||||
import { SearchFilterConfig } from '../../search-service/search-filter-config.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 { of as observableOf } from 'rxjs';
|
||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||
|
||||
describe('SearchFilterService', () => {
|
||||
let service: SearchFilterService;
|
||||
@@ -26,6 +28,12 @@ describe('SearchFilterService', () => {
|
||||
isOpenByDefault: false,
|
||||
pageSize: 2
|
||||
});
|
||||
|
||||
const mockFixedFilterService: SearchFixedFilterService = {
|
||||
getQueryByFilterName: (filter: string) => {
|
||||
return observableOf(undefined)
|
||||
}
|
||||
} as SearchFixedFilterService
|
||||
const value1 = 'random value';
|
||||
// const value2 = 'another value';
|
||||
const store: Store<SearchFiltersState> = jasmine.createSpyObj('store', {
|
||||
@@ -45,11 +53,15 @@ describe('SearchFilterService', () => {
|
||||
},
|
||||
addQueryParameterValue: (param: string, value: string) => {
|
||||
},
|
||||
getQueryParameterValue: (param: string) => {
|
||||
},
|
||||
getQueryParameterValues: (param: string) => {
|
||||
return observableOf({});
|
||||
},
|
||||
getQueryParamsWithPrefix: (param: string) => {
|
||||
return observableOf({});
|
||||
},
|
||||
getRouteParameterValue: (param: string) => {
|
||||
}
|
||||
/* tslint:enable:no-empty */
|
||||
};
|
||||
@@ -59,7 +71,7 @@ describe('SearchFilterService', () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
service = new SearchFilterService(store, routeServiceStub);
|
||||
service = new SearchFilterService(store, routeServiceStub, mockFixedFilterService);
|
||||
});
|
||||
|
||||
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 { mergeMap, map, distinctUntilChanged } from 'rxjs/operators';
|
||||
import { Injectable, InjectionToken } from '@angular/core';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { SearchFiltersState, SearchFilterState } from './search-filter.reducer';
|
||||
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
import {
|
||||
@@ -16,6 +16,11 @@ import {
|
||||
import { hasValue, isNotEmpty, } from '../../../shared/empty.util';
|
||||
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||
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';
|
||||
|
||||
const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
|
||||
@@ -29,8 +34,8 @@ export const FILTER_CONFIG: InjectionToken<SearchFilterConfig> = new InjectionTo
|
||||
export class SearchFilterService {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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 { SearchFilter } from './search-filter.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
|
||||
*/
|
||||
export class SearchOptions {
|
||||
view?: SetViewMode = SetViewMode.List;
|
||||
scope?: string;
|
||||
query?: string;
|
||||
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.query = options.query;
|
||||
this.dsoType = options.dsoType;
|
||||
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
|
||||
*/
|
||||
toRestUrl(url: string, args: string[] = []): string {
|
||||
|
||||
if (isNotEmpty(this.fixedFilter)) {
|
||||
args.push(this.fixedFilter);
|
||||
}
|
||||
if (isNotEmpty(this.query)) {
|
||||
args.push(`query=${this.query}`);
|
||||
}
|
||||
|
@@ -2,11 +2,14 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
import { FilteredSearchPageComponent } from './filtered-search-page.component';
|
||||
import { FilteredSearchPageGuard } from './filtered-search-page.guard';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
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="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"
|
||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"></ds-search-sidebar>
|
||||
<div class="col-12 col-md-9">
|
||||
<ds-search-form id="search-form"
|
||||
<div class="col-12 col-md-{{12 - sideBarWidth}}">
|
||||
<ds-search-form *ngIf="searchEnabled" id="search-form"
|
||||
[query]="(searchOptions$ | async)?.query"
|
||||
[scope]="(searchOptions$ | async)?.scope"
|
||||
[currentUrl]="getSearchLink()"
|
||||
[scopes]="(scopeListRD$ | async)">
|
||||
</ds-search-form>
|
||||
<ds-search-labels></ds-search-labels>
|
||||
<ds-search-labels *ngIf="searchEnabled"></ds-search-labels>
|
||||
<div class="row">
|
||||
<div id="search-body"
|
||||
class="row-offcanvas row-offcanvas-left"
|
||||
@@ -31,7 +31,9 @@
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
|
@@ -20,8 +20,8 @@ import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
|
||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { RouteService } from '../shared/services/route.service';
|
||||
|
||||
describe('SearchPageComponent', () => {
|
||||
let comp: SearchPageComponent;
|
||||
let fixture: ComponentFixture<SearchPageComponent>;
|
||||
let searchServiceObject: SearchService;
|
||||
@@ -44,9 +44,11 @@ describe('SearchPageComponent', () => {
|
||||
});
|
||||
const queryParam = 'test query';
|
||||
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
||||
const fixedFilter = 'fixed filter';
|
||||
const paginatedSearchOptions = {
|
||||
query: queryParam,
|
||||
scope: scopeParam,
|
||||
fixedFilter: fixedFilter,
|
||||
pagination,
|
||||
sort
|
||||
};
|
||||
@@ -62,10 +64,16 @@ describe('SearchPageComponent', () => {
|
||||
expand: () => this.isCollapsed = observableOf(false)
|
||||
};
|
||||
|
||||
beforeEach(async(() => {
|
||||
const routeServiceStub = {
|
||||
getRouteParameterValue: () => {
|
||||
return observableOf('');
|
||||
}
|
||||
};
|
||||
|
||||
export function configureSearchComponentTestingModule(compType) {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, NgbCollapseModule.forRoot()],
|
||||
declarations: [SearchPageComponent],
|
||||
declarations: [compType],
|
||||
providers: [
|
||||
{ provide: SearchService, useValue: searchServiceStub },
|
||||
{
|
||||
@@ -91,20 +99,35 @@ describe('SearchPageComponent', () => {
|
||||
{
|
||||
provide: SearchFilterService,
|
||||
useValue: {}
|
||||
}, {
|
||||
},
|
||||
{
|
||||
provide: SearchConfigurationService,
|
||||
useValue: {
|
||||
paginatedSearchOptions: hot('a', {
|
||||
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]
|
||||
}).overrideComponent(SearchPageComponent, {
|
||||
}).overrideComponent(compType, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}
|
||||
|
||||
describe('SearchPageComponent', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
configureSearchComponentTestingModule(SearchPageComponent);
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -177,4 +200,4 @@ describe('SearchPageComponent', () => {
|
||||
});
|
||||
|
||||
});
|
||||
})
|
||||
});
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { Observable , Subscription , BehaviorSubject } from 'rxjs';
|
||||
import { switchMap, } from 'rxjs/operators';
|
||||
import { PaginatedList } from '../core/data/paginated-list';
|
||||
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 { SearchService } from './search-service/search.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 { getSucceededRemoteData } from '../core/shared/operators';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
import { RouteService } from '../shared/services/route.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-page',
|
||||
@@ -31,6 +26,7 @@ import { getSucceededRemoteData } from '../core/shared/operators';
|
||||
|
||||
/**
|
||||
* This component represents the whole search page
|
||||
* It renders search results depending on the current search options
|
||||
*/
|
||||
export class SearchPageComponent implements OnInit {
|
||||
|
||||
@@ -59,11 +55,30 @@ export class SearchPageComponent implements OnInit {
|
||||
*/
|
||||
sub: Subscription;
|
||||
|
||||
constructor(private service: SearchService,
|
||||
private sidebarService: SearchSidebarService,
|
||||
private windowService: HostWindowService,
|
||||
private filterService: SearchFilterService,
|
||||
private searchConfigService: SearchConfigurationService) {
|
||||
/**
|
||||
* Whether or not the search bar should be visible
|
||||
*/
|
||||
@Input()
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -75,7 +90,7 @@ export class SearchPageComponent implements OnInit {
|
||||
* If something changes, update the list of scopes for the dropdown
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
|
||||
this.searchOptions$ = this.getSearchOptions();
|
||||
this.sub = this.searchOptions$.pipe(
|
||||
switchMap((options) => this.service.search(options).pipe(getSucceededRemoteData())))
|
||||
.subscribe((results) => {
|
||||
@@ -84,6 +99,17 @@ export class SearchPageComponent implements OnInit {
|
||||
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
||||
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 { SearchFacetFilterComponent } from './search-filters/search-filter/search-facet-filter/search-facet-filter.component';
|
||||
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 { 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';
|
||||
@@ -43,6 +46,7 @@ const effects = [
|
||||
],
|
||||
declarations: [
|
||||
SearchPageComponent,
|
||||
FilteredSearchPageComponent,
|
||||
SearchResultsComponent,
|
||||
SearchSidebarComponent,
|
||||
SearchSettingsComponent,
|
||||
@@ -68,6 +72,9 @@ const effects = [
|
||||
SearchService,
|
||||
SearchSidebarService,
|
||||
SearchFilterService,
|
||||
SearchFixedFilterService,
|
||||
FilteredSearchPageGuard,
|
||||
SearchFilterService,
|
||||
SearchConfigurationService
|
||||
],
|
||||
entryComponents: [
|
||||
@@ -82,6 +89,9 @@ const effects = [
|
||||
SearchTextFilterComponent,
|
||||
SearchHierarchyFilterComponent,
|
||||
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>
|
||||
<ds-viewable-collection
|
||||
[config]="searchConfig.pagination"
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { ComponentFixture, TestBed, async, tick, fakeAsync } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
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 { Community } from '../../core/shared/community.model';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
@@ -11,6 +11,8 @@ import { QueryParamsDirectiveStub } from '../../shared/testing/query-params-dire
|
||||
describe('SearchResultsComponent', () => {
|
||||
let comp: SearchResultsComponent;
|
||||
let fixture: ComponentFixture<SearchResultsComponent>;
|
||||
let heading: DebugElement;
|
||||
let title: DebugElement;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -24,7 +26,9 @@ describe('SearchResultsComponent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
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', () => {
|
||||
|
@@ -2,11 +2,12 @@ import { Component, Input } from '@angular/core';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
||||
import { SetViewMode } from '../../shared/view-mode';
|
||||
import { SearchOptions } from '../search-options.model';
|
||||
import { SearchResult } from '../search-result.model';
|
||||
import { PaginatedList } from '../../core/data/paginated-list';
|
||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-results',
|
||||
@@ -30,11 +31,23 @@ export class SearchResultsComponent {
|
||||
* The current configuration of the search
|
||||
*/
|
||||
@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.
|
||||
|
@@ -23,17 +23,21 @@ describe('SearchConfigurationService', () => {
|
||||
|
||||
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),
|
||||
getQueryParamsWithPrefix: observableOf(prefixFilter)
|
||||
getQueryParamsWithPrefix: observableOf(prefixFilter),
|
||||
getRouteParameterValue: observableOf('')
|
||||
});
|
||||
|
||||
const fixedFilterService = jasmine.createSpyObj('SearchFixedFilterService', {
|
||||
getQueryByFilterName: observableOf(''),
|
||||
});
|
||||
|
||||
const activatedRoute: any = new ActivatedRouteStub();
|
||||
|
||||
beforeEach(() => {
|
||||
service = new SearchConfigurationService(spy, activatedRoute);
|
||||
service = new SearchConfigurationService(routeService, fixedFilterService, activatedRoute);
|
||||
});
|
||||
|
||||
describe('when the scope is called', () => {
|
||||
beforeEach(() => {
|
||||
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,
|
||||
Subscription
|
||||
} 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 { PaginationComponentOptions } from '../../shared/pagination/pagination-component-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 { Injectable, OnDestroy } from '@angular/core';
|
||||
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 { getSucceededRemoteData } from '../../core/shared/operators';
|
||||
import { SearchFilter } from '../search-filter.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
|
||||
@@ -75,6 +76,7 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
* @param {ActivatedRoute} route
|
||||
*/
|
||||
constructor(private routeService: RouteService,
|
||||
private fixedFilterService: SearchFixedFilterService,
|
||||
private route: ActivatedRoute) {
|
||||
this.defaults
|
||||
.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
|
||||
*/
|
||||
@@ -191,7 +202,8 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
this.getScopePart(defaults.scope),
|
||||
this.getQueryPart(defaults.query),
|
||||
this.getDSOTypePart(),
|
||||
this.getFiltersPart()
|
||||
this.getFiltersPart(),
|
||||
this.getFixedFilterPart()
|
||||
).subscribe((update) => {
|
||||
const currentValue: SearchOptions = this.searchOptions.getValue();
|
||||
const updatedValue: SearchOptions = Object.assign(currentValue, update);
|
||||
@@ -211,7 +223,8 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
this.getScopePart(defaults.scope),
|
||||
this.getQueryPart(defaults.query),
|
||||
this.getDSOTypePart(),
|
||||
this.getFiltersPart()
|
||||
this.getFiltersPart(),
|
||||
this.getFixedFilterPart()
|
||||
).subscribe((update) => {
|
||||
const currentValue: PaginatedSearchOptions = this.paginatedSearchOptions.getValue();
|
||||
const updatedValue: PaginatedSearchOptions = Object.assign(currentValue, update);
|
||||
@@ -297,4 +310,30 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
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 { 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 { ActivatedRoute, Router, UrlTree } from '@angular/router';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
@@ -64,7 +67,7 @@ describe('SearchService', () => {
|
||||
|
||||
it('should return list view mode', () => {
|
||||
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', () => {
|
||||
searchService.setViewMode(ViewMode.List);
|
||||
searchService.setViewMode(SetViewMode.List);
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/search'], {
|
||||
queryParams: { view: ViewMode.List },
|
||||
queryParams: { view: SetViewMode.List },
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
});
|
||||
|
||||
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'], {
|
||||
queryParams: { view: ViewMode.Grid },
|
||||
queryParams: { view: SetViewMode.Grid },
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return ViewMode.List when the viewMode is set to ViewMode.List in the ActivatedRoute', () => {
|
||||
let viewMode = ViewMode.Grid;
|
||||
route.testParams = { view: ViewMode.List };
|
||||
let viewMode = SetViewMode.Grid;
|
||||
route.testParams = { view: SetViewMode.List };
|
||||
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', () => {
|
||||
let viewMode = ViewMode.List;
|
||||
route.testParams = { view: ViewMode.Grid };
|
||||
let viewMode = SetViewMode.List;
|
||||
route.testParams = { view: SetViewMode.Grid };
|
||||
searchService.getViewMode().subscribe((mode) => viewMode = mode);
|
||||
expect(viewMode).toEqual(ViewMode.Grid);
|
||||
expect(viewMode).toEqual(SetViewMode.Grid);
|
||||
});
|
||||
|
||||
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