mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-10 03:23:07 +00:00
Merge branch 'master' into w2p-45621_filter-facets-ui
Conflicts: src/app/+search-page/search-page.component.html src/app/+search-page/search-page.component.spec.ts src/app/+search-page/search-page.component.ts src/app/+search-page/search-results/search-results.component.html src/app/+search-page/search-service/search.service.ts src/app/shared/search-form/search-form.component.html src/app/shared/shared.module.ts
This commit is contained in:
@@ -11,7 +11,7 @@ language: node_js
|
|||||||
|
|
||||||
node_js:
|
node_js:
|
||||||
- "6"
|
- "6"
|
||||||
- "7"
|
- "8"
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
yarn: true
|
yarn: true
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { browser, element, by } from 'protractor';
|
import { browser, element, by, protractor } from 'protractor';
|
||||||
import { promise } from 'selenium-webdriver';
|
import { promise } from 'selenium-webdriver';
|
||||||
|
|
||||||
export class ProtractorPage {
|
export class ProtractorPage {
|
||||||
@@ -17,7 +17,9 @@ export class ProtractorPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getCurrentScope(): promise.Promise<string> {
|
getCurrentScope(): promise.Promise<string> {
|
||||||
return element(by.tagName('select')).getAttribute('value');
|
const scopeSelect = element(by.tagName('select'));
|
||||||
|
browser.wait(protractor.ExpectedConditions.presenceOf(scopeSelect), 10000);
|
||||||
|
return scopeSelect.getAttribute('value');
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentQuery(): promise.Promise<string> {
|
getCurrentQuery(): promise.Promise<string> {
|
||||||
|
@@ -131,7 +131,7 @@
|
|||||||
"angular2-template-loader": "0.6.2",
|
"angular2-template-loader": "0.6.2",
|
||||||
"autoprefixer": "7.1.5",
|
"autoprefixer": "7.1.5",
|
||||||
"awesome-typescript-loader": "3.2.3",
|
"awesome-typescript-loader": "3.2.3",
|
||||||
"caniuse-lite": "1.0.30000697",
|
"caniuse-lite": "1.0.30000746",
|
||||||
"codelyzer": "3.2.1",
|
"codelyzer": "3.2.1",
|
||||||
"compression-webpack-plugin": "1.0.1",
|
"compression-webpack-plugin": "1.0.1",
|
||||||
"copy-webpack-plugin": "4.1.1",
|
"copy-webpack-plugin": "4.1.1",
|
||||||
|
@@ -118,9 +118,10 @@
|
|||||||
"community": "Loading community...",
|
"community": "Loading community...",
|
||||||
"collection": "Loading collection...",
|
"collection": "Loading collection...",
|
||||||
"sub-collections": "Loading sub-collections...",
|
"sub-collections": "Loading sub-collections...",
|
||||||
"items": "Loading items...",
|
"recent-submissions": "Loading recent submissions...",
|
||||||
"item": "Loading item...",
|
"item": "Loading item...",
|
||||||
"objects": "Loading..."
|
"objects": "Loading...",
|
||||||
|
"search-results": "Loading search results..."
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"default": "Error",
|
"default": "Error",
|
||||||
@@ -128,8 +129,9 @@
|
|||||||
"community": "Error fetching community",
|
"community": "Error fetching community",
|
||||||
"collection": "Error fetching collection",
|
"collection": "Error fetching collection",
|
||||||
"sub-collections": "Error fetching sub-collections",
|
"sub-collections": "Error fetching sub-collections",
|
||||||
"items": "Error fetching items",
|
"recent-submissions": "Error fetching recent submissions",
|
||||||
"item": "Error fetching item",
|
"item": "Error fetching item",
|
||||||
"objects": "Error fetching objects"
|
"objects": "Error fetching objects",
|
||||||
|
"search-results": "Error fetching search results"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,52 +1,56 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="collection-page">
|
<div class="collection-page"
|
||||||
<div *ngIf="collectionData.hasSucceeded | async" @fadeInOut>
|
*ngVar="(collectionRDObs | async) as collectionRD">
|
||||||
<div *ngIf="collectionData.payload | async; let collectionPayload">
|
<div *ngIf="collectionRD?.hasSucceeded" @fadeInOut>
|
||||||
|
<div *ngIf="collectionRD?.payload as collection">
|
||||||
<!-- Collection Name -->
|
<!-- Collection Name -->
|
||||||
<ds-comcol-page-header
|
<ds-comcol-page-header
|
||||||
[name]="collectionPayload.name">
|
[name]="collection.name">
|
||||||
</ds-comcol-page-header>
|
</ds-comcol-page-header>
|
||||||
<!-- Collection logo -->
|
<!-- Collection logo -->
|
||||||
<ds-comcol-page-logo *ngIf="logoData"
|
<ds-comcol-page-logo *ngIf="logoRDObs"
|
||||||
[logo]="logoData.payload | async"
|
[logo]="(logoRDObs | async)?.payload"
|
||||||
[alternateText]="'Collection Logo'">
|
[alternateText]="'Collection Logo'">
|
||||||
</ds-comcol-page-logo>
|
</ds-comcol-page-logo>
|
||||||
<!-- Introductionary text -->
|
<!-- Introductionary text -->
|
||||||
<ds-comcol-page-content
|
<ds-comcol-page-content
|
||||||
[content]="collectionPayload.introductoryText"
|
[content]="collection.introductoryText"
|
||||||
[hasInnerHtml]="true">
|
[hasInnerHtml]="true">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
<!-- News -->
|
<!-- News -->
|
||||||
<ds-comcol-page-content
|
<ds-comcol-page-content
|
||||||
[content]="collectionPayload.sidebarText"
|
[content]="collection.sidebarText"
|
||||||
[hasInnerHtml]="true"
|
[hasInnerHtml]="true"
|
||||||
[title]="'community.page.news'">
|
[title]="'community.page.news'">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
<!-- Copyright -->
|
<!-- Copyright -->
|
||||||
<ds-comcol-page-content
|
<ds-comcol-page-content
|
||||||
[content]="collectionPayload.copyrightText"
|
[content]="collection.copyrightText"
|
||||||
[hasInnerHtml]="true">
|
[hasInnerHtml]="true">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
<!-- Licence -->
|
<!-- Licence -->
|
||||||
<ds-comcol-page-content
|
<ds-comcol-page-content
|
||||||
[content]="collectionPayload.license"
|
[content]="collection.license"
|
||||||
[title]="'collection.page.license'">
|
[title]="'collection.page.license'">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="collectionData.hasFailed | async" message="{{'error.collection' | translate}}"></ds-error>
|
<ds-error *ngIf="collectionRD?.hasFailed" message="{{'error.collection' | translate}}"></ds-error>
|
||||||
<ds-loading *ngIf="collectionData.isLoading | async" message="{{'loading.collection' | translate}}"></ds-loading>
|
<ds-loading *ngIf="collectionRD?.isLoading" message="{{'loading.collection' | translate}}"></ds-loading>
|
||||||
<br>
|
<br>
|
||||||
<div *ngIf="itemData.hasSucceeded | async" @fadeIn>
|
<ng-container *ngVar="(itemRDObs | async) as itemRD">
|
||||||
|
<div *ngIf="itemRD?.hasSucceeded" @fadeIn>
|
||||||
<h2>{{'collection.page.browse.recent.head' | translate}}</h2>
|
<h2>{{'collection.page.browse.recent.head' | translate}}</h2>
|
||||||
<ds-object-list
|
<ds-object-list
|
||||||
[config]="paginationConfig"
|
[config]="paginationConfig"
|
||||||
[sortConfig]="sortConfig"
|
[sortConfig]="sortConfig"
|
||||||
[objects]="itemData"
|
[objects]="itemRD"
|
||||||
[hideGear]="false">
|
[hideGear]="false"
|
||||||
|
(paginationChange)="onPaginationChange($event)">
|
||||||
</ds-object-list>
|
</ds-object-list>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="itemData.hasFailed | async" message="{{'error.items' | translate}}"></ds-error>
|
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.recent-submissions' | translate}}"></ds-error>
|
||||||
<ds-loading *ngIf="itemData.isLoading | async" message="{{'loading.items' | translate}}"></ds-loading>
|
<ds-loading *ngIf="!itemRD || itemRD.isLoading" message="{{'loading.recent-submissions' | translate}}"></ds-loading>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,43 +1,37 @@
|
|||||||
import {
|
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
ChangeDetectionStrategy,
|
import { ActivatedRoute } from '@angular/router';
|
||||||
ChangeDetectorRef,
|
|
||||||
Component,
|
|
||||||
OnDestroy,
|
|
||||||
OnInit
|
|
||||||
} from '@angular/core';
|
|
||||||
import { ActivatedRoute, Params } from '@angular/router';
|
|
||||||
|
|
||||||
import { PageInfo } from '../core/shared/page-info.model';
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { Subscription } from 'rxjs/Subscription';
|
|
||||||
|
|
||||||
import { Collection } from '../core/shared/collection.model';
|
import { Subscription } from 'rxjs/Subscription';
|
||||||
import { Bitstream } from '../core/shared/bitstream.model';
|
import { SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
|
||||||
import { CollectionDataService } from '../core/data/collection-data.service';
|
import { CollectionDataService } from '../core/data/collection-data.service';
|
||||||
import { ItemDataService } from '../core/data/item-data.service';
|
import { ItemDataService } from '../core/data/item-data.service';
|
||||||
import { Item } from '../core/shared/item.model';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
import { SortOptions, SortDirection } from '../core/cache/models/sort-options.model';
|
|
||||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
|
||||||
import { hasValue, isNotEmpty, isUndefined } from '../shared/empty.util';
|
|
||||||
|
|
||||||
import { MetadataService } from '../core/metadata/metadata.service';
|
import { MetadataService } from '../core/metadata/metadata.service';
|
||||||
|
import { Bitstream } from '../core/shared/bitstream.model';
|
||||||
|
|
||||||
|
import { Collection } from '../core/shared/collection.model';
|
||||||
|
import { Item } from '../core/shared/item.model';
|
||||||
|
|
||||||
import { fadeIn, fadeInOut } from '../shared/animations/fade';
|
import { fadeIn, fadeInOut } from '../shared/animations/fade';
|
||||||
|
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||||
|
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-collection-page',
|
selector: 'ds-collection-page',
|
||||||
styleUrls: ['./collection-page.component.scss'],
|
styleUrls: ['./collection-page.component.scss'],
|
||||||
templateUrl: './collection-page.component.html',
|
templateUrl: './collection-page.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
animations: [
|
animations: [
|
||||||
fadeIn,
|
fadeIn,
|
||||||
fadeInOut
|
fadeInOut
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CollectionPageComponent implements OnInit, OnDestroy {
|
export class CollectionPageComponent implements OnInit, OnDestroy {
|
||||||
collectionData: RemoteData<Collection>;
|
collectionRDObs: Observable<RemoteData<Collection>>;
|
||||||
itemData: RemoteData<Item[]>;
|
itemRDObs: Observable<RemoteData<Item[]>>;
|
||||||
logoData: RemoteData<Bitstream>;
|
logoRDObs: Observable<RemoteData<Bitstream>>;
|
||||||
paginationConfig: PaginationComponentOptions;
|
paginationConfig: PaginationComponentOptions;
|
||||||
sortConfig: SortOptions;
|
sortConfig: SortOptions;
|
||||||
private subs: Subscription[] = [];
|
private subs: Subscription[] = [];
|
||||||
@@ -51,8 +45,7 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
|||||||
) {
|
) {
|
||||||
this.paginationConfig = new PaginationComponentOptions();
|
this.paginationConfig = new PaginationComponentOptions();
|
||||||
this.paginationConfig.id = 'collection-page-pagination';
|
this.paginationConfig.id = 'collection-page-pagination';
|
||||||
this.paginationConfig.pageSizeOptions = [4];
|
this.paginationConfig.pageSize = 5;
|
||||||
this.paginationConfig.pageSize = 4;
|
|
||||||
this.paginationConfig.currentPage = 1;
|
this.paginationConfig.currentPage = 1;
|
||||||
this.sortConfig = new SortOptions();
|
this.sortConfig = new SortOptions();
|
||||||
}
|
}
|
||||||
@@ -67,9 +60,12 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
|||||||
})
|
})
|
||||||
.subscribe((params) => {
|
.subscribe((params) => {
|
||||||
this.collectionId = params.id;
|
this.collectionId = params.id;
|
||||||
this.collectionData = this.collectionDataService.findById(this.collectionId);
|
this.collectionRDObs = this.collectionDataService.findById(this.collectionId);
|
||||||
this.metadata.processRemoteData(this.collectionData);
|
this.metadata.processRemoteData(this.collectionRDObs);
|
||||||
this.subs.push(this.collectionData.payload.subscribe((collection) => this.logoData = collection.logo));
|
this.subs.push(this.collectionRDObs
|
||||||
|
.map((rd: RemoteData<Collection>) => rd.payload)
|
||||||
|
.filter((collection: Collection) => hasValue(collection))
|
||||||
|
.subscribe((collection: Collection) => this.logoRDObs = collection.logo));
|
||||||
|
|
||||||
const page = +params.page || this.paginationConfig.currentPage;
|
const page = +params.page || this.paginationConfig.currentPage;
|
||||||
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
|
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
|
||||||
@@ -91,7 +87,7 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updatePage(searchOptions) {
|
updatePage(searchOptions) {
|
||||||
this.itemData = this.itemDataService.findAll({
|
this.itemRDObs = this.itemDataService.findAll({
|
||||||
scopeID: this.collectionId,
|
scopeID: this.collectionId,
|
||||||
currentPage: searchOptions.pagination.currentPage,
|
currentPage: searchOptions.pagination.currentPage,
|
||||||
elementsPerPage: searchOptions.pagination.pageSize,
|
elementsPerPage: searchOptions.pagination.pageSize,
|
||||||
@@ -106,4 +102,17 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
|||||||
isNotEmpty(object: any) {
|
isNotEmpty(object: any) {
|
||||||
return isNotEmpty(object);
|
return isNotEmpty(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPaginationChange(event) {
|
||||||
|
this.updatePage({
|
||||||
|
pagination: {
|
||||||
|
currentPage: event.page,
|
||||||
|
pageSize: event.pageSize
|
||||||
|
},
|
||||||
|
sort: {
|
||||||
|
field: event.sortField,
|
||||||
|
direction: event.sortDirection
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
<div class="container">
|
<div class="container" *ngVar="(communityRDObs | async) as communityRD">
|
||||||
<div class="community-page" *ngIf="communityData.hasSucceeded | async" @fadeInOut>
|
<div class="community-page" *ngIf="communityRD?.hasSucceeded" @fadeInOut>
|
||||||
<div *ngIf="communityData.payload | async; let communityPayload">
|
<div *ngIf="communityRD?.payload; let communityPayload">
|
||||||
<!-- Community name -->
|
<!-- Community name -->
|
||||||
<ds-comcol-page-header [name]="communityPayload.name"></ds-comcol-page-header>
|
<ds-comcol-page-header [name]="communityPayload.name"></ds-comcol-page-header>
|
||||||
<!-- Community logo -->
|
<!-- Community logo -->
|
||||||
<ds-comcol-page-logo *ngIf="logoData"
|
<ds-comcol-page-logo *ngIf="logoRDObs"
|
||||||
[logo]="logoData.payload | async"
|
[logo]="(logoRDObs | async)?.payload"
|
||||||
[alternateText]="'Community Logo'">
|
[alternateText]="'Community Logo'">
|
||||||
</ds-comcol-page-logo>
|
</ds-comcol-page-logo>
|
||||||
<!-- Introductionary text -->
|
<!-- Introductionary text -->
|
||||||
@@ -27,6 +27,6 @@
|
|||||||
<ds-community-page-sub-collection-list></ds-community-page-sub-collection-list>
|
<ds-community-page-sub-collection-list></ds-community-page-sub-collection-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="communityData.hasFailed | async" message="{{'error.community' | translate}}"></ds-error>
|
<ds-error *ngIf="communityRD?.hasFailed" message="{{'error.community' | translate}}"></ds-error>
|
||||||
<ds-loading *ngIf="communityData.isLoading | async" message="{{'loading.community' | translate}}"></ds-loading>
|
<ds-loading *ngIf="communityRD?.isLoading" message="{{'loading.community' | translate}}"></ds-loading>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,27 +1,29 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute, Params } from '@angular/router';
|
import { ActivatedRoute, Params } from '@angular/router';
|
||||||
|
|
||||||
import { Subscription } from 'rxjs/Subscription';
|
import { Subscription } from 'rxjs/Subscription';
|
||||||
|
import { CommunityDataService } from '../core/data/community-data.service';
|
||||||
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { Bitstream } from '../core/shared/bitstream.model';
|
||||||
|
|
||||||
import { Community } from '../core/shared/community.model';
|
import { Community } from '../core/shared/community.model';
|
||||||
import { Bitstream } from '../core/shared/bitstream.model';
|
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
|
||||||
import { CommunityDataService } from '../core/data/community-data.service';
|
|
||||||
import { hasValue } from '../shared/empty.util';
|
|
||||||
|
|
||||||
import { MetadataService } from '../core/metadata/metadata.service';
|
import { MetadataService } from '../core/metadata/metadata.service';
|
||||||
|
|
||||||
import { fadeInOut } from '../shared/animations/fade';
|
import { fadeInOut } from '../shared/animations/fade';
|
||||||
|
import { hasValue } from '../shared/empty.util';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-community-page',
|
selector: 'ds-community-page',
|
||||||
styleUrls: ['./community-page.component.scss'],
|
styleUrls: ['./community-page.component.scss'],
|
||||||
templateUrl: './community-page.component.html',
|
templateUrl: './community-page.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
animations: [fadeInOut]
|
animations: [fadeInOut]
|
||||||
})
|
})
|
||||||
export class CommunityPageComponent implements OnInit, OnDestroy {
|
export class CommunityPageComponent implements OnInit, OnDestroy {
|
||||||
communityData: RemoteData<Community>;
|
communityRDObs: Observable<RemoteData<Community>>;
|
||||||
logoData: RemoteData<Bitstream>;
|
logoRDObs: Observable<RemoteData<Bitstream>>;
|
||||||
private subs: Subscription[] = [];
|
private subs: Subscription[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -34,9 +36,12 @@ export class CommunityPageComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.route.params.subscribe((params: Params) => {
|
this.route.params.subscribe((params: Params) => {
|
||||||
this.communityData = this.communityDataService.findById(params.id);
|
this.communityRDObs = this.communityDataService.findById(params.id);
|
||||||
this.metadata.processRemoteData(this.communityData);
|
this.metadata.processRemoteData(this.communityRDObs);
|
||||||
this.subs.push(this.communityData.payload.subscribe((community) => this.logoData = community.logo));
|
this.subs.push(this.communityRDObs
|
||||||
|
.map((rd: RemoteData<Community>) => rd.payload)
|
||||||
|
.filter((community: Community) => hasValue(community))
|
||||||
|
.subscribe((community: Community) => this.logoRDObs = community.logo));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,13 +1,15 @@
|
|||||||
<div *ngIf="subCollections.hasSucceeded | async" @fadeIn>
|
<ng-container *ngVar="(subCollectionsRDObs | async) as subCollectionsRD">
|
||||||
|
<div *ngIf="subCollectionsRD?.hasSucceeded" @fadeIn>
|
||||||
<h2>{{'community.sub-collection-list.head' | translate}}</h2>
|
<h2>{{'community.sub-collection-list.head' | translate}}</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li *ngFor="let collection of (subCollections.payload | async)">
|
<li *ngFor="let collection of subCollectionsRD?.payload">
|
||||||
<p>
|
<p>
|
||||||
<span class="lead"><a [routerLink]="['/collections', collection.id]">{{collection.name}}</a></span><br>
|
<span class="lead"><a [routerLink]="['/collections', collection.id]">{{collection.name}}</a></span><br>
|
||||||
<span class="text-muted">{{collection.shortDescription}}</span>
|
<span class="text-muted">{{collection.shortDescription}}</span>
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="subCollections.hasFailed | async" message="{{'error.sub-collections' | translate}}"></ds-error>
|
<ds-error *ngIf="subCollectionsRD?.hasFailed" message="{{'error.sub-collections' | translate}}"></ds-error>
|
||||||
<ds-loading *ngIf="subCollections.isLoading | async" message="{{'loading.sub-collections' | translate}}"></ds-loading>
|
<ds-loading *ngIf="subCollectionsRD?.isLoading" message="{{'loading.sub-collections' | translate}}"></ds-loading>
|
||||||
|
</ng-container>
|
||||||
|
@@ -5,6 +5,7 @@ import { RemoteData } from '../../core/data/remote-data';
|
|||||||
import { Collection } from '../../core/shared/collection.model';
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
|
|
||||||
import { fadeIn } from '../../shared/animations/fade';
|
import { fadeIn } from '../../shared/animations/fade';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-community-page-sub-collection-list',
|
selector: 'ds-community-page-sub-collection-list',
|
||||||
@@ -13,13 +14,13 @@ import { fadeIn } from '../../shared/animations/fade';
|
|||||||
animations:[fadeIn]
|
animations:[fadeIn]
|
||||||
})
|
})
|
||||||
export class CommunityPageSubCollectionListComponent implements OnInit {
|
export class CommunityPageSubCollectionListComponent implements OnInit {
|
||||||
subCollections: RemoteData<Collection[]>;
|
subCollectionsRDObs: Observable<RemoteData<Collection[]>>;
|
||||||
|
|
||||||
constructor(private cds: CollectionDataService) {
|
constructor(private cds: CollectionDataService) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.subCollections = this.cds.findAll();
|
this.subCollectionsRDObs = this.cds.findAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
import { SharedModule } from '../shared/shared.module';
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { HomeNewsComponent } from './home-news/home-news.component';
|
||||||
|
import { HomePageRoutingModule } from './home-page-routing.module';
|
||||||
|
|
||||||
import { HomePageComponent } from './home-page.component';
|
import { HomePageComponent } from './home-page.component';
|
||||||
import { HomePageRoutingModule } from './home-page-routing.module';
|
|
||||||
import { TopLevelCommunityListComponent } from './top-level-community-list/top-level-community-list.component';
|
import { TopLevelCommunityListComponent } from './top-level-community-list/top-level-community-list.component';
|
||||||
import { HomeNewsComponent } from './home-news/home-news.component';
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@@ -1,13 +1,15 @@
|
|||||||
<div *ngIf="topLevelCommunities.hasSucceeded | async" @fadeInOut>
|
<ng-container *ngVar="(communitiesRDObs | async) as communitiesRD">
|
||||||
|
<div *ngIf="communitiesRD?.hasSucceeded" @fadeInOut>
|
||||||
<h2>{{'home.top-level-communities.head' | translate}}</h2>
|
<h2>{{'home.top-level-communities.head' | translate}}</h2>
|
||||||
<p class="lead">{{'home.top-level-communities.help' | translate}}</p>
|
<p class="lead">{{'home.top-level-communities.help' | translate}}</p>
|
||||||
<ds-object-list
|
<ds-object-list
|
||||||
[config]="config"
|
[config]="config"
|
||||||
[sortConfig]="sortConfig"
|
[sortConfig]="sortConfig"
|
||||||
[objects]="topLevelCommunities"
|
[objects]="communitiesRD"
|
||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
(paginationChange)="updatePage($event)">
|
(paginationChange)="updatePage($event)">
|
||||||
</ds-object-list>
|
</ds-object-list>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="topLevelCommunities.hasFailed | async" message="{{'error.top-level-communites' | translate}}"></ds-error>
|
<ds-error *ngIf="communitiesRD?.hasFailed" message="{{'error.top-level-communites' | translate}}"></ds-error>
|
||||||
<ds-loading *ngIf="topLevelCommunities.isLoading | async" message="{{'loading.top-level-communities' | translate}}"></ds-loading>
|
<ds-loading *ngIf="communitiesRD?.isLoading" message="{{'loading.top-level-communities' | translate}}"></ds-loading>
|
||||||
|
</ng-container>
|
||||||
|
@@ -1,30 +1,30 @@
|
|||||||
import { Component, OnInit, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
|
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
|
||||||
import { Community } from '../../core/shared/community.model';
|
import { Community } from '../../core/shared/community.model';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
|
||||||
import { SortOptions, SortDirection } from '../../core/cache/models/sort-options.model';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
|
|
||||||
import { fadeInOut } from '../../shared/animations/fade';
|
import { fadeInOut } from '../../shared/animations/fade';
|
||||||
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-top-level-community-list',
|
selector: 'ds-top-level-community-list',
|
||||||
styleUrls: ['./top-level-community-list.component.scss'],
|
styleUrls: ['./top-level-community-list.component.scss'],
|
||||||
templateUrl: './top-level-community-list.component.html',
|
templateUrl: './top-level-community-list.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
animations: [fadeInOut]
|
animations: [fadeInOut]
|
||||||
})
|
})
|
||||||
export class TopLevelCommunityListComponent {
|
export class TopLevelCommunityListComponent {
|
||||||
topLevelCommunities: RemoteData<Community[]>;
|
communitiesRDObs: Observable<RemoteData<Community[]>>;
|
||||||
config: PaginationComponentOptions;
|
config: PaginationComponentOptions;
|
||||||
sortConfig: SortOptions;
|
sortConfig: SortOptions;
|
||||||
|
|
||||||
constructor(private cds: CommunityDataService) {
|
constructor(private cds: CommunityDataService) {
|
||||||
this.config = new PaginationComponentOptions();
|
this.config = new PaginationComponentOptions();
|
||||||
this.config.id = 'top-level-pagination';
|
this.config.id = 'top-level-pagination';
|
||||||
this.config.pageSizeOptions = [4];
|
this.config.pageSize = 5;
|
||||||
this.config.pageSize = 4;
|
|
||||||
this.config.currentPage = 1;
|
this.config.currentPage = 1;
|
||||||
this.sortConfig = new SortOptions();
|
this.sortConfig = new SortOptions();
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ export class TopLevelCommunityListComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updatePage(data) {
|
updatePage(data) {
|
||||||
this.topLevelCommunities = this.cds.findAll({
|
this.communitiesRDObs = this.cds.findAll({
|
||||||
currentPage: data.page,
|
currentPage: data.page,
|
||||||
elementsPerPage: data.pageSize,
|
elementsPerPage: data.pageSize,
|
||||||
sort: { field: data.sortField, direction: data.sortDirection }
|
sort: { field: data.sortField, direction: data.sortDirection }
|
||||||
|
@@ -4,6 +4,7 @@ import { Observable } from 'rxjs/Observable';
|
|||||||
import { Collection } from '../../../core/shared/collection.model';
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders the parent collections section of the item
|
* This component renders the parent collections section of the item
|
||||||
@@ -34,7 +35,7 @@ export class CollectionsComponent implements OnInit {
|
|||||||
// TODO: this should use parents, but the collections
|
// TODO: this should use parents, but the collections
|
||||||
// for an Item aren't returned by the REST API yet,
|
// for an Item aren't returned by the REST API yet,
|
||||||
// only the owning collection
|
// only the owning collection
|
||||||
this.collections = this.item.owner.payload.map((c) => [c]);
|
this.collections = this.item.owner.map((rd: RemoteData<Collection>) => [rd.payload]);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<ds-metadata-field-wrapper [label]="label | translate">
|
<ds-metadata-field-wrapper [label]="label | translate">
|
||||||
<div class="file-section row" *ngFor="let file of (files | async); let last=last;">
|
<div class="file-section row" *ngFor="let file of (bitstreamsObs | async); let last=last;">
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
<ds-thumbnail [thumbnail]="thumbnails.get(file.id) | async"></ds-thumbnail>
|
<ds-thumbnail [thumbnail]="thumbnails.get(file.id) | async"></ds-thumbnail>
|
||||||
</div>
|
</div>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<dt class="col-md-4">{{"item.page.filesection.format" | translate}}</dt>
|
<dt class="col-md-4">{{"item.page.filesection.format" | translate}}</dt>
|
||||||
<dd class="col-md-8">{{(file.mimetype)}}</dd>
|
<dd class="col-md-8">{{(file.format | async)?.payload?.description}}</dd>
|
||||||
|
|
||||||
|
|
||||||
<dt class="col-md-4">{{"item.page.filesection.description" | translate}}</dt>
|
<dt class="col-md-4">{{"item.page.filesection.description" | translate}}</dt>
|
||||||
|
@@ -22,7 +22,7 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
|
|||||||
|
|
||||||
label: string;
|
label: string;
|
||||||
|
|
||||||
files: Observable<Bitstream[]>;
|
bitstreamsObs: Observable<Bitstream[]>;
|
||||||
|
|
||||||
thumbnails: Map<string, Observable<Bitstream>> = new Map();
|
thumbnails: Map<string, Observable<Bitstream>> = new Map();
|
||||||
|
|
||||||
@@ -33,8 +33,8 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
|
|||||||
initialize(): void {
|
initialize(): void {
|
||||||
const originals = this.item.getFiles();
|
const originals = this.item.getFiles();
|
||||||
const licenses = this.item.getBitstreamsByBundleName('LICENSE');
|
const licenses = this.item.getBitstreamsByBundleName('LICENSE');
|
||||||
this.files = Observable.combineLatest(originals, licenses, (o, l) => [...o, ...l]);
|
this.bitstreamsObs = Observable.combineLatest(originals, licenses, (o, l) => [...o, ...l]);
|
||||||
this.files.subscribe(
|
this.bitstreamsObs.subscribe(
|
||||||
(files) =>
|
(files) =>
|
||||||
files.forEach(
|
files.forEach(
|
||||||
(original) => {
|
(original) => {
|
||||||
|
@@ -1,25 +1,25 @@
|
|||||||
<div class="container">
|
<div class="container" *ngVar="(itemRDObs | async) as itemRD">
|
||||||
<div class="item-page" *ngIf="item.hasSucceeded | async" @fadeInOut>
|
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||||
<div *ngIf="item.payload | async; let itemPayload">
|
<div *ngIf="itemRD?.payload as item">
|
||||||
<ds-item-page-title-field [item]="itemPayload"></ds-item-page-title-field>
|
<ds-item-page-title-field [item]="item"></ds-item-page-title-field>
|
||||||
<div class="simple-view-link">
|
<div class="simple-view-link">
|
||||||
<a class="btn btn-outline-primary col-4" [routerLink]="['/items/' + itemPayload.id]">
|
<a class="btn btn-outline-primary col-4" [routerLink]="['/items/' + item.id]">
|
||||||
{{"item.page.link.simple" | translate}}
|
{{"item.page.link.simple" | translate}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-responsive table-striped">
|
<table class="table table-responsive table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let metadatum of (metadata | async)">
|
<tr *ngFor="let metadatum of (metadataObs | async)">
|
||||||
<td>{{metadatum.key}}</td>
|
<td>{{metadatum.key}}</td>
|
||||||
<td>{{metadatum.value}}</td>
|
<td>{{metadatum.value}}</td>
|
||||||
<td>{{metadatum.language}}</td>
|
<td>{{metadatum.language}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<ds-item-page-full-file-section [item]="itemPayload"></ds-item-page-full-file-section>
|
<ds-item-page-full-file-section [item]="item"></ds-item-page-full-file-section>
|
||||||
<ds-item-page-collections [item]="itemPayload"></ds-item-page-collections>
|
<ds-item-page-collections [item]="item"></ds-item-page-collections>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="item.hasFailed | async" message="{{'error.item' | translate}}"></ds-error>
|
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
||||||
<ds-loading *ngIf="item.isLoading | async" message="{{'loading.item' | translate}}"></ds-loading>
|
<ds-loading *ngIf="itemRD?.isLoading" message="{{'loading.item' | translate}}"></ds-loading>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
@@ -13,6 +13,7 @@ import { Item } from '../../core/shared/item.model';
|
|||||||
import { MetadataService } from '../../core/metadata/metadata.service';
|
import { MetadataService } from '../../core/metadata/metadata.service';
|
||||||
|
|
||||||
import { fadeInOut } from '../../shared/animations/fade';
|
import { fadeInOut } from '../../shared/animations/fade';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a simple item page.
|
* This component renders a simple item page.
|
||||||
@@ -24,13 +25,14 @@ import { fadeInOut } from '../../shared/animations/fade';
|
|||||||
selector: 'ds-full-item-page',
|
selector: 'ds-full-item-page',
|
||||||
styleUrls: ['./full-item-page.component.scss'],
|
styleUrls: ['./full-item-page.component.scss'],
|
||||||
templateUrl: './full-item-page.component.html',
|
templateUrl: './full-item-page.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
animations: [fadeInOut]
|
animations: [fadeInOut]
|
||||||
})
|
})
|
||||||
export class FullItemPageComponent extends ItemPageComponent implements OnInit {
|
export class FullItemPageComponent extends ItemPageComponent implements OnInit {
|
||||||
|
|
||||||
item: RemoteData<Item>;
|
itemRDObs: Observable<RemoteData<Item>>;
|
||||||
|
|
||||||
metadata: Observable<Metadatum[]>;
|
metadataObs: Observable<Metadatum[]>;
|
||||||
|
|
||||||
constructor(route: ActivatedRoute, items: ItemDataService, metadataService: MetadataService) {
|
constructor(route: ActivatedRoute, items: ItemDataService, metadataService: MetadataService) {
|
||||||
super(route, items, metadataService);
|
super(route, items, metadataService);
|
||||||
@@ -43,7 +45,10 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit {
|
|||||||
|
|
||||||
initialize(params) {
|
initialize(params) {
|
||||||
super.initialize(params);
|
super.initialize(params);
|
||||||
this.metadata = this.item.payload.map((i) => i.metadata);
|
this.metadataObs = this.itemRDObs
|
||||||
|
.map((rd: RemoteData<Item>) => rd.payload)
|
||||||
|
.filter((item: Item) => hasValue(item))
|
||||||
|
.map((item: Item) => item.metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
<ds-metadata-field-wrapper *ngIf="(files | async)?.length > 0" [label]="label | translate">
|
<ng-container *ngVar="(bitstreamsObs | async) as bitstreams">
|
||||||
|
<ds-metadata-field-wrapper *ngIf="bitstreams?.length > 0" [label]="label | translate">
|
||||||
<div class="file-section">
|
<div class="file-section">
|
||||||
<a *ngFor="let file of (files | async); let last=last;" [href]="file?.content" [download]="file?.name">
|
<a *ngFor="let file of bitstreams; let last=last;" [href]="file?.content" [download]="file?.name">
|
||||||
<span>{{file?.name}}</span>
|
<span>{{file?.name}}</span>
|
||||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||||
<span *ngIf="!last" innerHTML="{{separator}}"></span>
|
<span *ngIf="!last" innerHTML="{{separator}}"></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</ds-metadata-field-wrapper>
|
</ds-metadata-field-wrapper>
|
||||||
|
</ng-container>
|
||||||
|
@@ -20,14 +20,14 @@ export class FileSectionComponent implements OnInit {
|
|||||||
|
|
||||||
separator = '<br/>';
|
separator = '<br/>';
|
||||||
|
|
||||||
files: Observable<Bitstream[]>;
|
bitstreamsObs: Observable<Bitstream[]>;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.initialize();
|
this.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize(): void {
|
initialize(): void {
|
||||||
this.files = this.item.getFiles();
|
this.bitstreamsObs = this.item.getFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,22 +1,22 @@
|
|||||||
<div class="container">
|
<div class="container" *ngVar="(itemRDObs | async) as itemRD">
|
||||||
<div class="item-page" *ngIf="item.hasSucceeded | async" @fadeInOut>
|
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||||
<div *ngIf="item.payload | async; let itemPayload">
|
<div *ngIf="itemRD?.payload as item">
|
||||||
<ds-item-page-title-field [item]="itemPayload"></ds-item-page-title-field>
|
<ds-item-page-title-field [item]="item"></ds-item-page-title-field>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-4">
|
<div class="col-xs-12 col-md-4">
|
||||||
<ds-metadata-field-wrapper>
|
<ds-metadata-field-wrapper>
|
||||||
<ds-thumbnail [thumbnail]="thumbnail | async"></ds-thumbnail>
|
<ds-thumbnail [thumbnail]="thumbnailObs | async"></ds-thumbnail>
|
||||||
</ds-metadata-field-wrapper>
|
</ds-metadata-field-wrapper>
|
||||||
<ds-item-page-file-section [item]="itemPayload"></ds-item-page-file-section>
|
<ds-item-page-file-section [item]="item"></ds-item-page-file-section>
|
||||||
<ds-item-page-date-field [item]="itemPayload"></ds-item-page-date-field>
|
<ds-item-page-date-field [item]="item"></ds-item-page-date-field>
|
||||||
<ds-item-page-author-field [item]="itemPayload"></ds-item-page-author-field>
|
<ds-item-page-author-field [item]="item"></ds-item-page-author-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-6">
|
<div class="col-xs-12 col-md-6">
|
||||||
<ds-item-page-abstract-field [item]="itemPayload"></ds-item-page-abstract-field>
|
<ds-item-page-abstract-field [item]="item"></ds-item-page-abstract-field>
|
||||||
<ds-item-page-uri-field [item]="itemPayload"></ds-item-page-uri-field>
|
<ds-item-page-uri-field [item]="item"></ds-item-page-uri-field>
|
||||||
<ds-item-page-collections [item]="itemPayload"></ds-item-page-collections>
|
<ds-item-page-collections [item]="item"></ds-item-page-collections>
|
||||||
<div>
|
<div>
|
||||||
<a class="btn btn-outline-primary" [routerLink]="['/items/' + itemPayload.id + '/full']">
|
<a class="btn btn-outline-primary" [routerLink]="['/items/' + item.id + '/full']">
|
||||||
{{"item.page.link.full" | translate}}
|
{{"item.page.link.full" | translate}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -24,6 +24,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="item.hasFailed | async" message="{{'error.item' | translate}}"></ds-error>
|
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
|
||||||
<ds-loading *ngIf="item.isLoading | async" message="{{'loading.item' | translate}}"></ds-loading>
|
<ds-loading *ngIf="itemRD?.isLoading" message="{{'loading.item' | translate}}"></ds-loading>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,16 +1,17 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
|
||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
|
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
|
||||||
import { MetadataService } from '../../core/metadata/metadata.service';
|
import { MetadataService } from '../../core/metadata/metadata.service';
|
||||||
|
|
||||||
import { fadeInOut } from '../../shared/animations/fade';
|
import { fadeInOut } from '../../shared/animations/fade';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a simple item page.
|
* This component renders a simple item page.
|
||||||
@@ -21,6 +22,7 @@ import { fadeInOut } from '../../shared/animations/fade';
|
|||||||
selector: 'ds-item-page',
|
selector: 'ds-item-page',
|
||||||
styleUrls: ['./item-page.component.scss'],
|
styleUrls: ['./item-page.component.scss'],
|
||||||
templateUrl: './item-page.component.html',
|
templateUrl: './item-page.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
animations: [fadeInOut]
|
animations: [fadeInOut]
|
||||||
})
|
})
|
||||||
export class ItemPageComponent implements OnInit {
|
export class ItemPageComponent implements OnInit {
|
||||||
@@ -29,9 +31,9 @@ export class ItemPageComponent implements OnInit {
|
|||||||
|
|
||||||
private sub: any;
|
private sub: any;
|
||||||
|
|
||||||
item: RemoteData<Item>;
|
itemRDObs: Observable<RemoteData<Item>>;
|
||||||
|
|
||||||
thumbnail: Observable<Bitstream>;
|
thumbnailObs: Observable<Bitstream>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@@ -49,9 +51,12 @@ export class ItemPageComponent implements OnInit {
|
|||||||
|
|
||||||
initialize(params) {
|
initialize(params) {
|
||||||
this.id = +params.id;
|
this.id = +params.id;
|
||||||
this.item = this.items.findById(params.id);
|
this.itemRDObs = this.items.findById(params.id);
|
||||||
this.metadataService.processRemoteData(this.item);
|
this.metadataService.processRemoteData(this.itemRDObs);
|
||||||
this.thumbnail = this.item.payload.flatMap((i) => i.getThumbnail());
|
this.thumbnailObs = this.itemRDObs
|
||||||
|
.map((rd: RemoteData<Item>) => rd.payload)
|
||||||
|
.filter((item: Item) => hasValue(item))
|
||||||
|
.flatMap((item: Item) => item.getThumbnail());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -6,9 +6,9 @@
|
|||||||
<div class="col-12 col-sm-9">
|
<div class="col-12 col-sm-9">
|
||||||
<ds-search-form id="search-form"
|
<ds-search-form id="search-form"
|
||||||
[query]="query"
|
[query]="query"
|
||||||
[scope]="scopeObject?.payload | async"
|
[scope]="(scopeObjectRDObs | async)?.payload"
|
||||||
[currentParams]="currentParams"
|
[currentParams]="currentParams"
|
||||||
[scopes]="scopeList?.payload">
|
[scopes]="(scopeListRDObs | async)?.payload">
|
||||||
</ds-search-form>
|
</ds-search-form>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div id="search-body"
|
<div id="search-body"
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
| translate}}
|
| translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ds-search-results [searchResults]="results"
|
<ds-search-results [searchResults]="resultsRDObs | async"
|
||||||
[searchConfig]="searchOptions"></ds-search-results>
|
[searchConfig]="searchOptions"></ds-search-results>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,4 +36,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -28,7 +28,7 @@ describe('SearchPageComponent', () => {
|
|||||||
/* tslint:enable:no-empty */
|
/* tslint:enable:no-empty */
|
||||||
select: Observable.of(true)
|
select: Observable.of(true)
|
||||||
});
|
});
|
||||||
const mockResults = ['test', 'data'];
|
const mockResults = Observable.of(['test', 'data']);
|
||||||
const searchServiceStub = {
|
const searchServiceStub = {
|
||||||
search: () => mockResults
|
search: () => mockResults
|
||||||
};
|
};
|
||||||
@@ -48,8 +48,8 @@ describe('SearchPageComponent', () => {
|
|||||||
|
|
||||||
const mockCommunityList = [];
|
const mockCommunityList = [];
|
||||||
const communityDataServiceStub = {
|
const communityDataServiceStub = {
|
||||||
findAll: () => mockCommunityList,
|
findAll: () => Observable.of(mockCommunityList),
|
||||||
findById: () => new Community()
|
findById: () => Observable.of(new Community())
|
||||||
};
|
};
|
||||||
|
|
||||||
class RouterStub {
|
class RouterStub {
|
||||||
@@ -140,7 +140,7 @@ describe('SearchPageComponent', () => {
|
|||||||
|
|
||||||
(comp as any).updateSearchResults({});
|
(comp as any).updateSearchResults({});
|
||||||
|
|
||||||
expect(comp.results as any).toBe(mockResults);
|
expect(comp.resultsRDObs as any).toBe(mockResults);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,15 +1,16 @@
|
|||||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { SearchService } from './search-service/search.service';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { SearchResult } from './search-result.model';
|
|
||||||
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
|
||||||
import { SortOptions } from '../core/cache/models/sort-options.model';
|
import { SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
|
import { CommunityDataService } from '../core/data/community-data.service';
|
||||||
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { Community } from '../core/shared/community.model';
|
||||||
|
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||||
|
import { isNotEmpty } from '../shared/empty.util';
|
||||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||||
import { SearchOptions } from './search-options.model';
|
import { SearchOptions } from './search-options.model';
|
||||||
import { CommunityDataService } from '../core/data/community-data.service';
|
import { SearchResult } from './search-result.model';
|
||||||
import { isNotEmpty } from '../shared/empty.util';
|
import { SearchService } from './search-service/search.service';
|
||||||
import { Community } from '../core/shared/community.model';
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { pushInOut } from '../shared/animations/push';
|
import { pushInOut } from '../shared/animations/push';
|
||||||
import { HostWindowService } from '../shared/host-window.service';
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
@@ -25,6 +26,7 @@ import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
|||||||
selector: 'ds-search-page',
|
selector: 'ds-search-page',
|
||||||
styleUrls: ['./search-page.component.scss'],
|
styleUrls: ['./search-page.component.scss'],
|
||||||
templateUrl: './search-page.component.html',
|
templateUrl: './search-page.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
animations: [pushInOut]
|
animations: [pushInOut]
|
||||||
})
|
})
|
||||||
export class SearchPageComponent implements OnInit, OnDestroy {
|
export class SearchPageComponent implements OnInit, OnDestroy {
|
||||||
@@ -33,11 +35,11 @@ export class SearchPageComponent implements OnInit, OnDestroy {
|
|||||||
private scope: string;
|
private scope: string;
|
||||||
|
|
||||||
query: string;
|
query: string;
|
||||||
scopeObject: RemoteData<DSpaceObject>;
|
scopeObjectRDObs: Observable<RemoteData<DSpaceObject>>;
|
||||||
results: RemoteData<Array<SearchResult<DSpaceObject>>>;
|
resultsRDObs: Observable<RemoteData<Array<SearchResult<DSpaceObject>>>>;
|
||||||
currentParams = {};
|
currentParams = {};
|
||||||
searchOptions: SearchOptions;
|
searchOptions: SearchOptions;
|
||||||
scopeList: RemoteData<Community[]>;
|
scopeListRDObs: Observable<RemoteData<Community[]>>;
|
||||||
isMobileView: Observable<boolean>;
|
isMobileView: Observable<boolean>;
|
||||||
|
|
||||||
constructor(private service: SearchService,
|
constructor(private service: SearchService,
|
||||||
@@ -46,7 +48,7 @@ export class SearchPageComponent implements OnInit, OnDestroy {
|
|||||||
private sidebarService: SearchSidebarService,
|
private sidebarService: SearchSidebarService,
|
||||||
private windowService: HostWindowService) {
|
private windowService: HostWindowService) {
|
||||||
this.isMobileView = this.windowService.isXs();
|
this.isMobileView = this.windowService.isXs();
|
||||||
this.scopeList = communityService.findAll();
|
this.scopeListRDObs = communityService.findAll();
|
||||||
// Initial pagination config
|
// Initial pagination config
|
||||||
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
||||||
pagination.id = 'search-results-pagination';
|
pagination.id = 'search-results-pagination';
|
||||||
@@ -80,9 +82,9 @@ export class SearchPageComponent implements OnInit, OnDestroy {
|
|||||||
sort: sort
|
sort: sort
|
||||||
});
|
});
|
||||||
if (isNotEmpty(this.scope)) {
|
if (isNotEmpty(this.scope)) {
|
||||||
this.scopeObject = this.communityService.findById(this.scope);
|
this.scopeObjectRDObs = this.communityService.findById(this.scope);
|
||||||
} else {
|
} else {
|
||||||
this.scopeObject = undefined;
|
this.scopeObjectRDObs = Observable.of(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -90,7 +92,7 @@ export class SearchPageComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private updateSearchResults(searchOptions) {
|
private updateSearchResults(searchOptions) {
|
||||||
// Resolve search results
|
// Resolve search results
|
||||||
this.results = this.service.search(this.query, this.scope, searchOptions);
|
this.resultsRDObs = this.service.search(this.query, this.scope, searchOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
@@ -1,7 +1,11 @@
|
|||||||
<h2 *ngIf="(searchResults.payload | async)?.length > 0">{{ 'search.results.head' | translate }}</h2>
|
<div *ngIf="searchResults?.hasSucceeded" @fadeIn>
|
||||||
<ds-object-list
|
<h2 *ngIf="searchResults?.payload?.length > 0">{{ 'search.results.head' | translate }}</h2>
|
||||||
|
<ds-object-list
|
||||||
[config]="searchConfig.pagination"
|
[config]="searchConfig.pagination"
|
||||||
[sortConfig]="searchConfig.sort"
|
[sortConfig]="searchConfig.sort"
|
||||||
[objects]="searchResults"
|
[objects]="searchResults"
|
||||||
[hideGear]="false">
|
[hideGear]="false">
|
||||||
</ds-object-list>
|
</ds-object-list>
|
||||||
|
</div>
|
||||||
|
<ds-loading *ngIf="searchResults?.isLoading" message="{{'loading.search-results' | translate}}"></ds-loading>
|
||||||
|
<ds-error *ngIf="searchResults?.hasFailed" message="{{'error.search-results' | translate}}"></ds-error>
|
@@ -1,8 +1,9 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { SearchResult } from '../search-result.model';
|
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
||||||
import { SearchOptions } from '../search-options.model';
|
import { SearchOptions } from '../search-options.model';
|
||||||
|
import { SearchResult } from '../search-result.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a simple item page.
|
* This component renders a simple item page.
|
||||||
@@ -12,6 +13,10 @@ import { SearchOptions } from '../search-options.model';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-results',
|
selector: 'ds-search-results',
|
||||||
templateUrl: './search-results.component.html',
|
templateUrl: './search-results.component.html',
|
||||||
|
animations: [
|
||||||
|
fadeIn,
|
||||||
|
fadeInOut
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class SearchResultsComponent {
|
export class SearchResultsComponent {
|
||||||
@Input() searchResults: RemoteData<Array<SearchResult<DSpaceObject>>>;
|
@Input() searchResults: RemoteData<Array<SearchResult<DSpaceObject>>>;
|
||||||
|
@@ -86,7 +86,7 @@ export class SearchService implements OnDestroy {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
search(query: string, scopeId?: string, searchOptions?: SearchOptions): RemoteData<Array<SearchResult<DSpaceObject>>> {
|
search(query: string, scopeId?: string, searchOptions?: SearchOptions): Observable<RemoteData<Array<SearchResult<DSpaceObject>>>> {
|
||||||
let self = `https://dspace7.4science.it/dspace-spring-rest/api/search?query=${query}`;
|
let self = `https://dspace7.4science.it/dspace-spring-rest/api/search?query=${query}`;
|
||||||
if (hasValue(scopeId)) {
|
if (hasValue(scopeId)) {
|
||||||
self += `&scope=${scopeId}`;
|
self += `&scope=${scopeId}`;
|
||||||
@@ -104,8 +104,8 @@ export class SearchService implements OnDestroy {
|
|||||||
self += `&sortField=${searchOptions.sort.field}`;
|
self += `&sortField=${searchOptions.sort.field}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorMessage = Observable.of(undefined);
|
const errorMessage = undefined;
|
||||||
const statusCode = Observable.of('200');
|
const statusCode = '200';
|
||||||
const returningPageInfo = new PageInfo();
|
const returningPageInfo = new PageInfo();
|
||||||
|
|
||||||
if (isNotEmpty(searchOptions)) {
|
if (isNotEmpty(searchOptions)) {
|
||||||
@@ -116,19 +116,20 @@ export class SearchService implements OnDestroy {
|
|||||||
returningPageInfo.currentPage = 1;
|
returningPageInfo.currentPage = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemsRD = this.itemDataService.findAll({
|
const itemsObs = this.itemDataService.findAll({
|
||||||
scopeID: scopeId,
|
scopeID: scopeId,
|
||||||
currentPage: returningPageInfo.currentPage,
|
currentPage: returningPageInfo.currentPage,
|
||||||
elementsPerPage: returningPageInfo.elementsPerPage
|
elementsPerPage: returningPageInfo.elementsPerPage
|
||||||
});
|
});
|
||||||
|
|
||||||
const pageInfo = itemsRD.pageInfo.map((info: PageInfo) => {
|
return itemsObs
|
||||||
const totalElements = info.totalElements > 20 ? 20 : info.totalElements;
|
.filter((rd: RemoteData<Item[]>) => rd.hasSucceeded)
|
||||||
return Object.assign({}, info, { totalElements: totalElements });
|
.map((rd: RemoteData<Item[]>) => {
|
||||||
});
|
|
||||||
|
|
||||||
const payload = itemsRD.payload.map((items: Item[]) => {
|
const totalElements = rd.pageInfo.totalElements > 20 ? 20 : rd.pageInfo.totalElements;
|
||||||
return shuffle(items)
|
const pageInfo = Object.assign({}, rd.pageInfo, { totalElements: totalElements });
|
||||||
|
|
||||||
|
const payload = shuffle(rd.payload)
|
||||||
.map((item: Item, index: number) => {
|
.map((item: Item, index: number) => {
|
||||||
const mockResult: SearchResult<DSpaceObject> = new ItemSearchResult();
|
const mockResult: SearchResult<DSpaceObject> = new ItemSearchResult();
|
||||||
mockResult.dspaceObject = item;
|
mockResult.dspaceObject = item;
|
||||||
@@ -138,43 +139,51 @@ export class SearchService implements OnDestroy {
|
|||||||
mockResult.hitHighlights = new Array(highlight);
|
mockResult.hitHighlights = new Array(highlight);
|
||||||
return mockResult;
|
return mockResult;
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return new RemoteData(
|
return new RemoteData(
|
||||||
Observable.of(self),
|
self,
|
||||||
itemsRD.isRequestPending,
|
rd.isRequestPending,
|
||||||
itemsRD.isResponsePending,
|
rd.isResponsePending,
|
||||||
itemsRD.hasSucceeded,
|
rd.hasSucceeded,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
statusCode,
|
statusCode,
|
||||||
pageInfo,
|
pageInfo,
|
||||||
payload
|
payload
|
||||||
)
|
)
|
||||||
|
}).startWith(new RemoteData(
|
||||||
|
'',
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
getConfig(): RemoteData<SearchFilterConfig[]> {
|
getConfig(): Observable<RemoteData<SearchFilterConfig[]>> {
|
||||||
const requestPending = Observable.of(false);
|
const requestPending = false;
|
||||||
const responsePending = Observable.of(false);
|
const responsePending = false;
|
||||||
const isSuccessful = Observable.of(true);
|
const isSuccessful = true;
|
||||||
const errorMessage = Observable.of(undefined);
|
const errorMessage = undefined;
|
||||||
const statusCode = Observable.of('200');
|
const statusCode = '200';
|
||||||
const returningPageInfo = Observable.of(new PageInfo());
|
const returningPageInfo = new PageInfo();
|
||||||
return new RemoteData(
|
return Observable.of(new RemoteData(
|
||||||
Observable.of('https://dspace7.4science.it/dspace-spring-rest/api/search'),
|
'https://dspace7.4science.it/dspace-spring-rest/api/search',
|
||||||
requestPending,
|
requestPending,
|
||||||
responsePending,
|
responsePending,
|
||||||
isSuccessful,
|
isSuccessful,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
statusCode,
|
statusCode,
|
||||||
returningPageInfo,
|
returningPageInfo,
|
||||||
Observable.of(this.config)
|
this.config
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
getFacetValuesFor(searchFilterConfigName: string): RemoteData<FacetValue[]> {
|
getFacetValuesFor(searchFilterConfigName: string): Observable<RemoteData<FacetValue[]>> {
|
||||||
|
|
||||||
const filterConfig = this.config.find((config: SearchFilterConfig) => config.name === searchFilterConfigName);
|
const filterConfig = this.config.find((config: SearchFilterConfig) => config.name === searchFilterConfigName);
|
||||||
|
|
||||||
const values: FacetValue[] = [];
|
const values: FacetValue[] = [];
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
const value = searchFilterConfigName + ' ' + (i + 1);
|
const value = searchFilterConfigName + ' ' + (i + 1);
|
||||||
@@ -184,22 +193,22 @@ export class SearchService implements OnDestroy {
|
|||||||
search: decodeURI(this.router.url) + (this.router.url.includes('?') ? '&' : '?') + filterConfig.paramName + '=' + value
|
search: decodeURI(this.router.url) + (this.router.url.includes('?') ? '&' : '?') + filterConfig.paramName + '=' + value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const requestPending = Observable.of(false);
|
const requestPending = false;
|
||||||
const responsePending = Observable.of(false);
|
const responsePending = false;
|
||||||
const isSuccessful = Observable.of(true);
|
const isSuccessful = true;
|
||||||
const errorMessage = Observable.of(undefined);
|
const errorMessage = undefined;
|
||||||
const statusCode = Observable.of('200');
|
const statusCode = '200';
|
||||||
const returningPageInfo = Observable.of(new PageInfo());
|
const returningPageInfo = new PageInfo();
|
||||||
return new RemoteData(
|
return Observable.of(new RemoteData(
|
||||||
Observable.of('https://dspace7.4science.it/dspace-spring-rest/api/search'),
|
'https://dspace7.4science.it/dspace-spring-rest/api/search',
|
||||||
requestPending,
|
requestPending,
|
||||||
responsePending,
|
responsePending,
|
||||||
isSuccessful,
|
isSuccessful,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
statusCode,
|
statusCode,
|
||||||
returningPageInfo,
|
returningPageInfo,
|
||||||
Observable.of(values)
|
values
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
getViewMode(): Observable<ViewMode> {
|
getViewMode(): Observable<ViewMode> {
|
||||||
|
@@ -1,29 +1,29 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
|
HostListener,
|
||||||
Inject,
|
Inject,
|
||||||
ViewEncapsulation,
|
|
||||||
OnInit,
|
OnInit,
|
||||||
HostListener
|
ViewEncapsulation
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
import { TransferState } from '../modules/transfer-state/transfer-state';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { HostWindowState } from './shared/host-window.reducer';
|
|
||||||
import { HostWindowResizeAction } from './shared/host-window.actions';
|
|
||||||
import { NativeWindowRef, NativeWindowService } from './shared/window.service';
|
|
||||||
import { MetadataService } from './core/metadata/metadata.service';
|
|
||||||
|
|
||||||
import { GLOBAL_CONFIG, GlobalConfig } from '../config';
|
import { GLOBAL_CONFIG, GlobalConfig } from '../config';
|
||||||
|
|
||||||
|
import { TransferState } from '../modules/transfer-state/transfer-state';
|
||||||
|
import { MetadataService } from './core/metadata/metadata.service';
|
||||||
|
import { HostWindowResizeAction } from './shared/host-window.actions';
|
||||||
|
import { HostWindowState } from './shared/host-window.reducer';
|
||||||
|
import { NativeWindowRef, NativeWindowService } from './shared/window.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-app',
|
selector: 'ds-app',
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrls: ['./app.component.scss'],
|
styleUrls: ['./app.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.Default,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit {
|
export class AppComponent implements OnInit {
|
||||||
|
@@ -40,10 +40,7 @@ export function getBase() {
|
|||||||
|
|
||||||
export function getMetaReducers(config: GlobalConfig): Array<MetaReducer<AppState>> {
|
export function getMetaReducers(config: GlobalConfig): Array<MetaReducer<AppState>> {
|
||||||
const metaReducers: Array<MetaReducer<AppState>> = config.production ? appMetaReducers : [...appMetaReducers, storeFreeze];
|
const metaReducers: Array<MetaReducer<AppState>> = config.production ? appMetaReducers : [...appMetaReducers, storeFreeze];
|
||||||
if (config.debug) {
|
return config.debug ? [...metaReducers, ...debugMetaReducers] : metaReducers;
|
||||||
metaReducers.concat(debugMetaReducers)
|
|
||||||
}
|
|
||||||
return metaReducers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEV_MODULES: any[] = [];
|
const DEV_MODULES: any[] = [];
|
||||||
|
205
src/app/core/browse/browse.service.spec.ts
Normal file
205
src/app/core/browse/browse.service.spec.ts
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import { BrowseService } from './browse.service';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { GlobalConfig } from '../../../config';
|
||||||
|
import { hot, cold, getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
|
import { BrowseEndpointRequest } from '../data/request.models';
|
||||||
|
import { TestScheduler } from 'rxjs/Rx';
|
||||||
|
|
||||||
|
describe('BrowseService', () => {
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let service: BrowseService;
|
||||||
|
let responseCache: ResponseCacheService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
|
||||||
|
const envConfig = {} as GlobalConfig;
|
||||||
|
const browsesEndpointURL = 'https://rest.api/browses';
|
||||||
|
const browseDefinitions = [
|
||||||
|
Object.assign(new BrowseDefinition(), {
|
||||||
|
metadataBrowse: false,
|
||||||
|
sortOptions: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
metadata: 'dc.title'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dateissued',
|
||||||
|
metadata: 'dc.date.issued'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dateaccessioned',
|
||||||
|
metadata: 'dc.date.accessioned'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
defaultSortOrder: 'ASC',
|
||||||
|
type: 'browse',
|
||||||
|
metadataKeys: [
|
||||||
|
'dc.date.issued'
|
||||||
|
],
|
||||||
|
_links: {
|
||||||
|
self: 'https://rest.api/discover/browses/dateissued',
|
||||||
|
items: 'https://rest.api/discover/browses/dateissued/items'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Object.assign(new BrowseDefinition(), {
|
||||||
|
metadataBrowse: true,
|
||||||
|
sortOptions: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
metadata: 'dc.title'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dateissued',
|
||||||
|
metadata: 'dc.date.issued'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dateaccessioned',
|
||||||
|
metadata: 'dc.date.accessioned'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
defaultSortOrder: 'ASC',
|
||||||
|
type: 'browse',
|
||||||
|
metadataKeys: [
|
||||||
|
'dc.contributor.*',
|
||||||
|
'dc.creator'
|
||||||
|
],
|
||||||
|
_links: {
|
||||||
|
self: 'https://rest.api/discover/browses/author',
|
||||||
|
entries: 'https://rest.api/discover/browses/author/entries',
|
||||||
|
items: 'https://rest.api/discover/browses/author/items'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
function initMockResponseCacheService(isSuccessful: boolean) {
|
||||||
|
return jasmine.createSpyObj('responseCache', {
|
||||||
|
get: cold('b-', {
|
||||||
|
b: {
|
||||||
|
response: {
|
||||||
|
isSuccessful,
|
||||||
|
browseDefinitions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initMockRequestService() {
|
||||||
|
return jasmine.createSpyObj('requestService', ['configure']);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTestService() {
|
||||||
|
return new BrowseService(
|
||||||
|
responseCache,
|
||||||
|
requestService,
|
||||||
|
envConfig
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBrowseURLFor', () => {
|
||||||
|
|
||||||
|
describe('if getEndpoint fires', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
responseCache = initMockResponseCacheService(true);
|
||||||
|
requestService = initMockRequestService();
|
||||||
|
service = initTestService();
|
||||||
|
spyOn(service, 'getEndpoint').and
|
||||||
|
.returnValue(hot('--a-', { a: browsesEndpointURL }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the URL for the given metadatumKey and linkName', () => {
|
||||||
|
const metadatumKey = 'dc.date.issued';
|
||||||
|
const linkName = 'items';
|
||||||
|
const expectedURL = browseDefinitions[0]._links[linkName];
|
||||||
|
|
||||||
|
const result = service.getBrowseURLFor(metadatumKey, linkName);
|
||||||
|
const expected = cold('c-d-', { c: undefined, d: expectedURL });
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work when the definition uses a wildcard in the metadatumKey', () => {
|
||||||
|
const metadatumKey = 'dc.contributor.author'; // should match dc.contributor.* in the definition
|
||||||
|
const linkName = 'items';
|
||||||
|
const expectedURL = browseDefinitions[1]._links[linkName];
|
||||||
|
|
||||||
|
const result = service.getBrowseURLFor(metadatumKey, linkName);
|
||||||
|
const expected = cold('c-d-', { c: undefined, d: expectedURL });
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when the key doesn\'t match', () => {
|
||||||
|
const metadatumKey = 'dc.title'; // isn't in the definitions
|
||||||
|
const linkName = 'items';
|
||||||
|
|
||||||
|
const result = service.getBrowseURLFor(metadatumKey, linkName);
|
||||||
|
const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkName} on ${metadatumKey} isn't configured`));
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when the link doesn\'t match', () => {
|
||||||
|
const metadatumKey = 'dc.date.issued';
|
||||||
|
const linkName = 'collections'; // isn't in the definitions
|
||||||
|
|
||||||
|
const result = service.getBrowseURLFor(metadatumKey, linkName);
|
||||||
|
const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkName} on ${metadatumKey} isn't configured`));
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should configure a new BrowseEndpointRequest', () => {
|
||||||
|
const metadatumKey = 'dc.date.issued';
|
||||||
|
const linkName = 'items';
|
||||||
|
const expected = new BrowseEndpointRequest(browsesEndpointURL);
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.getBrowseURLFor(metadatumKey, linkName).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if getEndpoint doesn\'t fire', () => {
|
||||||
|
it('should return undefined', () => {
|
||||||
|
responseCache = initMockResponseCacheService(true);
|
||||||
|
requestService = initMockRequestService();
|
||||||
|
service = initTestService();
|
||||||
|
spyOn(service, 'getEndpoint').and
|
||||||
|
.returnValue(hot('----'));
|
||||||
|
|
||||||
|
const metadatumKey = 'dc.date.issued';
|
||||||
|
const linkName = 'items';
|
||||||
|
|
||||||
|
const result = service.getBrowseURLFor(metadatumKey, linkName);
|
||||||
|
const expected = cold('b---', { b: undefined });
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if the browses endpoint can\'t be retrieved', () => {
|
||||||
|
it('should throw an error', () => {
|
||||||
|
responseCache = initMockResponseCacheService(false);
|
||||||
|
requestService = initMockRequestService();
|
||||||
|
service = initTestService();
|
||||||
|
spyOn(service, 'getEndpoint').and
|
||||||
|
.returnValue(hot('--a-', { a: browsesEndpointURL }));
|
||||||
|
|
||||||
|
const metadatumKey = 'dc.date.issued';
|
||||||
|
const linkName = 'items';
|
||||||
|
|
||||||
|
const result = service.getBrowseURLFor(metadatumKey, linkName);
|
||||||
|
const expected = cold('c-#-', { c: undefined }, new Error(`Couldn't retrieve the browses endpoint`));
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
73
src/app/core/browse/browse.service.ts
Normal file
73
src/app/core/browse/browse.service.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { BrowseSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
||||||
|
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { BrowseEndpointRequest, RestRequest } from '../data/request.models';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BrowseService extends HALEndpointService {
|
||||||
|
protected linkName = 'browses';
|
||||||
|
|
||||||
|
private static toSearchKeyArray(metadatumKey: string): string[] {
|
||||||
|
const keyParts = metadatumKey.split('.');
|
||||||
|
const searchFor = [];
|
||||||
|
searchFor.push('*');
|
||||||
|
for (let i = 0; i < keyParts.length - 1; i++) {
|
||||||
|
const prevParts = keyParts.slice(0, i + 1);
|
||||||
|
const nextPart = [...prevParts, '*'].join('.');
|
||||||
|
searchFor.push(nextPart);
|
||||||
|
}
|
||||||
|
searchFor.push(metadatumKey);
|
||||||
|
return searchFor;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected responseCache: ResponseCacheService,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
getBrowseURLFor(metadatumKey: string, linkName: string): Observable<string> {
|
||||||
|
const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey);
|
||||||
|
return this.getEndpoint()
|
||||||
|
.filter((href: string) => isNotEmpty(href))
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.map((endpointURL: string) => new BrowseEndpointRequest(endpointURL))
|
||||||
|
.do((request: RestRequest) => this.requestService.configure(request))
|
||||||
|
.flatMap((request: RestRequest) => {
|
||||||
|
const [successResponse, errorResponse] = this.responseCache.get(request.href)
|
||||||
|
.map((entry: ResponseCacheEntry) => entry.response)
|
||||||
|
.partition((response: RestResponse) => response.isSuccessful);
|
||||||
|
|
||||||
|
return Observable.merge(
|
||||||
|
errorResponse.flatMap((response: ErrorResponse) =>
|
||||||
|
Observable.throw(new Error(`Couldn't retrieve the browses endpoint`))),
|
||||||
|
successResponse
|
||||||
|
.filter((response: BrowseSuccessResponse) => isNotEmpty(response.browseDefinitions))
|
||||||
|
.map((response: BrowseSuccessResponse) => response.browseDefinitions)
|
||||||
|
.map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
|
||||||
|
.find((def: BrowseDefinition) => {
|
||||||
|
const matchingKeys = def.metadataKeys.find((key: string) => searchKeyArray.indexOf(key) >= 0);
|
||||||
|
return isNotEmpty(matchingKeys);
|
||||||
|
})
|
||||||
|
).map((def: BrowseDefinition) => {
|
||||||
|
if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkName])) {
|
||||||
|
throw new Error(`A browse endpoint for ${linkName} on ${metadatumKey} isn't configured`);
|
||||||
|
} else {
|
||||||
|
return def._links[linkName];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}).startWith(undefined)
|
||||||
|
.distinctUntilChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -28,7 +28,7 @@ export class RemoteDataBuildService {
|
|||||||
buildSingle<TNormalized extends CacheableObject, TDomain>(
|
buildSingle<TNormalized extends CacheableObject, TDomain>(
|
||||||
hrefObs: string | Observable<string>,
|
hrefObs: string | Observable<string>,
|
||||||
normalizedType: GenericConstructor<TNormalized>
|
normalizedType: GenericConstructor<TNormalized>
|
||||||
): RemoteData<TDomain> {
|
): Observable<RemoteData<TDomain>> {
|
||||||
if (typeof hrefObs === 'string') {
|
if (typeof hrefObs === 'string') {
|
||||||
hrefObs = Observable.of(hrefObs);
|
hrefObs = Observable.of(hrefObs);
|
||||||
}
|
}
|
||||||
@@ -49,46 +49,8 @@ export class RemoteDataBuildService {
|
|||||||
requestHrefObs.flatMap((requestHref) => this.responseCache.get(requestHref)).filter((entry) => hasValue(entry))
|
requestHrefObs.flatMap((requestHref) => this.responseCache.get(requestHref)).filter((entry) => hasValue(entry))
|
||||||
);
|
);
|
||||||
|
|
||||||
const requestPending = requestObs
|
|
||||||
.map((entry: RequestEntry) => entry.requestPending)
|
|
||||||
.startWith(true)
|
|
||||||
.distinctUntilChanged();
|
|
||||||
|
|
||||||
const responsePending = requestObs
|
|
||||||
.map((entry: RequestEntry) => entry.responsePending)
|
|
||||||
.startWith(false)
|
|
||||||
.distinctUntilChanged();
|
|
||||||
|
|
||||||
const isSuccessFul = responseCacheObs
|
|
||||||
.map((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
|
||||||
.startWith(false)
|
|
||||||
.distinctUntilChanged();
|
|
||||||
|
|
||||||
const errorMessage = responseCacheObs
|
|
||||||
.filter((entry: ResponseCacheEntry) => !entry.response.isSuccessful)
|
|
||||||
.map((entry: ResponseCacheEntry) => (entry.response as ErrorResponse).errorMessage)
|
|
||||||
.distinctUntilChanged();
|
|
||||||
|
|
||||||
const statusCode = responseCacheObs
|
|
||||||
.map((entry: ResponseCacheEntry) => entry.response.statusCode)
|
|
||||||
.distinctUntilChanged();
|
|
||||||
|
|
||||||
/* tslint:disable:no-string-literal */
|
|
||||||
const pageInfo = responseCacheObs
|
|
||||||
.filter((entry: ResponseCacheEntry) => hasValue(entry.response) && hasValue(entry.response['pageInfo']))
|
|
||||||
.map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).pageInfo)
|
|
||||||
.map((pInfo: PageInfo) => {
|
|
||||||
if (isNotEmpty(pageInfo) && pInfo.currentPage >= 0) {
|
|
||||||
return Object.assign({}, pInfo, {currentPage: pInfo.currentPage + 1});
|
|
||||||
} else {
|
|
||||||
return pInfo;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.distinctUntilChanged();
|
|
||||||
/* tslint:enable:no-string-literal */
|
|
||||||
|
|
||||||
// always use self link if that is cached, only if it isn't, get it via the response.
|
// always use self link if that is cached, only if it isn't, get it via the response.
|
||||||
const payload =
|
const payloadObs =
|
||||||
Observable.combineLatest(
|
Observable.combineLatest(
|
||||||
hrefObs.flatMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href, normalizedType))
|
hrefObs.flatMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href, normalizedType))
|
||||||
.startWith(undefined),
|
.startWith(undefined),
|
||||||
@@ -114,10 +76,38 @@ export class RemoteDataBuildService {
|
|||||||
).filter((normalized) => hasValue(normalized))
|
).filter((normalized) => hasValue(normalized))
|
||||||
.map((normalized: TNormalized) => {
|
.map((normalized: TNormalized) => {
|
||||||
return this.build<TNormalized, TDomain>(normalized);
|
return this.build<TNormalized, TDomain>(normalized);
|
||||||
}).distinctUntilChanged();
|
})
|
||||||
|
.startWith(undefined)
|
||||||
|
.distinctUntilChanged();
|
||||||
|
return this.toRemoteDataObservable(hrefObs, requestObs, responseCacheObs, payloadObs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private toRemoteDataObservable<T>(hrefObs: Observable<string>, requestObs: Observable<RequestEntry>, responseCacheObs: Observable<ResponseCacheEntry>, payloadObs: Observable<T>) {
|
||||||
|
return Observable.combineLatest(hrefObs, requestObs, responseCacheObs.startWith(undefined), payloadObs,
|
||||||
|
(href: string, reqEntry: RequestEntry, resEntry: ResponseCacheEntry, payload: T) => {
|
||||||
|
const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true;
|
||||||
|
const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false;
|
||||||
|
let isSuccessFul: boolean;
|
||||||
|
let errorMessage: string;
|
||||||
|
let statusCode: string;
|
||||||
|
let pageInfo: PageInfo;
|
||||||
|
if (hasValue(resEntry) && hasValue(resEntry.response)) {
|
||||||
|
isSuccessFul = resEntry.response.isSuccessful;
|
||||||
|
errorMessage = isSuccessFul === false ? (resEntry.response as ErrorResponse).errorMessage : undefined;
|
||||||
|
statusCode = resEntry.response.statusCode;
|
||||||
|
|
||||||
|
if (hasValue((resEntry.response as DSOSuccessResponse).pageInfo)) {
|
||||||
|
const resPageInfo = (resEntry.response as DSOSuccessResponse).pageInfo;
|
||||||
|
if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) {
|
||||||
|
pageInfo = Object.assign({}, resPageInfo, { currentPage: resPageInfo.currentPage + 1 });
|
||||||
|
} else {
|
||||||
|
pageInfo = resPageInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new RemoteData(
|
return new RemoteData(
|
||||||
hrefObs,
|
href,
|
||||||
requestPending,
|
requestPending,
|
||||||
responsePending,
|
responsePending,
|
||||||
isSuccessFul,
|
isSuccessFul,
|
||||||
@@ -126,12 +116,13 @@ export class RemoteDataBuildService {
|
|||||||
pageInfo,
|
pageInfo,
|
||||||
payload
|
payload
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
buildList<TNormalized extends CacheableObject, TDomain>(
|
buildList<TNormalized extends CacheableObject, TDomain>(
|
||||||
hrefObs: string | Observable<string>,
|
hrefObs: string | Observable<string>,
|
||||||
normalizedType: GenericConstructor<TNormalized>
|
normalizedType: GenericConstructor<TNormalized>
|
||||||
): RemoteData<TDomain[]> {
|
): Observable<RemoteData<TDomain[]>> {
|
||||||
if (typeof hrefObs === 'string') {
|
if (typeof hrefObs === 'string') {
|
||||||
hrefObs = Observable.of(hrefObs);
|
hrefObs = Observable.of(hrefObs);
|
||||||
}
|
}
|
||||||
@@ -141,38 +132,7 @@ export class RemoteDataBuildService {
|
|||||||
const responseCacheObs = hrefObs.flatMap((href: string) => this.responseCache.get(href))
|
const responseCacheObs = hrefObs.flatMap((href: string) => this.responseCache.get(href))
|
||||||
.filter((entry) => hasValue(entry));
|
.filter((entry) => hasValue(entry));
|
||||||
|
|
||||||
const requestPending = requestObs
|
const payloadObs = responseCacheObs
|
||||||
.map((entry: RequestEntry) => entry.requestPending)
|
|
||||||
.startWith(true)
|
|
||||||
.distinctUntilChanged();
|
|
||||||
|
|
||||||
const responsePending = requestObs
|
|
||||||
.map((entry: RequestEntry) => entry.responsePending)
|
|
||||||
.startWith(false)
|
|
||||||
.distinctUntilChanged();
|
|
||||||
|
|
||||||
const isSuccessFul = responseCacheObs
|
|
||||||
.map((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
|
||||||
.startWith(false)
|
|
||||||
.distinctUntilChanged();
|
|
||||||
|
|
||||||
const errorMessage = responseCacheObs
|
|
||||||
.filter((entry: ResponseCacheEntry) => !entry.response.isSuccessful)
|
|
||||||
.map((entry: ResponseCacheEntry) => (entry.response as ErrorResponse).errorMessage)
|
|
||||||
.distinctUntilChanged();
|
|
||||||
|
|
||||||
const statusCode = responseCacheObs
|
|
||||||
.map((entry: ResponseCacheEntry) => entry.response.statusCode)
|
|
||||||
.distinctUntilChanged();
|
|
||||||
|
|
||||||
/* tslint:disable:no-string-literal */
|
|
||||||
const pageInfo = responseCacheObs
|
|
||||||
.filter((entry: ResponseCacheEntry) => hasValue(entry.response) && hasValue(entry.response['pageInfo']))
|
|
||||||
.map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).pageInfo)
|
|
||||||
.distinctUntilChanged();
|
|
||||||
/* tslint:enable:no-string-literal */
|
|
||||||
|
|
||||||
const payload = responseCacheObs
|
|
||||||
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
||||||
.map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks)
|
.map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks)
|
||||||
.flatMap((resourceUUIDs: string[]) => {
|
.flatMap((resourceUUIDs: string[]) => {
|
||||||
@@ -183,18 +143,10 @@ export class RemoteDataBuildService {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
.startWith([])
|
||||||
.distinctUntilChanged();
|
.distinctUntilChanged();
|
||||||
|
|
||||||
return new RemoteData(
|
return this.toRemoteDataObservable(hrefObs, requestObs, responseCacheObs, payloadObs);
|
||||||
hrefObs,
|
|
||||||
requestPending,
|
|
||||||
responsePending,
|
|
||||||
isSuccessFul,
|
|
||||||
errorMessage,
|
|
||||||
statusCode,
|
|
||||||
pageInfo,
|
|
||||||
payload
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
build<TNormalized extends CacheableObject, TDomain>(normalized: TNormalized): TDomain {
|
build<TNormalized extends CacheableObject, TDomain>(normalized: TNormalized): TDomain {
|
||||||
@@ -207,13 +159,9 @@ export class RemoteDataBuildService {
|
|||||||
const { resourceType, isList } = getRelationMetadata(normalized, relationship);
|
const { resourceType, isList } = getRelationMetadata(normalized, relationship);
|
||||||
const resourceConstructor = NormalizedObjectFactory.getConstructor(resourceType);
|
const resourceConstructor = NormalizedObjectFactory.getConstructor(resourceType);
|
||||||
if (Array.isArray(normalized[relationship])) {
|
if (Array.isArray(normalized[relationship])) {
|
||||||
// without the setTimeout, the actions inside requestService.configure
|
|
||||||
// are dispatched, but sometimes don't arrive. I'm unsure why atm.
|
|
||||||
setTimeout(() => {
|
|
||||||
normalized[relationship].forEach((href: string) => {
|
normalized[relationship].forEach((href: string) => {
|
||||||
this.requestService.configure(new RestRequest(href))
|
this.requestService.configure(new RestRequest(href))
|
||||||
});
|
});
|
||||||
}, 0);
|
|
||||||
|
|
||||||
const rdArr = [];
|
const rdArr = [];
|
||||||
normalized[relationship].forEach((href: string) => {
|
normalized[relationship].forEach((href: string) => {
|
||||||
@@ -226,11 +174,7 @@ export class RemoteDataBuildService {
|
|||||||
links[relationship] = rdArr[0];
|
links[relationship] = rdArr[0];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// without the setTimeout, the actions inside requestService.configure
|
|
||||||
// are dispatched, but sometimes don't arrive. I'm unsure why atm.
|
|
||||||
setTimeout(() => {
|
|
||||||
this.requestService.configure(new RestRequest(normalized[relationship]));
|
this.requestService.configure(new RestRequest(normalized[relationship]));
|
||||||
}, 0);
|
|
||||||
|
|
||||||
// The rest API can return a single URL to represent a list of resources (e.g. /items/:id/bitstreams)
|
// The rest API can return a single URL to represent a list of resources (e.g. /items/:id/bitstreams)
|
||||||
// in that case only 1 href will be stored in the normalized obj (so the isArray above fails),
|
// in that case only 1 href will be stored in the normalized obj (so the isArray above fails),
|
||||||
@@ -248,55 +192,46 @@ export class RemoteDataBuildService {
|
|||||||
return Object.assign(new domainModel(), normalized, links);
|
return Object.assign(new domainModel(), normalized, links);
|
||||||
}
|
}
|
||||||
|
|
||||||
aggregate<T>(input: Array<RemoteData<T>>): RemoteData<T[]> {
|
aggregate<T>(input: Array<Observable<RemoteData<T>>>): Observable<RemoteData<T[]>> {
|
||||||
const requestPending = Observable.combineLatest(
|
return Observable.combineLatest(
|
||||||
...input.map((rd) => rd.isRequestPending),
|
...input,
|
||||||
).map((...pendingArray) => pendingArray.every((e) => e === true))
|
(...arr: Array<RemoteData<T>>) => {
|
||||||
.distinctUntilChanged();
|
const requestPending: boolean = arr
|
||||||
|
.map((d: RemoteData<T>) => d.isRequestPending)
|
||||||
|
.every((b: boolean) => b === true);
|
||||||
|
|
||||||
const responsePending = Observable.combineLatest(
|
const responsePending: boolean = arr
|
||||||
...input.map((rd) => rd.isResponsePending),
|
.map((d: RemoteData<T>) => d.isResponsePending)
|
||||||
).map((...pendingArray) => pendingArray.every((e) => e === true))
|
.every((b: boolean) => b === true);
|
||||||
.distinctUntilChanged();
|
|
||||||
|
|
||||||
const isSuccessFul = Observable.combineLatest(
|
const isSuccessFul: boolean = arr
|
||||||
...input.map((rd) => rd.hasSucceeded),
|
.map((d: RemoteData<T>) => d.hasSucceeded)
|
||||||
).map((...successArray) => successArray.every((e) => e === true))
|
.every((b: boolean) => b === true);
|
||||||
.distinctUntilChanged();
|
|
||||||
|
|
||||||
const errorMessage = Observable.combineLatest(
|
const errorMessage: string = arr
|
||||||
...input.map((rd) => rd.errorMessage),
|
.map((d: RemoteData<T>) => d.errorMessage)
|
||||||
).map((...errors) => errors
|
.map((e: string, idx: number) => {
|
||||||
.map((e, idx) => {
|
|
||||||
if (hasValue(e)) {
|
if (hasValue(e)) {
|
||||||
return `[${idx}]: ${e}`;
|
return `[${idx}]: ${e}`;
|
||||||
}
|
}
|
||||||
})
|
}).filter((e: string) => hasValue(e))
|
||||||
.filter((e) => hasValue(e))
|
.join(', ');
|
||||||
.join(', ')
|
|
||||||
);
|
|
||||||
|
|
||||||
const statusCode = Observable.combineLatest(
|
const statusCode: string = arr
|
||||||
...input.map((rd) => rd.statusCode),
|
.map((d: RemoteData<T>) => d.statusCode)
|
||||||
).map((...statusCodes) => statusCodes
|
.map((c: string, idx: number) => {
|
||||||
.map((code, idx) => {
|
if (hasValue(c)) {
|
||||||
if (hasValue(code)) {
|
return `[${idx}]: ${c}`;
|
||||||
return `[${idx}]: ${code}`;
|
|
||||||
}
|
}
|
||||||
})
|
}).filter((c: string) => hasValue(c))
|
||||||
.filter((c) => hasValue(c))
|
.join(', ');
|
||||||
.join(', ')
|
|
||||||
);
|
|
||||||
|
|
||||||
const pageInfo = Observable.of(undefined);
|
const pageInfo = undefined;
|
||||||
|
|
||||||
const payload = Observable.combineLatest(...input.map((rd) => rd.payload)) as Observable<T[]>;
|
const payload: T[] = arr.map((d: RemoteData<T>) => d.payload);
|
||||||
|
|
||||||
return new RemoteData(
|
return new RemoteData(
|
||||||
// This is an aggregated object, it doesn't necessarily correspond
|
`dspace-angular://aggregated/object/${new Date().getTime()}`,
|
||||||
// to a single REST endpoint, so instead of a self link, use the
|
|
||||||
// current time in ms for a somewhat unique id
|
|
||||||
Observable.of(`${new Date().getTime()}`),
|
|
||||||
requestPending,
|
requestPending,
|
||||||
responsePending,
|
responsePending,
|
||||||
isSuccessFul,
|
isSuccessFul,
|
||||||
@@ -305,6 +240,7 @@ export class RemoteDataBuildService {
|
|||||||
pageInfo,
|
pageInfo,
|
||||||
payload
|
payload
|
||||||
);
|
);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -66,4 +66,14 @@ export abstract class NormalizedDSpaceObject extends NormalizedObject {
|
|||||||
@autoserialize
|
@autoserialize
|
||||||
owner: string;
|
owner: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The links to all related resources returned by the rest api.
|
||||||
|
*
|
||||||
|
* Repeated here to make the serialization work,
|
||||||
|
* inheritSerialization doesn't seem to work for more than one level
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
_links: {
|
||||||
|
[name: string]: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,4 +17,8 @@ export abstract class NormalizedObject implements CacheableObject {
|
|||||||
@autoserialize
|
@autoserialize
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
_links: {
|
||||||
|
[name: string]: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
10
src/app/core/cache/response-cache.models.ts
vendored
10
src/app/core/cache/response-cache.models.ts
vendored
@@ -1,5 +1,6 @@
|
|||||||
import { RequestError } from '../data/request.models';
|
import { RequestError } from '../data/request.models';
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
export class RestResponse {
|
export class RestResponse {
|
||||||
@@ -32,6 +33,15 @@ export class RootSuccessResponse extends RestResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class BrowseSuccessResponse extends RestResponse {
|
||||||
|
constructor(
|
||||||
|
public browseDefinitions: BrowseDefinition[],
|
||||||
|
public statusCode: string
|
||||||
|
) {
|
||||||
|
super(true, statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class ErrorResponse extends RestResponse {
|
export class ErrorResponse extends RestResponse {
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
|
|
||||||
|
2
src/app/core/cache/response-cache.service.ts
vendored
2
src/app/core/cache/response-cache.service.ts
vendored
@@ -4,7 +4,7 @@ import { MemoizedSelector, Store } from '@ngrx/store';
|
|||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
import { ResponseCacheEntry } from './response-cache.reducer';
|
import { ResponseCacheEntry } from './response-cache.reducer';
|
||||||
import { hasNoValue } from '../../shared/empty.util';
|
import { hasNoValue, hasValue } from '../../shared/empty.util';
|
||||||
import { ResponseCacheRemoveAction, ResponseCacheAddAction } from './response-cache.actions';
|
import { ResponseCacheRemoveAction, ResponseCacheAddAction } from './response-cache.actions';
|
||||||
import { RestResponse } from './response-cache.models';
|
import { RestResponse } from './response-cache.models';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
|
@@ -30,6 +30,8 @@ import { ResponseCacheService } from './cache/response-cache.service';
|
|||||||
import { RootResponseParsingService } from './data/root-response-parsing.service';
|
import { RootResponseParsingService } from './data/root-response-parsing.service';
|
||||||
import { ServerResponseService } from '../shared/server-response.service';
|
import { ServerResponseService } from '../shared/server-response.service';
|
||||||
import { NativeWindowFactory, NativeWindowService } from '../shared/window.service';
|
import { NativeWindowFactory, NativeWindowService } from '../shared/window.service';
|
||||||
|
import { BrowseService } from './browse/browse.service';
|
||||||
|
import { BrowseResponseParsingService } from './data/browse-response-parsing.service';
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -61,6 +63,8 @@ const PROVIDERS = [
|
|||||||
ResponseCacheService,
|
ResponseCacheService,
|
||||||
RootResponseParsingService,
|
RootResponseParsingService,
|
||||||
ServerResponseService,
|
ServerResponseService,
|
||||||
|
BrowseResponseParsingService,
|
||||||
|
BrowseService,
|
||||||
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
|
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
164
src/app/core/data/browse-response-parsing.service.spec.ts
Normal file
164
src/app/core/data/browse-response-parsing.service.spec.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { BrowseResponseParsingService } from './browse-response-parsing.service';
|
||||||
|
import { BrowseEndpointRequest } from './request.models';
|
||||||
|
import { BrowseSuccessResponse, ErrorResponse } from '../cache/response-cache.models';
|
||||||
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
|
|
||||||
|
describe('BrowseResponseParsingService', () => {
|
||||||
|
let service: BrowseResponseParsingService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new BrowseResponseParsingService();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parse', () => {
|
||||||
|
const validRequest = new BrowseEndpointRequest('https://rest.api/discover/browses');
|
||||||
|
|
||||||
|
const validResponse = {
|
||||||
|
payload: {
|
||||||
|
_embedded: {
|
||||||
|
browses: [{
|
||||||
|
metadataBrowse: false,
|
||||||
|
sortOptions: [{ name: 'title', metadata: 'dc.title' }, {
|
||||||
|
name: 'dateissued',
|
||||||
|
metadata: 'dc.date.issued'
|
||||||
|
}, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }],
|
||||||
|
order: 'ASC',
|
||||||
|
type: 'browse',
|
||||||
|
metadata: ['dc.date.issued'],
|
||||||
|
_links: {
|
||||||
|
self: { href: 'https://rest.api/discover/browses/dateissued' },
|
||||||
|
items: { href: 'https://rest.api/discover/browses/dateissued/items' }
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
metadataBrowse: true,
|
||||||
|
sortOptions: [{ name: 'title', metadata: 'dc.title' }, {
|
||||||
|
name: 'dateissued',
|
||||||
|
metadata: 'dc.date.issued'
|
||||||
|
}, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }],
|
||||||
|
order: 'ASC',
|
||||||
|
type: 'browse',
|
||||||
|
metadata: ['dc.contributor.*', 'dc.creator'],
|
||||||
|
_links: {
|
||||||
|
self: { href: 'https://rest.api/discover/browses/author' },
|
||||||
|
entries: { href: 'https://rest.api/discover/browses/author/entries' },
|
||||||
|
items: { href: 'https://rest.api/discover/browses/author/items' }
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
_links: { self: { href: 'https://rest.api/discover/browses' } },
|
||||||
|
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
||||||
|
}, statusCode: '200'
|
||||||
|
};
|
||||||
|
|
||||||
|
const invalidResponse1 = {
|
||||||
|
payload: {
|
||||||
|
_embedded: {
|
||||||
|
browse: {
|
||||||
|
metadataBrowse: false,
|
||||||
|
sortOptions: [{ name: 'title', metadata: 'dc.title' }, {
|
||||||
|
name: 'dateissued',
|
||||||
|
metadata: 'dc.date.issued'
|
||||||
|
}, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }],
|
||||||
|
order: 'ASC',
|
||||||
|
type: 'browse',
|
||||||
|
metadata: ['dc.date.issued'],
|
||||||
|
_links: {
|
||||||
|
self: { href: 'https://rest.api/discover/browses/dateissued' },
|
||||||
|
items: { href: 'https://rest.api/discover/browses/dateissued/items' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_links: { self: { href: 'https://rest.api/discover/browses' } },
|
||||||
|
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
||||||
|
}, statusCode: '200'
|
||||||
|
};
|
||||||
|
|
||||||
|
const invalidResponse2 = {
|
||||||
|
payload: {
|
||||||
|
browses: [{}, {}],
|
||||||
|
_links: { self: { href: 'https://rest.api/discover/browses' } },
|
||||||
|
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
||||||
|
}, statusCode: '200'
|
||||||
|
};
|
||||||
|
|
||||||
|
const invalidResponse3 = {
|
||||||
|
payload: {
|
||||||
|
_links: { self: { href: 'https://rest.api/discover/browses' } },
|
||||||
|
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
||||||
|
}, statusCode: '500'
|
||||||
|
};
|
||||||
|
|
||||||
|
const definitions = [
|
||||||
|
Object.assign(new BrowseDefinition(), {
|
||||||
|
metadataBrowse: false,
|
||||||
|
sortOptions: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
metadata: 'dc.title'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dateissued',
|
||||||
|
metadata: 'dc.date.issued'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dateaccessioned',
|
||||||
|
metadata: 'dc.date.accessioned'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
defaultSortOrder: 'ASC',
|
||||||
|
type: 'browse',
|
||||||
|
metadataKeys: [
|
||||||
|
'dc.date.issued'
|
||||||
|
],
|
||||||
|
_links: { }
|
||||||
|
}),
|
||||||
|
Object.assign(new BrowseDefinition(), {
|
||||||
|
metadataBrowse: true,
|
||||||
|
sortOptions: [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
metadata: 'dc.title'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dateissued',
|
||||||
|
metadata: 'dc.date.issued'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dateaccessioned',
|
||||||
|
metadata: 'dc.date.accessioned'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
defaultSortOrder: 'ASC',
|
||||||
|
type: 'browse',
|
||||||
|
metadataKeys: [
|
||||||
|
'dc.contributor.*',
|
||||||
|
'dc.creator'
|
||||||
|
],
|
||||||
|
_links: { }
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
it('should return a BrowseSuccessResponse if data contains a valid browse endpoint response', () => {
|
||||||
|
const response = service.parse(validRequest, validResponse);
|
||||||
|
expect(response.constructor).toBe(BrowseSuccessResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an ErrorResponse if data contains an invalid browse endpoint response', () => {
|
||||||
|
const response1 = service.parse(validRequest, invalidResponse1);
|
||||||
|
const response2 = service.parse(validRequest, invalidResponse2);
|
||||||
|
expect(response1.constructor).toBe(ErrorResponse);
|
||||||
|
expect(response2.constructor).toBe(ErrorResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an ErrorResponse if data contains a statuscode other than 200', () => {
|
||||||
|
const response = service.parse(validRequest, invalidResponse3);
|
||||||
|
expect(response.constructor).toBe(ErrorResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a BrowseSuccessResponse with the BrowseDefinitions in data', () => {
|
||||||
|
const response = service.parse(validRequest, validResponse);
|
||||||
|
expect((response as BrowseSuccessResponse).browseDefinitions).toEqual(definitions);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
28
src/app/core/data/browse-response-parsing.service.ts
Normal file
28
src/app/core/data/browse-response-parsing.service.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { RestRequest } from './request.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { BrowseSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BrowseResponseParsingService implements ResponseParsingService {
|
||||||
|
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._embedded)
|
||||||
|
&& Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
|
||||||
|
const serializer = new DSpaceRESTv2Serializer(BrowseDefinition);
|
||||||
|
const browseDefinitions = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
|
||||||
|
return new BrowseSuccessResponse(browseDefinitions, data.statusCode);
|
||||||
|
} else {
|
||||||
|
return new ErrorResponse(
|
||||||
|
Object.assign(
|
||||||
|
new Error('Unexpected response from browse endpoint'),
|
||||||
|
{ statusText: data.statusCode }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,28 +1,29 @@
|
|||||||
import { Inject, Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
import { DataService } from './data.service';
|
|
||||||
import { Collection } from '../shared/collection.model';
|
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
|
||||||
import { NormalizedCollection } from '../cache/models/normalized-collection.model';
|
|
||||||
import { CoreState } from '../core.reducers';
|
|
||||||
import { RequestService } from './request.service';
|
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
|
||||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { NormalizedCollection } from '../cache/models/normalized-collection.model';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { Collection } from '../shared/collection.model';
|
||||||
|
import { ComColDataService } from './comcol-data.service';
|
||||||
|
import { CommunityDataService } from './community-data.service';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CollectionDataService extends DataService<NormalizedCollection, Collection> {
|
export class CollectionDataService extends ComColDataService<NormalizedCollection, Collection> {
|
||||||
protected linkName = 'collections';
|
protected linkName = 'collections';
|
||||||
protected browseEndpoint = '/discover/browses/dateissued/collections';
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
protected responseCache: ResponseCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
@Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
|
protected cds: CommunityDataService,
|
||||||
|
protected objectCache: ObjectCacheService
|
||||||
) {
|
) {
|
||||||
super(NormalizedCollection, EnvConfig);
|
super(NormalizedCollection);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
181
src/app/core/data/comcol-data.service.spec.ts
Normal file
181
src/app/core/data/comcol-data.service.spec.ts
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
|
import { TestScheduler } from 'rxjs/Rx';
|
||||||
|
import { GlobalConfig } from '../../../config';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { NormalizedCommunity } from '../cache/models/normalized-community.model';
|
||||||
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { ComColDataService } from './comcol-data.service';
|
||||||
|
import { CommunityDataService } from './community-data.service';
|
||||||
|
import { FindByIDRequest } from './request.models';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
|
||||||
|
const LINK_NAME = 'test';
|
||||||
|
|
||||||
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
class NormalizedTestObject implements CacheableObject {
|
||||||
|
self: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestService extends ComColDataService<NormalizedTestObject, any> {
|
||||||
|
protected linkName = LINK_NAME;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected responseCache: ResponseCacheService,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected EnvConfig: GlobalConfig,
|
||||||
|
protected cds: CommunityDataService,
|
||||||
|
protected objectCache: ObjectCacheService
|
||||||
|
) {
|
||||||
|
super(NormalizedTestObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
|
||||||
|
describe('ComColDataService', () => {
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let service: TestService;
|
||||||
|
let responseCache: ResponseCacheService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
let cds: CommunityDataService;
|
||||||
|
let objectCache: ObjectCacheService;
|
||||||
|
|
||||||
|
const rdbService = {} as RemoteDataBuildService;
|
||||||
|
const store = {} as Store<CoreState>;
|
||||||
|
const EnvConfig = {} as GlobalConfig;
|
||||||
|
|
||||||
|
const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d';
|
||||||
|
const communitiesEndpoint = 'https://rest.api/core/communities';
|
||||||
|
const communityEndpoint = `${communitiesEndpoint}/${scopeID}`;
|
||||||
|
const scopedEndpoint = `${communityEndpoint}/${LINK_NAME}`;
|
||||||
|
const serviceEndpoint = `https://rest.api/core/${LINK_NAME}`;
|
||||||
|
|
||||||
|
function initMockCommunityDataService(): CommunityDataService {
|
||||||
|
return jasmine.createSpyObj('responseCache', {
|
||||||
|
getEndpoint: hot('--a-', { a: communitiesEndpoint }),
|
||||||
|
getFindByIDHref: cold('b-', { b: communityEndpoint })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initMockRequestService(): RequestService {
|
||||||
|
return jasmine.createSpyObj('requestService', ['configure']);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initMockResponceCacheService(isSuccessful: boolean): ResponseCacheService {
|
||||||
|
return jasmine.createSpyObj('responseCache', {
|
||||||
|
get: cold('c-', {
|
||||||
|
c: { response: { isSuccessful } }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initMockObjectCacheService(): ObjectCacheService {
|
||||||
|
return jasmine.createSpyObj('objectCache', {
|
||||||
|
getByUUID: cold('d-', {
|
||||||
|
d: {
|
||||||
|
_links: {
|
||||||
|
[LINK_NAME]: scopedEndpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTestService(): TestService {
|
||||||
|
return new TestService(
|
||||||
|
responseCache,
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
store,
|
||||||
|
EnvConfig,
|
||||||
|
cds,
|
||||||
|
objectCache
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('getScopedEndpoint', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should configure a new FindByIDRequest for the scope Community', () => {
|
||||||
|
cds = initMockCommunityDataService();
|
||||||
|
requestService = initMockRequestService();
|
||||||
|
objectCache = initMockObjectCacheService();
|
||||||
|
responseCache = initMockResponceCacheService(true);
|
||||||
|
service = initTestService();
|
||||||
|
|
||||||
|
const expected = new FindByIDRequest(communityEndpoint, scopeID);
|
||||||
|
|
||||||
|
scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if the scope Community can be found', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cds = initMockCommunityDataService();
|
||||||
|
requestService = initMockRequestService();
|
||||||
|
objectCache = initMockObjectCacheService();
|
||||||
|
responseCache = initMockResponceCacheService(true);
|
||||||
|
service = initTestService();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch the scope Community from the cache', () => {
|
||||||
|
scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
expect(objectCache.getByUUID).toHaveBeenCalledWith(scopeID, NormalizedCommunity);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the endpoint to fetch resources within the given scope', () => {
|
||||||
|
const result = service.getScopedEndpoint(scopeID);
|
||||||
|
const expected = cold('--e-', { e: scopedEndpoint });
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if the scope Community can\'t be found', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cds = initMockCommunityDataService();
|
||||||
|
requestService = initMockRequestService();
|
||||||
|
objectCache = initMockObjectCacheService();
|
||||||
|
responseCache = initMockResponceCacheService(false);
|
||||||
|
service = initTestService();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error', () => {
|
||||||
|
const result = service.getScopedEndpoint(scopeID);
|
||||||
|
const expected = cold('--#-', undefined, new Error(`The Community with scope ${scopeID} couldn't be retrieved`));
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if the scope is not specified', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cds = initMockCommunityDataService();
|
||||||
|
requestService = initMockRequestService();
|
||||||
|
objectCache = initMockObjectCacheService();
|
||||||
|
responseCache = initMockResponceCacheService(true);
|
||||||
|
service = initTestService();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return this.getEndpoint()', () => {
|
||||||
|
spyOn(service, 'getEndpoint').and.returnValue(cold('--e-', { e: serviceEndpoint }))
|
||||||
|
|
||||||
|
const result = service.getScopedEndpoint(undefined);
|
||||||
|
const expected = cold('--f-', { f: serviceEndpoint });
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
56
src/app/core/data/comcol-data.service.ts
Normal file
56
src/app/core/data/comcol-data.service.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { NormalizedCommunity } from '../cache/models/normalized-community.model';
|
||||||
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { DSOSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
||||||
|
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||||
|
import { CommunityDataService } from './community-data.service';
|
||||||
|
|
||||||
|
import { DataService } from './data.service';
|
||||||
|
import { FindByIDRequest } from './request.models';
|
||||||
|
|
||||||
|
export abstract class ComColDataService<TNormalized extends CacheableObject, TDomain> extends DataService<TNormalized, TDomain> {
|
||||||
|
protected abstract cds: CommunityDataService;
|
||||||
|
protected abstract objectCache: ObjectCacheService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the scoped endpoint URL by fetching the object with
|
||||||
|
* the given scopeID and returning its HAL link with this
|
||||||
|
* data-service's linkName
|
||||||
|
*
|
||||||
|
* @param {string} scopeID
|
||||||
|
* the id of the scope object
|
||||||
|
* @return { Observable<string> }
|
||||||
|
* an Observable<string> containing the scoped URL
|
||||||
|
*/
|
||||||
|
public getScopedEndpoint(scopeID: string): Observable<string> {
|
||||||
|
if (isEmpty(scopeID)) {
|
||||||
|
return this.getEndpoint();
|
||||||
|
} else {
|
||||||
|
const scopeCommunityHrefObs = this.cds.getEndpoint()
|
||||||
|
.flatMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, scopeID))
|
||||||
|
.filter((href: string) => isNotEmpty(href))
|
||||||
|
.take(1)
|
||||||
|
.do((href: string) => {
|
||||||
|
const request = new FindByIDRequest(href, scopeID);
|
||||||
|
this.requestService.configure(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [successResponse, errorResponse] = scopeCommunityHrefObs
|
||||||
|
.flatMap((href: string) => this.responseCache.get(href))
|
||||||
|
.map((entry: ResponseCacheEntry) => entry.response)
|
||||||
|
.share()
|
||||||
|
.partition((response: RestResponse) => response.isSuccessful);
|
||||||
|
|
||||||
|
return Observable.merge(
|
||||||
|
errorResponse.flatMap((response: ErrorResponse) =>
|
||||||
|
Observable.throw(new Error(`The Community with scope ${scopeID} couldn't be retrieved`))),
|
||||||
|
successResponse
|
||||||
|
.flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(scopeID, NormalizedCommunity))
|
||||||
|
.map((nc: NormalizedCommunity) => nc._links[this.linkName])
|
||||||
|
.filter((href) => isNotEmpty(href))
|
||||||
|
).distinctUntilChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,29 +1,29 @@
|
|||||||
import { Inject, Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
import { DataService } from './data.service';
|
|
||||||
import { Community } from '../shared/community.model';
|
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
|
||||||
import { NormalizedCommunity } from '../cache/models/normalized-community.model';
|
|
||||||
import { CoreState } from '../core.reducers';
|
|
||||||
import { RequestService } from './request.service';
|
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
|
||||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { NormalizedCommunity } from '../cache/models/normalized-community.model';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { Community } from '../shared/community.model';
|
||||||
|
import { ComColDataService } from './comcol-data.service';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommunityDataService extends DataService<NormalizedCommunity, Community> {
|
export class CommunityDataService extends ComColDataService<NormalizedCommunity, Community> {
|
||||||
protected linkName = 'communities';
|
protected linkName = 'communities';
|
||||||
protected browseEndpoint = '/discover/browses/dateissued/communities';
|
protected cds = this;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
protected responseCache: ResponseCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
@Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
|
protected objectCache: ObjectCacheService
|
||||||
) {
|
) {
|
||||||
super(NormalizedCommunity, EnvConfig);
|
super(NormalizedCommunity);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,74 +1,42 @@
|
|||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
|
||||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
|
||||||
import { RemoteData } from './remote-data';
|
|
||||||
import {
|
|
||||||
FindAllOptions,
|
|
||||||
FindAllRequest,
|
|
||||||
FindByIDRequest,
|
|
||||||
RestRequest,
|
|
||||||
RootEndpointRequest
|
|
||||||
} from './request.models';
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { CoreState } from '../core.reducers';
|
|
||||||
import { RequestService } from './request.service';
|
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
|
||||||
import { GenericConstructor } from '../shared/generic-constructor';
|
|
||||||
import { GlobalConfig } from '../../../config';
|
|
||||||
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
import { GlobalConfig } from '../../../config';
|
||||||
import { EndpointMap, RootSuccessResponse } from '../cache/response-cache.models';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import { FindAllOptions, FindAllRequest, FindByIDRequest, RestRequest } from './request.models';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
|
|
||||||
export abstract class DataService<TNormalized extends CacheableObject, TDomain> {
|
export abstract class DataService<TNormalized extends CacheableObject, TDomain> extends HALEndpointService {
|
||||||
protected abstract responseCache: ResponseCacheService;
|
protected abstract responseCache: ResponseCacheService;
|
||||||
protected abstract requestService: RequestService;
|
protected abstract requestService: RequestService;
|
||||||
protected abstract rdbService: RemoteDataBuildService;
|
protected abstract rdbService: RemoteDataBuildService;
|
||||||
protected abstract store: Store<CoreState>;
|
protected abstract store: Store<CoreState>;
|
||||||
protected abstract linkName: string;
|
protected abstract linkName: string;
|
||||||
protected abstract browseEndpoint: string;
|
protected abstract EnvConfig: GlobalConfig;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private normalizedResourceType: GenericConstructor<TNormalized>,
|
protected normalizedResourceType: GenericConstructor<TNormalized>,
|
||||||
protected EnvConfig: GlobalConfig
|
|
||||||
) {
|
) {
|
||||||
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEndpointMap(): Observable<EndpointMap> {
|
public abstract getScopedEndpoint(scope: string): Observable<string>
|
||||||
const request = new RootEndpointRequest(this.EnvConfig);
|
|
||||||
this.requestService.configure(request);
|
|
||||||
return this.responseCache.get(request.href)
|
|
||||||
.map((entry: ResponseCacheEntry) => entry.response)
|
|
||||||
.filter((response: RootSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.endpointMap))
|
|
||||||
.map((response: RootSuccessResponse) => response.endpointMap)
|
|
||||||
.distinctUntilChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getEndpoint(): Observable<string> {
|
protected getFindAllHref(endpoint, options: FindAllOptions = {}): Observable<string> {
|
||||||
const request = new RootEndpointRequest(this.EnvConfig);
|
let result: Observable<string>;
|
||||||
this.requestService.configure(request);
|
|
||||||
return this.getEndpointMap()
|
|
||||||
.map((map: EndpointMap) => map[this.linkName])
|
|
||||||
.distinctUntilChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public isEnabledOnRestApi(): Observable<boolean> {
|
|
||||||
return this.getEndpointMap()
|
|
||||||
.map((map: EndpointMap) => isNotEmpty(map[this.linkName]))
|
|
||||||
.startWith(undefined)
|
|
||||||
.distinctUntilChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getFindAllHref(endpoint, options: FindAllOptions = {}): string {
|
|
||||||
let result;
|
|
||||||
const args = [];
|
const args = [];
|
||||||
|
|
||||||
if (hasValue(options.scopeID)) {
|
if (hasValue(options.scopeID)) {
|
||||||
result = new RESTURLCombiner(this.EnvConfig, this.browseEndpoint).toString();
|
result = this.getScopedEndpoint(options.scopeID).distinctUntilChanged();
|
||||||
args.push(`scope=${options.scopeID}`);
|
|
||||||
} else {
|
} else {
|
||||||
result = endpoint;
|
result = Observable.of(endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
|
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
|
||||||
@@ -89,16 +57,19 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isNotEmpty(args)) {
|
if (isNotEmpty(args)) {
|
||||||
result = `${result}?${args.join('&')}`;
|
return result.map((href: string) => new URLCombiner(href, `?${args.join('&')}`).toString());
|
||||||
}
|
} else {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
findAll(options: FindAllOptions = {}): RemoteData<TDomain[]> {
|
findAll(options: FindAllOptions = {}): Observable<RemoteData<TDomain[]>> {
|
||||||
const hrefObs = this.getEndpoint()
|
const hrefObs = this.getEndpoint().filter((href: string) => isNotEmpty(href))
|
||||||
.map((endpoint: string) => this.getFindAllHref(endpoint, options));
|
.flatMap((endpoint: string) => this.getFindAllHref(endpoint, options));
|
||||||
|
|
||||||
hrefObs
|
hrefObs
|
||||||
|
.filter((href: string) => hasValue(href))
|
||||||
|
.take(1)
|
||||||
.subscribe((href: string) => {
|
.subscribe((href: string) => {
|
||||||
const request = new FindAllRequest(href, options);
|
const request = new FindAllRequest(href, options);
|
||||||
this.requestService.configure(request);
|
this.requestService.configure(request);
|
||||||
@@ -107,15 +78,17 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
|
|||||||
return this.rdbService.buildList<TNormalized, TDomain>(hrefObs, this.normalizedResourceType);
|
return this.rdbService.buildList<TNormalized, TDomain>(hrefObs, this.normalizedResourceType);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getFindByIDHref(endpoint, resourceID): string {
|
getFindByIDHref(endpoint, resourceID): string {
|
||||||
return `${endpoint}/${resourceID}`;
|
return `${endpoint}/${resourceID}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
findById(id: string): RemoteData<TDomain> {
|
findById(id: string): Observable<RemoteData<TDomain>> {
|
||||||
const hrefObs = this.getEndpoint()
|
const hrefObs = this.getEndpoint()
|
||||||
.map((endpoint: string) => this.getFindByIDHref(endpoint, id));
|
.map((endpoint: string) => this.getFindByIDHref(endpoint, id));
|
||||||
|
|
||||||
hrefObs
|
hrefObs
|
||||||
|
.filter((href: string) => hasValue(href))
|
||||||
|
.take(1)
|
||||||
.subscribe((href: string) => {
|
.subscribe((href: string) => {
|
||||||
const request = new FindByIDRequest(href, id);
|
const request = new FindByIDRequest(href, id);
|
||||||
this.requestService.configure(request);
|
this.requestService.configure(request);
|
||||||
@@ -124,10 +97,9 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
|
|||||||
return this.rdbService.buildSingle<TNormalized, TDomain>(hrefObs, this.normalizedResourceType);
|
return this.rdbService.buildSingle<TNormalized, TDomain>(hrefObs, this.normalizedResourceType);
|
||||||
}
|
}
|
||||||
|
|
||||||
findByHref(href: string): RemoteData<TDomain> {
|
findByHref(href: string): Observable<RemoteData<TDomain>> {
|
||||||
this.requestService.configure(new RestRequest(href));
|
this.requestService.configure(new RestRequest(href));
|
||||||
return this.rdbService.buildSingle<TNormalized, TDomain>(href, this.normalizedResourceType);
|
return this.rdbService.buildSingle<TNormalized, TDomain>(href, this.normalizedResourceType);
|
||||||
// return this.rdbService.buildSingle(href));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
94
src/app/core/data/item-data.service.spec.ts
Normal file
94
src/app/core/data/item-data.service.spec.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||||
|
import { TestScheduler } from 'rxjs/Rx';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
import { BrowseService } from '../browse/browse.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { ItemDataService } from './item-data.service';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
|
||||||
|
describe('ItemDataService', () => {
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let service: ItemDataService;
|
||||||
|
let bs: BrowseService;
|
||||||
|
|
||||||
|
const requestService = {} as RequestService;
|
||||||
|
const responseCache = {} as ResponseCacheService;
|
||||||
|
const rdbService = {} as RemoteDataBuildService;
|
||||||
|
const store = {} as Store<CoreState>;
|
||||||
|
const EnvConfig = {} as GlobalConfig;
|
||||||
|
|
||||||
|
const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39';
|
||||||
|
const browsesEndpoint = 'https://rest.api/discover/browses';
|
||||||
|
const itemBrowseEndpoint = `${browsesEndpoint}/author/items`;
|
||||||
|
const scopedEndpoint = `${itemBrowseEndpoint}?scope=${scopeID}`;
|
||||||
|
const serviceEndpoint = `https://rest.api/core/items`;
|
||||||
|
const browseError = new Error('getBrowseURL failed');
|
||||||
|
|
||||||
|
function initMockBrowseService(isSuccessful: boolean) {
|
||||||
|
const obs = isSuccessful ?
|
||||||
|
cold('--a-', { a: itemBrowseEndpoint }) :
|
||||||
|
cold('--#-', undefined, browseError);
|
||||||
|
return jasmine.createSpyObj('bs', {
|
||||||
|
getBrowseURLFor: obs
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTestService() {
|
||||||
|
return new ItemDataService(
|
||||||
|
responseCache,
|
||||||
|
requestService,
|
||||||
|
rdbService,
|
||||||
|
store,
|
||||||
|
EnvConfig,
|
||||||
|
bs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('getScopedEndpoint', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the endpoint to fetch Items within the given scope', () => {
|
||||||
|
bs = initMockBrowseService(true);
|
||||||
|
service = initTestService();
|
||||||
|
|
||||||
|
const result = service.getScopedEndpoint(scopeID);
|
||||||
|
const expected = cold('--b-', { b: scopedEndpoint });
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if the dc.date.issue browse isn\'t configured for items', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
bs = initMockBrowseService(false);
|
||||||
|
service = initTestService();
|
||||||
|
});
|
||||||
|
it('should throw an error', () => {
|
||||||
|
const result = service.getScopedEndpoint(scopeID);
|
||||||
|
const expected = cold('--#-', undefined, browseError);
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if the scope is not specified', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
bs = initMockBrowseService(true);
|
||||||
|
service = initTestService();
|
||||||
|
spyOn(service, 'getEndpoint').and.returnValue(cold('--b-', { b: serviceEndpoint }))
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return this.getEndpoint()', () => {
|
||||||
|
const result = service.getScopedEndpoint(undefined);
|
||||||
|
const expected = cold('--c-', { c: serviceEndpoint });
|
||||||
|
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
@@ -1,28 +1,44 @@
|
|||||||
import { Inject, Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { DataService } from './data.service';
|
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||||
import { Item } from '../shared/item.model';
|
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { BrowseService } from '../browse/browse.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { NormalizedItem } from '../cache/models/normalized-item.model';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
import { NormalizedItem } from '../cache/models/normalized-item.model';
|
import { Item } from '../shared/item.model';
|
||||||
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
|
|
||||||
|
import { DataService } from './data.service';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
|
||||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ItemDataService extends DataService<NormalizedItem, Item> {
|
export class ItemDataService extends DataService<NormalizedItem, Item> {
|
||||||
protected linkName = 'items';
|
protected linkName = 'items';
|
||||||
protected browseEndpoint = '/discover/browses/dateissued/items';
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
protected responseCache: ResponseCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
@Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
|
private bs: BrowseService
|
||||||
) {
|
) {
|
||||||
super(NormalizedItem, EnvConfig);
|
super(NormalizedItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getScopedEndpoint(scopeID: string): Observable<string> {
|
||||||
|
if (isEmpty(scopeID)) {
|
||||||
|
return this.getEndpoint();
|
||||||
|
} else {
|
||||||
|
return this.bs.getBrowseURLFor('dc.date.issued', this.linkName)
|
||||||
|
.filter((href: string) => isNotEmpty(href))
|
||||||
|
.map((href: string) => new URLCombiner(href, `?scope=${scopeID}`).toString())
|
||||||
|
.distinctUntilChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,11 @@
|
|||||||
import { Observable } from 'rxjs/Observable';
|
|
||||||
|
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
|
||||||
export enum RemoteDataState {
|
export enum RemoteDataState {
|
||||||
RequestPending = 'RequestPending' as any,
|
RequestPending = 'RequestPending',
|
||||||
ResponsePending = 'ResponsePending' as any,
|
ResponsePending = 'ResponsePending',
|
||||||
Failed = 'Failed' as any,
|
Failed = 'Failed',
|
||||||
Success = 'Success' as any
|
Success = 'Success'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,57 +13,48 @@ export enum RemoteDataState {
|
|||||||
*/
|
*/
|
||||||
export class RemoteData<T> {
|
export class RemoteData<T> {
|
||||||
constructor(
|
constructor(
|
||||||
public self: Observable<string>,
|
public self: string,
|
||||||
private requestPending: Observable<boolean>,
|
private requestPending: boolean,
|
||||||
private responsePending: Observable<boolean>,
|
private responsePending: boolean,
|
||||||
private isSuccessFul: Observable<boolean>,
|
private isSuccessFul: boolean,
|
||||||
public errorMessage: Observable<string>,
|
public errorMessage: string,
|
||||||
public statusCode: Observable<string>,
|
public statusCode: string,
|
||||||
public pageInfo: Observable<PageInfo>,
|
public pageInfo: PageInfo,
|
||||||
public payload: Observable<T>
|
public payload: T
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
get state(): Observable<RemoteDataState> {
|
get state(): RemoteDataState {
|
||||||
return Observable.combineLatest(
|
if (this.isSuccessFul === true && hasValue(this.payload)) {
|
||||||
this.requestPending,
|
|
||||||
this.responsePending,
|
|
||||||
this.isSuccessFul,
|
|
||||||
(requestPending, responsePending, isSuccessFul) => {
|
|
||||||
if (requestPending) {
|
|
||||||
return RemoteDataState.RequestPending
|
|
||||||
} else if (responsePending) {
|
|
||||||
return RemoteDataState.ResponsePending
|
|
||||||
} else if (!isSuccessFul) {
|
|
||||||
return RemoteDataState.Failed
|
|
||||||
} else {
|
|
||||||
return RemoteDataState.Success
|
return RemoteDataState.Success
|
||||||
|
} else if (this.isSuccessFul === false) {
|
||||||
|
return RemoteDataState.Failed
|
||||||
|
} else if (this.requestPending === true) {
|
||||||
|
return RemoteDataState.RequestPending
|
||||||
|
} else {
|
||||||
|
return RemoteDataState.ResponsePending
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
).distinctUntilChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
get isRequestPending(): Observable<boolean> {
|
get isRequestPending(): boolean {
|
||||||
return this.state.map((state) => state === RemoteDataState.RequestPending).distinctUntilChanged();
|
return this.state === RemoteDataState.RequestPending;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isResponsePending(): Observable<boolean> {
|
get isResponsePending(): boolean {
|
||||||
return this.state.map((state) => state === RemoteDataState.ResponsePending).distinctUntilChanged();
|
return this.state === RemoteDataState.ResponsePending;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isLoading(): Observable<boolean> {
|
get isLoading(): boolean {
|
||||||
return this.state.map((state) => {
|
return this.state === RemoteDataState.RequestPending
|
||||||
return state === RemoteDataState.RequestPending
|
|| this.state === RemoteDataState.ResponsePending;
|
||||||
|| state === RemoteDataState.ResponsePending
|
|
||||||
}).distinctUntilChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasFailed(): Observable<boolean> {
|
get hasFailed(): boolean {
|
||||||
return this.state.map((state) => state === RemoteDataState.Failed).distinctUntilChanged();
|
return this.state === RemoteDataState.Failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasSucceeded(): Observable<boolean> {
|
get hasSucceeded(): boolean {
|
||||||
return this.state.map((state) => state === RemoteDataState.Success).distinctUntilChanged();
|
return this.state === RemoteDataState.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,7 @@ import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
|||||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||||
import { ResponseParsingService } from './parsing.service';
|
import { ResponseParsingService } from './parsing.service';
|
||||||
import { RootResponseParsingService } from './root-response-parsing.service';
|
import { RootResponseParsingService } from './root-response-parsing.service';
|
||||||
|
import { BrowseResponseParsingService } from './browse-response-parsing.service';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
export class RestRequest {
|
export class RestRequest {
|
||||||
@@ -53,6 +54,16 @@ export class RootEndpointRequest extends RestRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class BrowseEndpointRequest extends RestRequest {
|
||||||
|
constructor(href: string) {
|
||||||
|
super(href);
|
||||||
|
}
|
||||||
|
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return BrowseResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class RequestError extends Error {
|
export class RequestError extends Error {
|
||||||
statusText: string;
|
statusText: string;
|
||||||
}
|
}
|
||||||
|
@@ -1,27 +1,35 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { MemoizedSelector, Store } from '@ngrx/store';
|
import { createSelector, MemoizedSelector, Store } from '@ngrx/store';
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { DSOSuccessResponse } from '../cache/response-cache.models';
|
import { DSOSuccessResponse, RestResponse } from '../cache/response-cache.models';
|
||||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
import { CoreState } from '../core.reducers';
|
import { coreSelector, CoreState } from '../core.reducers';
|
||||||
import { keySelector } from '../shared/selectors';
|
import { keySelector } from '../shared/selectors';
|
||||||
import { RequestConfigureAction, RequestExecuteAction } from './request.actions';
|
import { RequestConfigureAction, RequestExecuteAction } from './request.actions';
|
||||||
import { RestRequest } from './request.models';
|
import { RestRequest } from './request.models';
|
||||||
|
|
||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry, RequestState } from './request.reducer';
|
||||||
|
|
||||||
function entryFromHrefSelector(href: string): MemoizedSelector<CoreState, RequestEntry> {
|
function entryFromHrefSelector(href: string): MemoizedSelector<CoreState, RequestEntry> {
|
||||||
return keySelector<RequestEntry>('data/request', href);
|
return keySelector<RequestEntry>('data/request', href);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function requestStateSelector(): MemoizedSelector<CoreState, RequestState> {
|
||||||
|
return createSelector(coreSelector, (state: CoreState) => {
|
||||||
|
return state['data/request'] as RequestState;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RequestService {
|
export class RequestService {
|
||||||
|
private requestsOnTheirWayToTheStore: string[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private objectCache: ObjectCacheService,
|
private objectCache: ObjectCacheService,
|
||||||
@@ -31,6 +39,12 @@ export class RequestService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isPending(href: string): boolean {
|
isPending(href: string): boolean {
|
||||||
|
// first check requests that haven't made it to the store yet
|
||||||
|
if (this.requestsOnTheirWayToTheStore.includes(href)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// then check the store
|
||||||
let isPending = false;
|
let isPending = false;
|
||||||
this.store.select(entryFromHrefSelector(href))
|
this.store.select(entryFromHrefSelector(href))
|
||||||
.take(1)
|
.take(1)
|
||||||
@@ -47,16 +61,19 @@ export class RequestService {
|
|||||||
|
|
||||||
configure<T extends CacheableObject>(request: RestRequest): void {
|
configure<T extends CacheableObject>(request: RestRequest): void {
|
||||||
let isCached = this.objectCache.hasBySelfLink(request.href);
|
let isCached = this.objectCache.hasBySelfLink(request.href);
|
||||||
|
|
||||||
if (!isCached && this.responseCache.has(request.href)) {
|
if (!isCached && this.responseCache.has(request.href)) {
|
||||||
const [dsoSuccessResponse, otherSuccessResponse] = this.responseCache.get(request.href)
|
const [successResponse, errorResponse] = this.responseCache.get(request.href)
|
||||||
.take(1)
|
.take(1)
|
||||||
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
|
||||||
.map((entry: ResponseCacheEntry) => entry.response)
|
.map((entry: ResponseCacheEntry) => entry.response)
|
||||||
|
.share()
|
||||||
|
.partition((response: RestResponse) => response.isSuccessful);
|
||||||
|
|
||||||
|
const [dsoSuccessResponse, otherSuccessResponse] = successResponse
|
||||||
.share()
|
.share()
|
||||||
.partition((response: DSOSuccessResponse) => hasValue(response.resourceSelfLinks));
|
.partition((response: DSOSuccessResponse) => hasValue(response.resourceSelfLinks));
|
||||||
|
|
||||||
Observable.merge(
|
Observable.merge(
|
||||||
|
errorResponse.map(() => true), // TODO add a configurable number of retries in case of an error.
|
||||||
otherSuccessResponse.map(() => true),
|
otherSuccessResponse.map(() => true),
|
||||||
dsoSuccessResponse // a DSOSuccessResponse should only be considered cached if all its resources are cached
|
dsoSuccessResponse // a DSOSuccessResponse should only be considered cached if all its resources are cached
|
||||||
.map((response: DSOSuccessResponse) => response.resourceSelfLinks)
|
.map((response: DSOSuccessResponse) => response.resourceSelfLinks)
|
||||||
@@ -71,6 +88,24 @@ export class RequestService {
|
|||||||
if (!(isCached || isPending)) {
|
if (!(isCached || isPending)) {
|
||||||
this.store.dispatch(new RequestConfigureAction(request));
|
this.store.dispatch(new RequestConfigureAction(request));
|
||||||
this.store.dispatch(new RequestExecuteAction(request.href));
|
this.store.dispatch(new RequestExecuteAction(request.href));
|
||||||
|
this.trackRequestsOnTheirWayToTheStore(request.href);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ngrx action dispatches are asynchronous. But this.isPending needs to return true as soon as the
|
||||||
|
* configure method for a request has been executed, otherwise certain requests will happen multiple times.
|
||||||
|
*
|
||||||
|
* This method will store the href of every request that gets configured in a local variable, and
|
||||||
|
* remove it as soon as it can be found in the store.
|
||||||
|
*/
|
||||||
|
private trackRequestsOnTheirWayToTheStore(href: string) {
|
||||||
|
this.requestsOnTheirWayToTheStore = [...this.requestsOnTheirWayToTheStore, href];
|
||||||
|
this.store.select(entryFromHrefSelector(href))
|
||||||
|
.filter((re: RequestEntry) => hasValue(re))
|
||||||
|
.take(1)
|
||||||
|
.subscribe((re: RequestEntry) => {
|
||||||
|
this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((pendingHref: string) => pendingHref !== href)
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -19,12 +19,7 @@ export class RootResponseParsingService implements ResponseParsingService {
|
|||||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) {
|
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) {
|
||||||
const links = data.payload._links;
|
const links = data.payload._links;
|
||||||
for (const link of Object.keys(links)) {
|
for (const link of Object.keys(links)) {
|
||||||
let href = links[link].href;
|
links[link] = links[link].href;
|
||||||
// TODO temporary workaround as these endpoint paths are relative, but should be absolute
|
|
||||||
if (isNotEmpty(href) && !href.startsWith('http')) {
|
|
||||||
href = new RESTURLCombiner(this.EnvConfig, href.substring(this.EnvConfig.rest.nameSpace.length)).toString();
|
|
||||||
}
|
|
||||||
links[link] = href;
|
|
||||||
}
|
}
|
||||||
return new RootSuccessResponse(links, data.statusCode);
|
return new RootSuccessResponse(links, data.statusCode);
|
||||||
} else {
|
} else {
|
||||||
|
@@ -30,6 +30,8 @@ import { Item } from '../../core/shared/item.model';
|
|||||||
|
|
||||||
import { MockItem } from '../../shared/mocks/mock-item';
|
import { MockItem } from '../../shared/mocks/mock-item';
|
||||||
import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader';
|
import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader';
|
||||||
|
import { BrowseService } from '../browse/browse.service';
|
||||||
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
@Component({
|
@Component({
|
||||||
@@ -111,6 +113,7 @@ describe('MetadataService', () => {
|
|||||||
Meta,
|
Meta,
|
||||||
Title,
|
Title,
|
||||||
ItemDataService,
|
ItemDataService,
|
||||||
|
BrowseService,
|
||||||
MetadataService
|
MetadataService
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
@@ -173,33 +176,17 @@ describe('MetadataService', () => {
|
|||||||
expect(tagStore.get('description')[0].content).toEqual('This is a dummy item component for testing!');
|
expect(tagStore.get('description')[0].content).toEqual('This is a dummy item component for testing!');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockRemoteData = (mockItem: Item): RemoteData<Item> => {
|
const mockRemoteData = (mockItem: Item): Observable<RemoteData<Item>> => {
|
||||||
return new RemoteData<Item>(
|
return Observable.of(new RemoteData<Item>(
|
||||||
Observable.create((observer) => {
|
'',
|
||||||
observer.next('');
|
false,
|
||||||
}),
|
false,
|
||||||
Observable.create((observer) => {
|
true,
|
||||||
observer.next(false);
|
'',
|
||||||
}),
|
'200',
|
||||||
Observable.create((observer) => {
|
{} as PageInfo,
|
||||||
observer.next(false);
|
MockItem
|
||||||
}),
|
));
|
||||||
Observable.create((observer) => {
|
|
||||||
observer.next(true);
|
|
||||||
}),
|
|
||||||
Observable.create((observer) => {
|
|
||||||
observer.next('');
|
|
||||||
}),
|
|
||||||
Observable.create((observer) => {
|
|
||||||
observer.next(200);
|
|
||||||
}),
|
|
||||||
Observable.create((observer) => {
|
|
||||||
observer.next({});
|
|
||||||
}),
|
|
||||||
Observable.create((observer) => {
|
|
||||||
observer.next(MockItem);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockType = (mockItem: Item, type: string): Item => {
|
const mockType = (mockItem: Item, type: string): Item => {
|
||||||
|
@@ -25,6 +25,8 @@ import { Item } from '../shared/item.model';
|
|||||||
import { Metadatum } from '../shared/metadatum.model';
|
import { Metadatum } from '../shared/metadatum.model';
|
||||||
|
|
||||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||||
|
import { BitstreamFormat } from '../shared/bitstream-format.model';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MetadataService {
|
export class MetadataService {
|
||||||
@@ -64,8 +66,11 @@ export class MetadataService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public processRemoteData(remoteData: RemoteData<CacheableObject>): void {
|
public processRemoteData(remoteData: Observable<RemoteData<CacheableObject>>): void {
|
||||||
remoteData.payload.take(1).subscribe((dspaceObject: DSpaceObject) => {
|
remoteData.map((rd: RemoteData<CacheableObject>) => rd.payload)
|
||||||
|
.filter((co: CacheableObject) => hasValue(co))
|
||||||
|
.take(1)
|
||||||
|
.subscribe((dspaceObject: DSpaceObject) => {
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
this.initialize(dspaceObject);
|
this.initialize(dspaceObject);
|
||||||
}
|
}
|
||||||
@@ -268,7 +273,10 @@ export class MetadataService {
|
|||||||
// taking only two, fist one is empty array
|
// taking only two, fist one is empty array
|
||||||
item.getFiles().take(2).subscribe((bitstreams: Bitstream[]) => {
|
item.getFiles().take(2).subscribe((bitstreams: Bitstream[]) => {
|
||||||
for (const bitstream of bitstreams) {
|
for (const bitstream of bitstreams) {
|
||||||
bitstream.format.payload.take(1).subscribe((format) => {
|
bitstream.format.take(1)
|
||||||
|
.map((rd: RemoteData<BitstreamFormat>) => rd.payload)
|
||||||
|
.filter((format: BitstreamFormat) => hasValue(format))
|
||||||
|
.subscribe((format: BitstreamFormat) => {
|
||||||
if (format.mimetype === 'application/pdf') {
|
if (format.mimetype === 'application/pdf') {
|
||||||
this.addMetaTag('citation_pdf_url', bitstream.content);
|
this.addMetaTag('citation_pdf_url', bitstream.content);
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ import { DSpaceObject } from './dspace-object.model';
|
|||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { Item } from './item.model';
|
import { Item } from './item.model';
|
||||||
import { BitstreamFormat } from './bitstream-format.model';
|
import { BitstreamFormat } from './bitstream-format.model';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
export class Bitstream extends DSpaceObject {
|
export class Bitstream extends DSpaceObject {
|
||||||
|
|
||||||
@@ -23,17 +24,17 @@ export class Bitstream extends DSpaceObject {
|
|||||||
/**
|
/**
|
||||||
* An array of Bitstream Format of this Bitstream
|
* An array of Bitstream Format of this Bitstream
|
||||||
*/
|
*/
|
||||||
format: RemoteData<BitstreamFormat>;
|
format: Observable<RemoteData<BitstreamFormat>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of Items that are direct parents of this Bitstream
|
* An array of Items that are direct parents of this Bitstream
|
||||||
*/
|
*/
|
||||||
parents: RemoteData<Item[]>;
|
parents: Observable<RemoteData<Item[]>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Bundle that owns this Bitstream
|
* The Bundle that owns this Bitstream
|
||||||
*/
|
*/
|
||||||
owner: RemoteData<Item>;
|
owner: Observable<RemoteData<Item>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The URL to retrieve this Bitstream's file
|
* The URL to retrieve this Bitstream's file
|
||||||
|
24
src/app/core/shared/browse-definition.model.ts
Normal file
24
src/app/core/shared/browse-definition.model.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { autoserialize, autoserializeAs } from 'cerialize';
|
||||||
|
import { SortOption } from './sort-option.model';
|
||||||
|
|
||||||
|
export class BrowseDefinition {
|
||||||
|
@autoserialize
|
||||||
|
metadataBrowse: boolean;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
sortOptions: SortOption[];
|
||||||
|
|
||||||
|
@autoserializeAs('order')
|
||||||
|
defaultSortOrder: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@autoserializeAs('metadata')
|
||||||
|
metadataKeys: string[];
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
_links: {
|
||||||
|
[name: string]: string
|
||||||
|
}
|
||||||
|
}
|
@@ -2,23 +2,24 @@ import { DSpaceObject } from './dspace-object.model';
|
|||||||
import { Bitstream } from './bitstream.model';
|
import { Bitstream } from './bitstream.model';
|
||||||
import { Item } from './item.model';
|
import { Item } from './item.model';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
export class Bundle extends DSpaceObject {
|
export class Bundle extends DSpaceObject {
|
||||||
/**
|
/**
|
||||||
* The primary bitstream of this Bundle
|
* The primary bitstream of this Bundle
|
||||||
*/
|
*/
|
||||||
primaryBitstream: RemoteData<Bitstream>;
|
primaryBitstream: Observable<RemoteData<Bitstream>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of Items that are direct parents of this Bundle
|
* An array of Items that are direct parents of this Bundle
|
||||||
*/
|
*/
|
||||||
parents: RemoteData<Item[]>;
|
parents: Observable<RemoteData<Item[]>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Item that owns this Bundle
|
* The Item that owns this Bundle
|
||||||
*/
|
*/
|
||||||
owner: RemoteData<Item>;
|
owner: Observable<RemoteData<Item>>;
|
||||||
|
|
||||||
bitstreams: RemoteData<Bitstream[]>
|
bitstreams: Observable<RemoteData<Bitstream[]>>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ import { DSpaceObject } from './dspace-object.model';
|
|||||||
import { Bitstream } from './bitstream.model';
|
import { Bitstream } from './bitstream.model';
|
||||||
import { Item } from './item.model';
|
import { Item } from './item.model';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
export class Collection extends DSpaceObject {
|
export class Collection extends DSpaceObject {
|
||||||
|
|
||||||
@@ -53,18 +54,18 @@ export class Collection extends DSpaceObject {
|
|||||||
/**
|
/**
|
||||||
* The Bitstream that represents the logo of this Collection
|
* The Bitstream that represents the logo of this Collection
|
||||||
*/
|
*/
|
||||||
logo: RemoteData<Bitstream>;
|
logo: Observable<RemoteData<Bitstream>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of Collections that are direct parents of this Collection
|
* An array of Collections that are direct parents of this Collection
|
||||||
*/
|
*/
|
||||||
parents: RemoteData<Collection[]>;
|
parents: Observable<RemoteData<Collection[]>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Collection that owns this Collection
|
* The Collection that owns this Collection
|
||||||
*/
|
*/
|
||||||
owner: RemoteData<Collection>;
|
owner: Observable<RemoteData<Collection>>;
|
||||||
|
|
||||||
items: RemoteData<Item[]>;
|
items: Observable<RemoteData<Item[]>>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ import { DSpaceObject } from './dspace-object.model';
|
|||||||
import { Bitstream } from './bitstream.model';
|
import { Bitstream } from './bitstream.model';
|
||||||
import { Collection } from './collection.model';
|
import { Collection } from './collection.model';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
export class Community extends DSpaceObject {
|
export class Community extends DSpaceObject {
|
||||||
|
|
||||||
@@ -45,18 +46,18 @@ export class Community extends DSpaceObject {
|
|||||||
/**
|
/**
|
||||||
* The Bitstream that represents the logo of this Community
|
* The Bitstream that represents the logo of this Community
|
||||||
*/
|
*/
|
||||||
logo: RemoteData<Bitstream>;
|
logo: Observable<RemoteData<Bitstream>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of Communities that are direct parents of this Community
|
* An array of Communities that are direct parents of this Community
|
||||||
*/
|
*/
|
||||||
parents: RemoteData<DSpaceObject[]>;
|
parents: Observable<RemoteData<DSpaceObject[]>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Community that owns this Community
|
* The Community that owns this Community
|
||||||
*/
|
*/
|
||||||
owner: RemoteData<Community>;
|
owner: Observable<RemoteData<Community>>;
|
||||||
|
|
||||||
collections: RemoteData<Collection[]>;
|
collections: Observable<RemoteData<Collection[]>>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import { CacheableObject } from '../cache/object-cache.reducer';
|
|||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { ResourceType } from './resource-type';
|
import { ResourceType } from './resource-type';
|
||||||
import { ListableObject } from '../../object-list/listable-object/listable-object.model';
|
import { ListableObject } from '../../object-list/listable-object/listable-object.model';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An abstract model class for a DSpaceObject.
|
* An abstract model class for a DSpaceObject.
|
||||||
@@ -40,12 +41,12 @@ export abstract class DSpaceObject implements CacheableObject, ListableObject {
|
|||||||
/**
|
/**
|
||||||
* An array of DSpaceObjects that are direct parents of this DSpaceObject
|
* An array of DSpaceObjects that are direct parents of this DSpaceObject
|
||||||
*/
|
*/
|
||||||
parents: RemoteData<DSpaceObject[]>;
|
parents: Observable<RemoteData<DSpaceObject[]>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The DSpaceObject that owns this DSpaceObject
|
* The DSpaceObject that owns this DSpaceObject
|
||||||
*/
|
*/
|
||||||
owner: RemoteData<DSpaceObject>;
|
owner: Observable<RemoteData<DSpaceObject>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a metadata field by key and language
|
* Find a metadata field by key and language
|
||||||
|
135
src/app/core/shared/hal-endpoint.service.spec.ts
Normal file
135
src/app/core/shared/hal-endpoint.service.spec.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { cold, hot } from 'jasmine-marbles';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { RootEndpointRequest } from '../data/request.models';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { HALEndpointService } from './hal-endpoint.service';
|
||||||
|
|
||||||
|
describe('HALEndpointService', () => {
|
||||||
|
let service: HALEndpointService;
|
||||||
|
let responseCache: ResponseCacheService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
let envConfig: GlobalConfig;
|
||||||
|
|
||||||
|
const endpointMap = {
|
||||||
|
test: 'https://rest.api/test',
|
||||||
|
};
|
||||||
|
|
||||||
|
/* tslint:disable:no-shadowed-variable */
|
||||||
|
class TestService extends HALEndpointService {
|
||||||
|
protected linkName = 'test';
|
||||||
|
|
||||||
|
constructor(protected responseCache: ResponseCacheService,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected EnvConfig: GlobalConfig) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tslint:enable:no-shadowed-variable */
|
||||||
|
|
||||||
|
describe('getEndpointMap', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
responseCache = jasmine.createSpyObj('responseCache', {
|
||||||
|
get: hot('--a-', {
|
||||||
|
a: {
|
||||||
|
response: { endpointMap: endpointMap }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
requestService = jasmine.createSpyObj('requestService', ['configure']);
|
||||||
|
|
||||||
|
envConfig = {
|
||||||
|
rest: { baseUrl: 'https://rest.api/' }
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
service = new TestService(
|
||||||
|
responseCache,
|
||||||
|
requestService,
|
||||||
|
envConfig
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should configure a new RootEndpointRequest', () => {
|
||||||
|
(service as any).getEndpointMap();
|
||||||
|
const expected = new RootEndpointRequest(envConfig);
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an Observable of the endpoint map', () => {
|
||||||
|
const result = (service as any).getEndpointMap();
|
||||||
|
const expected = cold('--b-', { b: endpointMap });
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getEndpoint', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new TestService(
|
||||||
|
responseCache,
|
||||||
|
requestService,
|
||||||
|
envConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
spyOn(service as any, 'getEndpointMap').and
|
||||||
|
.returnValue(hot('--a-', { a: endpointMap }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the endpoint URL for the service\'s linkName', () => {
|
||||||
|
const result = service.getEndpoint();
|
||||||
|
const expected = cold('--b-', { b: endpointMap.test });
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined for a linkName that isn\'t in the endpoint map', () => {
|
||||||
|
(service as any).linkName = 'unknown';
|
||||||
|
const result = service.getEndpoint();
|
||||||
|
const expected = cold('--b-', { b: undefined });
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isEnabledOnRestApi', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new TestService(
|
||||||
|
responseCache,
|
||||||
|
requestService,
|
||||||
|
envConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined as long as getEndpointMap hasn\'t fired', () => {
|
||||||
|
spyOn(service as any, 'getEndpointMap').and
|
||||||
|
.returnValue(hot('----'));
|
||||||
|
|
||||||
|
const result = service.isEnabledOnRestApi();
|
||||||
|
const expected = cold('b---', { b: undefined });
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if the service\'s linkName is in the endpoint map', () => {
|
||||||
|
spyOn(service as any, 'getEndpointMap').and
|
||||||
|
.returnValue(hot('--a-', { a: endpointMap }));
|
||||||
|
|
||||||
|
const result = service.isEnabledOnRestApi();
|
||||||
|
const expected = cold('b-c-', { b: undefined, c: true });
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if the service\'s linkName isn\'t in the endpoint map', () => {
|
||||||
|
spyOn(service as any, 'getEndpointMap').and
|
||||||
|
.returnValue(hot('--a-', { a: endpointMap }));
|
||||||
|
|
||||||
|
(service as any).linkName = 'unknown';
|
||||||
|
const result = service.isEnabledOnRestApi();
|
||||||
|
const expected = cold('b-c-', { b: undefined, c: false });
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
39
src/app/core/shared/hal-endpoint.service.ts
Normal file
39
src/app/core/shared/hal-endpoint.service.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
import { EndpointMap, RootSuccessResponse } from '../cache/response-cache.models';
|
||||||
|
import { RootEndpointRequest } from '../data/request.models';
|
||||||
|
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
|
||||||
|
export abstract class HALEndpointService {
|
||||||
|
protected abstract responseCache: ResponseCacheService;
|
||||||
|
protected abstract requestService: RequestService;
|
||||||
|
protected abstract linkName: string;
|
||||||
|
protected abstract EnvConfig: GlobalConfig;
|
||||||
|
|
||||||
|
protected getEndpointMap(): Observable<EndpointMap> {
|
||||||
|
const request = new RootEndpointRequest(this.EnvConfig);
|
||||||
|
this.requestService.configure(request);
|
||||||
|
return this.responseCache.get(request.href)
|
||||||
|
.map((entry: ResponseCacheEntry) => entry.response)
|
||||||
|
.filter((response: RootSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.endpointMap))
|
||||||
|
.map((response: RootSuccessResponse) => response.endpointMap)
|
||||||
|
.distinctUntilChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getEndpoint(): Observable<string> {
|
||||||
|
return this.getEndpointMap()
|
||||||
|
.map((map: EndpointMap) => map[this.linkName])
|
||||||
|
.distinctUntilChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public isEnabledOnRestApi(): Observable<boolean> {
|
||||||
|
return this.getEndpointMap()
|
||||||
|
.map((map: EndpointMap) => isNotEmpty(map[this.linkName]))
|
||||||
|
.startWith(undefined)
|
||||||
|
.distinctUntilChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -103,23 +103,15 @@ describe('Item', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function createRemoteDataObject(object: any) {
|
function createRemoteDataObject(object: any) {
|
||||||
const self = Observable.of('');
|
return Observable.of(new RemoteData(
|
||||||
const requestPending = Observable.of(false);
|
'',
|
||||||
const responsePending = Observable.of(false);
|
false,
|
||||||
const isSuccessful = Observable.of(true);
|
false,
|
||||||
const errorMessage = Observable.of(undefined);
|
true,
|
||||||
const statusCode = Observable.of('200');
|
undefined,
|
||||||
const pageInfo = Observable.of(new PageInfo());
|
'200',
|
||||||
const payload = Observable.of(object);
|
new PageInfo(),
|
||||||
return new RemoteData(
|
object
|
||||||
self,
|
));
|
||||||
requestPending,
|
|
||||||
responsePending,
|
|
||||||
isSuccessful,
|
|
||||||
errorMessage,
|
|
||||||
statusCode,
|
|
||||||
pageInfo,
|
|
||||||
payload
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ import { DSpaceObject } from './dspace-object.model';
|
|||||||
import { Collection } from './collection.model';
|
import { Collection } from './collection.model';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { Bitstream } from './bitstream.model';
|
import { Bitstream } from './bitstream.model';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
|
||||||
export class Item extends DSpaceObject {
|
export class Item extends DSpaceObject {
|
||||||
|
|
||||||
@@ -36,18 +36,18 @@ export class Item extends DSpaceObject {
|
|||||||
/**
|
/**
|
||||||
* An array of Collections that are direct parents of this Item
|
* An array of Collections that are direct parents of this Item
|
||||||
*/
|
*/
|
||||||
parents: RemoteData<Collection[]>;
|
parents: Observable<RemoteData<Collection[]>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Collection that owns this Item
|
* The Collection that owns this Item
|
||||||
*/
|
*/
|
||||||
owningCollection: RemoteData<Collection>;
|
owningCollection: Observable<RemoteData<Collection>>;
|
||||||
|
|
||||||
get owner(): RemoteData<Collection> {
|
get owner(): Observable<RemoteData<Collection>> {
|
||||||
return this.owningCollection;
|
return this.owningCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
bitstreams: RemoteData<Bitstream[]>;
|
bitstreams: Observable<RemoteData<Bitstream[]>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the thumbnail of this item
|
* Retrieves the thumbnail of this item
|
||||||
@@ -87,9 +87,14 @@ export class Item extends DSpaceObject {
|
|||||||
* @returns {Observable<Bitstream[]>} the bitstreams with the given bundleName
|
* @returns {Observable<Bitstream[]>} the bitstreams with the given bundleName
|
||||||
*/
|
*/
|
||||||
getBitstreamsByBundleName(bundleName: string): Observable<Bitstream[]> {
|
getBitstreamsByBundleName(bundleName: string): Observable<Bitstream[]> {
|
||||||
return this.bitstreams.payload.startWith([])
|
return this.bitstreams
|
||||||
|
.map((rd: RemoteData<Bitstream[]>) => rd.payload)
|
||||||
|
.filter((bitstreams: Bitstream[]) => hasValue(bitstreams))
|
||||||
|
.startWith([])
|
||||||
.map((bitstreams) => {
|
.map((bitstreams) => {
|
||||||
return bitstreams.filter((bitstream) => bitstream.bundleName === bundleName)
|
return bitstreams
|
||||||
|
.filter((bitstream) => hasValue(bitstream))
|
||||||
|
.filter((bitstream) => bitstream.bundleName === bundleName)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,10 +3,10 @@
|
|||||||
* https://github.com/Microsoft/TypeScript/pull/15486
|
* https://github.com/Microsoft/TypeScript/pull/15486
|
||||||
*/
|
*/
|
||||||
export enum ResourceType {
|
export enum ResourceType {
|
||||||
Bundle = 'bundle' as any,
|
Bundle = 'bundle',
|
||||||
Bitstream = 'bitstream' as any,
|
Bitstream = 'bitstream',
|
||||||
BitstreamFormat = 'bitstreamformat' as any,
|
BitstreamFormat = 'bitstreamformat',
|
||||||
Item = 'item' as any,
|
Item = 'item',
|
||||||
Collection = 'collection' as any,
|
Collection = 'collection',
|
||||||
Community = 'community' as any,
|
Community = 'community',
|
||||||
}
|
}
|
||||||
|
9
src/app/core/shared/sort-option.model.ts
Normal file
9
src/app/core/shared/sort-option.model.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { autoserialize } from 'cerialize';
|
||||||
|
|
||||||
|
export class SortOption {
|
||||||
|
@autoserialize
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
metadata: string;
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
<ds-pagination
|
<ds-pagination
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
[pageInfoState]="pageInfo"
|
[pageInfoState]="pageInfo"
|
||||||
[collectionSize]="(pageInfo | async)?.totalElements"
|
[collectionSize]="pageInfo?.totalElements"
|
||||||
[sortOptions]="sortConfig"
|
[sortOptions]="sortConfig"
|
||||||
[hideGear]="hideGear"
|
[hideGear]="hideGear"
|
||||||
[hidePagerWhenSinglePage]="hidePagerWhenSinglePage"
|
[hidePagerWhenSinglePage]="hidePagerWhenSinglePage"
|
||||||
@@ -10,11 +10,9 @@
|
|||||||
(sortDirectionChange)="onSortDirectionChange($event)"
|
(sortDirectionChange)="onSortDirectionChange($event)"
|
||||||
(sortFieldChange)="onSortDirectionChange($event)"
|
(sortFieldChange)="onSortDirectionChange($event)"
|
||||||
(paginationChange)="onPaginationChange($event)">
|
(paginationChange)="onPaginationChange($event)">
|
||||||
<ul *ngIf="objects.hasSucceeded | async" @fadeIn> <!--class="list-unstyled"-->
|
<ul *ngIf="objects?.hasSucceeded"> <!--class="list-unstyled"-->
|
||||||
<li *ngFor="let object of (objects.payload | async) | paginate: { itemsPerPage: (pageInfo | async)?.elementsPerPage, currentPage: (pageInfo | async)?.currentPage, totalItems: (pageInfo | async)?.totalElements }">
|
<li *ngFor="let object of objects?.payload">
|
||||||
<ds-wrapper-list-element [object]="object"></ds-wrapper-list-element>
|
<ds-wrapper-list-element [object]="object"></ds-wrapper-list-element>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ds-error *ngIf="objects.hasFailed | async" message="{{'error.objects' | translate}}"></ds-error>
|
|
||||||
<ds-loading *ngIf="objects.isLoading | async" message="{{'loading.objects' | translate}}"></ds-loading>
|
|
||||||
</ds-pagination>
|
</ds-pagination>
|
||||||
|
@@ -1,24 +1,22 @@
|
|||||||
import {
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
ViewEncapsulation,
|
Output,
|
||||||
ChangeDetectionStrategy,
|
ViewEncapsulation
|
||||||
OnInit,
|
|
||||||
Output, SimpleChanges, OnChanges, ChangeDetectorRef, DoCheck
|
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
|
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
import { PageInfo } from '../core/shared/page-info.model';
|
import { PageInfo } from '../core/shared/page-info.model';
|
||||||
|
|
||||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
|
||||||
|
|
||||||
import { SortOptions, SortDirection } from '../core/cache/models/sort-options.model';
|
|
||||||
import { ListableObject } from '../object-list/listable-object/listable-object.model';
|
import { ListableObject } from '../object-list/listable-object/listable-object.model';
|
||||||
|
|
||||||
import { fadeIn } from '../shared/animations/fade';
|
import { fadeIn } from '../shared/animations/fade';
|
||||||
|
import { hasValue } from '../shared/empty.util';
|
||||||
|
|
||||||
|
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
changeDetection: ChangeDetectionStrategy.Default,
|
changeDetection: ChangeDetectionStrategy.Default,
|
||||||
@@ -28,14 +26,35 @@ import { fadeIn } from '../shared/animations/fade';
|
|||||||
templateUrl: './object-list.component.html',
|
templateUrl: './object-list.component.html',
|
||||||
animations: [fadeIn]
|
animations: [fadeIn]
|
||||||
})
|
})
|
||||||
export class ObjectListComponent implements OnChanges, OnInit {
|
export class ObjectListComponent {
|
||||||
|
|
||||||
@Input() objects: RemoteData<ListableObject[]>;
|
|
||||||
@Input() config: PaginationComponentOptions;
|
@Input() config: PaginationComponentOptions;
|
||||||
@Input() sortConfig: SortOptions;
|
@Input() sortConfig: SortOptions;
|
||||||
@Input() hideGear = false;
|
@Input() hideGear = false;
|
||||||
@Input() hidePagerWhenSinglePage = true;
|
@Input() hidePagerWhenSinglePage = true;
|
||||||
pageInfo: Observable<PageInfo>;
|
private _objects: RemoteData<ListableObject[]>;
|
||||||
|
pageInfo: PageInfo;
|
||||||
|
@Input() set objects(objects: RemoteData<ListableObject[]>) {
|
||||||
|
this._objects = objects;
|
||||||
|
if (hasValue(objects)) {
|
||||||
|
this.pageInfo = objects.pageInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get objects() {
|
||||||
|
return this._objects;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when the page is changed.
|
||||||
|
* Event's payload equals to the newly selected page.
|
||||||
|
*/
|
||||||
|
@Output() change: EventEmitter<{
|
||||||
|
pagination: PaginationComponentOptions,
|
||||||
|
sort: SortOptions
|
||||||
|
}> = new EventEmitter<{
|
||||||
|
pagination: PaginationComponentOptions,
|
||||||
|
sort: SortOptions
|
||||||
|
}>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An event fired when the page is changed.
|
* An event fired when the page is changed.
|
||||||
@@ -64,26 +83,6 @@ export class ObjectListComponent implements OnChanges, OnInit {
|
|||||||
@Output() sortFieldChange: EventEmitter<string> = new EventEmitter<string>();
|
@Output() sortFieldChange: EventEmitter<string> = new EventEmitter<string>();
|
||||||
data: any = {};
|
data: any = {};
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges) {
|
|
||||||
if (changes.objects && !changes.objects.isFirstChange()) {
|
|
||||||
this.pageInfo = this.objects.pageInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.pageInfo = this.objects.pageInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param route
|
|
||||||
* Route is a singleton service provided by Angular.
|
|
||||||
* @param router
|
|
||||||
* Router is a singleton service provided by Angular.
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
private cdRef: ChangeDetectorRef) {
|
|
||||||
}
|
|
||||||
|
|
||||||
onPageChange(event) {
|
onPageChange(event) {
|
||||||
this.pageChange.emit(event);
|
this.pageChange.emit(event);
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,7 @@ import { Component, ChangeDetectionStrategy } from '@angular/core';
|
|||||||
selector: 'ds-pagenotfound',
|
selector: 'ds-pagenotfound',
|
||||||
styleUrls: ['./pagenotfound.component.scss'],
|
styleUrls: ['./pagenotfound.component.scss'],
|
||||||
templateUrl: './pagenotfound.component.html',
|
templateUrl: './pagenotfound.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.Default
|
||||||
})
|
})
|
||||||
export class PageNotFoundComponent {
|
export class PageNotFoundComponent {
|
||||||
constructor(responseService: ServerResponseService) {
|
constructor(responseService: ServerResponseService) {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { animate, state, transition, trigger, style, keyframes } from '@angular/animations';
|
import { animate, style, transition, trigger } from '@angular/animations';
|
||||||
|
|
||||||
const fadeEnter = transition(':enter', [
|
const fadeEnter = transition(':enter', [
|
||||||
style({ opacity: 0 }),
|
style({ opacity: 0 }),
|
||||||
|
@@ -9,61 +9,27 @@ export const MockItem: Item = Object.assign(new Item(), {
|
|||||||
isArchived: true,
|
isArchived: true,
|
||||||
isDiscoverable: true,
|
isDiscoverable: true,
|
||||||
isWithdrawn: false,
|
isWithdrawn: false,
|
||||||
bitstreams: {
|
bitstreams: Observable.of({
|
||||||
self: {
|
self: 'dspace-angular://aggregated/object/1507836003548',
|
||||||
_isScalar: true,
|
requestPending: false,
|
||||||
value: '1507836003548',
|
responsePending: false,
|
||||||
scheduler: null
|
isSuccessFul: true,
|
||||||
},
|
errorMessage: '',
|
||||||
requestPending: Observable.create((observer) => {
|
statusCode: '202',
|
||||||
observer.next(false);
|
pageInfo: {},
|
||||||
}),
|
payload: [
|
||||||
responsePending: Observable.create((observer) => {
|
|
||||||
observer.next(false);
|
|
||||||
}),
|
|
||||||
isSuccessFul: Observable.create((observer) => {
|
|
||||||
observer.next(true);
|
|
||||||
}),
|
|
||||||
errorMessage: Observable.create((observer) => {
|
|
||||||
observer.next('');
|
|
||||||
}),
|
|
||||||
statusCode: Observable.create((observer) => {
|
|
||||||
observer.next(202);
|
|
||||||
}),
|
|
||||||
pageInfo: Observable.create((observer) => {
|
|
||||||
observer.next({});
|
|
||||||
}),
|
|
||||||
payload: Observable.create((observer) => {
|
|
||||||
observer.next([
|
|
||||||
{
|
{
|
||||||
sizeBytes: 10201,
|
sizeBytes: 10201,
|
||||||
content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
||||||
format: {
|
format: Observable.of({
|
||||||
self: {
|
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/10',
|
||||||
_isScalar: true,
|
requestPending: false,
|
||||||
value: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/10',
|
responsePending: false,
|
||||||
scheduler: null
|
isSuccessFul: true,
|
||||||
},
|
errorMessage: '',
|
||||||
requestPending: Observable.create((observer) => {
|
statusCode: '202',
|
||||||
observer.next(false);
|
pageInfo: {},
|
||||||
}),
|
payload: {
|
||||||
responsePending: Observable.create((observer) => {
|
|
||||||
observer.next(false);
|
|
||||||
}),
|
|
||||||
isSuccessFul: Observable.create((observer) => {
|
|
||||||
observer.next(true);
|
|
||||||
}),
|
|
||||||
errorMessage: Observable.create((observer) => {
|
|
||||||
observer.next('');
|
|
||||||
}),
|
|
||||||
statusCode: Observable.create((observer) => {
|
|
||||||
observer.next(202);
|
|
||||||
}),
|
|
||||||
pageInfo: Observable.create((observer) => {
|
|
||||||
observer.next({});
|
|
||||||
}),
|
|
||||||
payload: Observable.create((observer) => {
|
|
||||||
observer.next({
|
|
||||||
shortDescription: 'Microsoft Word XML',
|
shortDescription: 'Microsoft Word XML',
|
||||||
description: 'Microsoft Word XML',
|
description: 'Microsoft Word XML',
|
||||||
mimetype: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
mimetype: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
@@ -71,9 +37,8 @@ export const MockItem: Item = Object.assign(new Item(), {
|
|||||||
internal: false,
|
internal: false,
|
||||||
extensions: null,
|
extensions: null,
|
||||||
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/10'
|
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/10'
|
||||||
});
|
}
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
bundleName: 'ORIGINAL',
|
bundleName: 'ORIGINAL',
|
||||||
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||||
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||||
@@ -91,32 +56,15 @@ export const MockItem: Item = Object.assign(new Item(), {
|
|||||||
{
|
{
|
||||||
sizeBytes: 31302,
|
sizeBytes: 31302,
|
||||||
content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/content',
|
content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/content',
|
||||||
format: {
|
format: Observable.of({
|
||||||
self: {
|
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/4',
|
||||||
_isScalar: true,
|
requestPending: false,
|
||||||
value: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/4',
|
responsePending: false,
|
||||||
scheduler: null
|
isSuccessFul: true,
|
||||||
},
|
errorMessage: '',
|
||||||
requestPending: Observable.create((observer) => {
|
statusCode: '202',
|
||||||
observer.next(false);
|
pageInfo: {},
|
||||||
}),
|
payload: {
|
||||||
responsePending: Observable.create((observer) => {
|
|
||||||
observer.next(false);
|
|
||||||
}),
|
|
||||||
isSuccessFul: Observable.create((observer) => {
|
|
||||||
observer.next(true);
|
|
||||||
}),
|
|
||||||
errorMessage: Observable.create((observer) => {
|
|
||||||
observer.next('');
|
|
||||||
}),
|
|
||||||
statusCode: Observable.create((observer) => {
|
|
||||||
observer.next(202);
|
|
||||||
}),
|
|
||||||
pageInfo: Observable.create((observer) => {
|
|
||||||
observer.next({});
|
|
||||||
}),
|
|
||||||
payload: Observable.create((observer) => {
|
|
||||||
observer.next({
|
|
||||||
shortDescription: 'Adobe PDF',
|
shortDescription: 'Adobe PDF',
|
||||||
description: 'Adobe Portable Document Format',
|
description: 'Adobe Portable Document Format',
|
||||||
mimetype: 'application/pdf',
|
mimetype: 'application/pdf',
|
||||||
@@ -124,9 +72,8 @@ export const MockItem: Item = Object.assign(new Item(), {
|
|||||||
internal: false,
|
internal: false,
|
||||||
extensions: null,
|
extensions: null,
|
||||||
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/4'
|
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/4'
|
||||||
});
|
}
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
bundleName: 'ORIGINAL',
|
bundleName: 'ORIGINAL',
|
||||||
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28',
|
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28',
|
||||||
id: '99b00f3c-1cc6-4689-8158-91965bee6b28',
|
id: '99b00f3c-1cc6-4689-8158-91965bee6b28',
|
||||||
@@ -141,9 +88,8 @@ export const MockItem: Item = Object.assign(new Item(), {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]);
|
]
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
||||||
id: '0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
id: '0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
||||||
uuid: '0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
uuid: '0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
||||||
@@ -241,33 +187,15 @@ export const MockItem: Item = Object.assign(new Item(), {
|
|||||||
value: 'text'
|
value: 'text'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
owningCollection: {
|
owningCollection: Observable.of({
|
||||||
self: {
|
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb',
|
||||||
_isScalar: true,
|
requestPending: false,
|
||||||
value: 'https://dspace7.4science.it/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb',
|
responsePending: false,
|
||||||
scheduler: null
|
isSuccessFul: true,
|
||||||
},
|
errorMessage: '',
|
||||||
requestPending: Observable.create((observer) => {
|
statusCode: '202',
|
||||||
observer.next(false);
|
pageInfo: {},
|
||||||
}),
|
payload: []
|
||||||
responsePending: Observable.create((observer) => {
|
|
||||||
observer.next(false);
|
|
||||||
}),
|
|
||||||
isSuccessFul: Observable.create((observer) => {
|
|
||||||
observer.next(true);
|
|
||||||
}),
|
|
||||||
errorMessage: Observable.create((observer) => {
|
|
||||||
observer.next('');
|
|
||||||
}),
|
|
||||||
statusCode: Observable.create((observer) => {
|
|
||||||
observer.next(202);
|
|
||||||
}),
|
|
||||||
pageInfo: Observable.create((observer) => {
|
|
||||||
observer.next({});
|
|
||||||
}),
|
|
||||||
payload: Observable.create((observer) => {
|
|
||||||
observer.next([]);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
)});
|
||||||
/* tslint:enable:no-shadowed-variable */
|
/* tslint:enable:no-shadowed-variable */
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
<form #form="ngForm" (ngSubmit)="onSubmit(form.value)" class="row" action="/search">
|
<form #form="ngForm" (ngSubmit)="onSubmit(form.value)" class="row">
|
||||||
<div *ngIf="isNotEmpty(scopes | async)" class="col-12 col-sm-3">
|
<div *ngIf="isNotEmpty(scopes)" class="col-12 col-sm-3">
|
||||||
<select [(ngModel)]="selectedId" name="scope" class="form-control" aria-label="Search scope" [compareWith]="byId">
|
<select [(ngModel)]="selectedId" name="scope" class="form-control" aria-label="Search scope" [compareWith]="byId">
|
||||||
<option value>{{'search.form.search_dspace' | translate}}</option>
|
<option value>{{'search.form.search_dspace' | translate}}</option>
|
||||||
<option *ngFor="let scopeOption of scopes | async" [value]="scopeOption.id">{{scopeOption?.name ? scopeOption.name : 'search.form.search_dspace' | translate}}</option>
|
<option *ngFor="let scopeOption of scopes" [value]="scopeOption.id">{{scopeOption?.name ? scopeOption.name : 'search.form.search_dspace' | translate}}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div [ngClass]="{'col-sm-9': isNotEmpty(scopes | async)}" class="col-12">
|
<div [ngClass]="{'col-sm-9': isNotEmpty(scopes)}" class="col-12">
|
||||||
<div class="form-group input-group">
|
<div class="form-group input-group">
|
||||||
<input type="text" [(ngModel)]="query" name="query" class="form-control" aria-label="Search input">
|
<input type="text" [(ngModel)]="query" name="query" class="form-control" aria-label="Search input">
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
|
@@ -8,6 +8,7 @@ import { ResourceType } from '../../core/shared/resource-type';
|
|||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { Community } from '../../core/shared/community.model';
|
import { Community } from '../../core/shared/community.model';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
|
||||||
describe('SearchFormComponent', () => {
|
describe('SearchFormComponent', () => {
|
||||||
let comp: SearchFormComponent;
|
let comp: SearchFormComponent;
|
||||||
@@ -30,7 +31,7 @@ describe('SearchFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should display scopes when available with default and all scopes', () => {
|
it('should display scopes when available with default and all scopes', () => {
|
||||||
comp.scopes = Observable.of(objects);
|
comp.scopes = objects;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const select: HTMLElement = de.query(By.css('select')).nativeElement;
|
const select: HTMLElement = de.query(By.css('select')).nativeElement;
|
||||||
expect(select).toBeDefined();
|
expect(select).toBeDefined();
|
||||||
@@ -64,7 +65,7 @@ describe('SearchFormComponent', () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
it('should select correct scope option in scope select', fakeAsync(() => {
|
it('should select correct scope option in scope select', fakeAsync(() => {
|
||||||
comp.scopes = Observable.of(objects);
|
comp.scopes = objects;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const testCommunity = objects[1];
|
const testCommunity = objects[1];
|
||||||
@@ -100,7 +101,7 @@ describe('SearchFormComponent', () => {
|
|||||||
// }));
|
// }));
|
||||||
});
|
});
|
||||||
|
|
||||||
export const objects = [
|
export const objects: DSpaceObject[] = [
|
||||||
Object.assign(new Community(), {
|
Object.assign(new Community(), {
|
||||||
handle: '10673/11',
|
handle: '10673/11',
|
||||||
logo: {
|
logo: {
|
||||||
|
@@ -20,7 +20,7 @@ export class SearchFormComponent {
|
|||||||
selectedId = '';
|
selectedId = '';
|
||||||
// Optional existing search parameters
|
// Optional existing search parameters
|
||||||
@Input() currentParams: {};
|
@Input() currentParams: {};
|
||||||
@Input() scopes: Observable<DSpaceObject[]>;
|
@Input() scopes: DSpaceObject[];
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
set scope(dso: DSpaceObject) {
|
set scope(dso: DSpaceObject) {
|
||||||
|
@@ -30,6 +30,7 @@ import { SearchResultListElementComponent } from '../object-list/search-result-l
|
|||||||
import { SearchFormComponent } from './search-form/search-form.component';
|
import { SearchFormComponent } from './search-form/search-form.component';
|
||||||
import { WrapperListElementComponent } from '../object-list/wrapper-list-element/wrapper-list-element.component';
|
import { WrapperListElementComponent } from '../object-list/wrapper-list-element/wrapper-list-element.component';
|
||||||
import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
|
import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
|
||||||
|
import { VarDirective } from './utils/var.directive';
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||||
@@ -77,6 +78,10 @@ const ENTRY_COMPONENTS = [
|
|||||||
SearchResultListElementComponent
|
SearchResultListElementComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const DIRECTIVES = [
|
||||||
|
VarDirective
|
||||||
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
...MODULES
|
...MODULES
|
||||||
@@ -84,6 +89,7 @@ const ENTRY_COMPONENTS = [
|
|||||||
declarations: [
|
declarations: [
|
||||||
...PIPES,
|
...PIPES,
|
||||||
...COMPONENTS,
|
...COMPONENTS,
|
||||||
|
...DIRECTIVES,
|
||||||
...ENTRY_COMPONENTS,
|
...ENTRY_COMPONENTS,
|
||||||
...DIRECTIVES
|
...DIRECTIVES
|
||||||
],
|
],
|
||||||
|
23
src/app/shared/utils/var.directive.ts
Normal file
23
src/app/shared/utils/var.directive.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
|
||||||
|
|
||||||
|
/* tslint:disable:directive-selector */
|
||||||
|
@Directive({
|
||||||
|
selector: '[ngVar]',
|
||||||
|
})
|
||||||
|
export class VarDirective {
|
||||||
|
@Input()
|
||||||
|
set ngVar(context: any) {
|
||||||
|
this.context.$implicit = this.context.ngVar = context;
|
||||||
|
this.updateView();
|
||||||
|
}
|
||||||
|
|
||||||
|
context: any = {};
|
||||||
|
|
||||||
|
constructor(private vcRef: ViewContainerRef, private templateRef: TemplateRef<any>) {}
|
||||||
|
|
||||||
|
updateView() {
|
||||||
|
this.vcRef.clear();
|
||||||
|
this.vcRef.createEmbeddedView(this.templateRef, this.context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* tslint:enable:directive-selector */
|
@@ -8,19 +8,15 @@ import * as https from 'https';
|
|||||||
import * as morgan from 'morgan';
|
import * as morgan from 'morgan';
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
import * as bodyParser from 'body-parser';
|
import * as bodyParser from 'body-parser';
|
||||||
import * as session from 'express-session';
|
|
||||||
import * as compression from 'compression';
|
import * as compression from 'compression';
|
||||||
import * as cookieParser from 'cookie-parser';
|
import * as cookieParser from 'cookie-parser';
|
||||||
|
|
||||||
import { platformServer, renderModuleFactory } from '@angular/platform-server';
|
|
||||||
import { enableProdMode } from '@angular/core';
|
import { enableProdMode } from '@angular/core';
|
||||||
|
|
||||||
import { ngExpressEngine } from '@nguniversal/express-engine';
|
import { ngExpressEngine } from '@nguniversal/express-engine';
|
||||||
|
|
||||||
import { ServerAppModule } from './modules/app/server-app.module';
|
import { ServerAppModule } from './modules/app/server-app.module';
|
||||||
|
|
||||||
import { serverApi, createMockApi } from './backend/api';
|
|
||||||
|
|
||||||
import { ROUTES } from './routes';
|
import { ROUTES } from './routes';
|
||||||
import { ENV_CONFIG } from './config';
|
import { ENV_CONFIG } from './config';
|
||||||
|
|
||||||
|
@@ -1,26 +1,25 @@
|
|||||||
import { NgModule, APP_INITIALIZER } from '@angular/core';
|
import { HttpClient, HttpClientModule } from '@angular/common/http';
|
||||||
import { HttpClientModule, HttpClient } from '@angular/common/http';
|
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
import { IdlePreload, IdlePreloadModule } from '@angularclass/idle-preload';
|
import { IdlePreload, IdlePreloadModule } from '@angularclass/idle-preload';
|
||||||
|
|
||||||
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
|
|
||||||
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
|
||||||
|
|
||||||
import { EffectsModule } from '@ngrx/effects';
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
|
|
||||||
import { TransferState } from '../transfer-state/transfer-state';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
import { BrowserTransferStateModule } from '../transfer-state/browser-transfer-state.module';
|
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||||
import { BrowserTransferStoreEffects } from '../transfer-store/browser-transfer-store.effects';
|
|
||||||
import { BrowserTransferStoreModule } from '../transfer-store/browser-transfer-store.module';
|
|
||||||
|
|
||||||
import { AppModule } from '../../app/app.module';
|
|
||||||
import { CoreModule } from '../../app/core/core.module';
|
|
||||||
|
|
||||||
import { AppComponent } from '../../app/app.component';
|
import { AppComponent } from '../../app/app.component';
|
||||||
|
|
||||||
|
import { AppModule } from '../../app/app.module';
|
||||||
|
import { BrowserTransferStateModule } from '../transfer-state/browser-transfer-state.module';
|
||||||
|
|
||||||
|
import { TransferState } from '../transfer-state/transfer-state';
|
||||||
|
import { BrowserTransferStoreEffects } from '../transfer-store/browser-transfer-store.effects';
|
||||||
|
import { BrowserTransferStoreModule } from '../transfer-store/browser-transfer-store.module';
|
||||||
|
|
||||||
export function init(cache: TransferState) {
|
export function init(cache: TransferState) {
|
||||||
return () => {
|
return () => {
|
||||||
cache.initialize();
|
cache.initialize();
|
||||||
@@ -41,6 +40,7 @@ export function createTranslateLoader(http: HttpClient) {
|
|||||||
// forRoot ensures the providers are only created once
|
// forRoot ensures the providers are only created once
|
||||||
IdlePreloadModule.forRoot(),
|
IdlePreloadModule.forRoot(),
|
||||||
RouterModule.forRoot([], {
|
RouterModule.forRoot([], {
|
||||||
|
// enableTracing: true,
|
||||||
useHash: false,
|
useHash: false,
|
||||||
preloadingStrategy:
|
preloadingStrategy:
|
||||||
IdlePreload
|
IdlePreload
|
||||||
|
@@ -36,7 +36,15 @@ export function boot(cache: TransferState, appRef: ApplicationRef, store: Store<
|
|||||||
// authentication mechanism goes here
|
// authentication mechanism goes here
|
||||||
return () => {
|
return () => {
|
||||||
appRef.isStable.filter((stable: boolean) => stable).first().subscribe(() => {
|
appRef.isStable.filter((stable: boolean) => stable).first().subscribe(() => {
|
||||||
|
// isStable == true doesn't guarantee that all dispatched actions have been
|
||||||
|
// processed yet. So in those cases the store snapshot wouldn't be complete
|
||||||
|
// and a rehydrate would leave the app in a broken state
|
||||||
|
//
|
||||||
|
// This setTimeout without delay schedules the cache.inject() to happen ASAP
|
||||||
|
// after everything that's already scheduled, and it solves that problem.
|
||||||
|
setTimeout(() => {
|
||||||
cache.inject();
|
cache.inject();
|
||||||
|
}, 0);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -1073,9 +1073,9 @@ caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
|
|||||||
version "1.0.30000740"
|
version "1.0.30000740"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000740.tgz#03fcaaa176e3ed075895f72d46c1a12149bbeac9"
|
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000740.tgz#03fcaaa176e3ed075895f72d46c1a12149bbeac9"
|
||||||
|
|
||||||
caniuse-lite@1.0.30000697:
|
caniuse-lite@1.0.30000746:
|
||||||
version "1.0.30000697"
|
version "1.0.30000746"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000697.tgz#125fb00604b63fbb188db96a667ce2922dcd6cdd"
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000746.tgz#c64f95a3925cfd30207a308ed76c1ae96ea09ea0"
|
||||||
|
|
||||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000744:
|
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000744:
|
||||||
version "1.0.30000745"
|
version "1.0.30000745"
|
||||||
|
Reference in New Issue
Block a user