Merge pull request #211 from artlowel/post-support

Store requests by UUID instead of URL
This commit is contained in:
Art Lowel
2018-01-19 13:07:25 +01:00
committed by GitHub
61 changed files with 1311 additions and 544 deletions

View File

@@ -107,6 +107,7 @@
"reflect-metadata": "0.1.10", "reflect-metadata": "0.1.10",
"rxjs": "5.4.3", "rxjs": "5.4.3",
"ts-md5": "1.2.2", "ts-md5": "1.2.2",
"uuid": "^3.1.0",
"webfontloader": "1.6.28", "webfontloader": "1.6.28",
"zone.js": "0.8.18" "zone.js": "0.8.18"
}, },
@@ -126,6 +127,7 @@
"@types/node": "8.0.34", "@types/node": "8.0.34",
"@types/serve-static": "1.7.32", "@types/serve-static": "1.7.32",
"@types/source-map": "0.5.1", "@types/source-map": "0.5.1",
"@types/uuid": "^3.4.3",
"@types/webfontloader": "1.6.29", "@types/webfontloader": "1.6.29",
"ajv": "5.2.3", "ajv": "5.2.3",
"ajv-keywords": "2.1.0", "ajv-keywords": "2.1.0",

View File

@@ -6,6 +6,7 @@ import { Subscription } from 'rxjs/Subscription';
import { SortOptions } from '../core/cache/models/sort-options.model'; import { SortOptions } from '../core/cache/models/sort-options.model';
import { CollectionDataService } from '../core/data/collection-data.service'; import { CollectionDataService } from '../core/data/collection-data.service';
import { ItemDataService } from '../core/data/item-data.service'; import { ItemDataService } from '../core/data/item-data.service';
import { PaginatedList } from '../core/data/paginated-list';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { MetadataService } from '../core/metadata/metadata.service'; import { MetadataService } from '../core/metadata/metadata.service';
@@ -30,7 +31,7 @@ import { PaginationComponentOptions } from '../shared/pagination/pagination-comp
}) })
export class CollectionPageComponent implements OnInit, OnDestroy { export class CollectionPageComponent implements OnInit, OnDestroy {
collectionRDObs: Observable<RemoteData<Collection>>; collectionRDObs: Observable<RemoteData<Collection>>;
itemRDObs: Observable<RemoteData<Item[]>>; itemRDObs: Observable<RemoteData<PaginatedList<Item>>>;
logoRDObs: Observable<RemoteData<Bitstream>>; logoRDObs: Observable<RemoteData<Bitstream>>;
paginationConfig: PaginationComponentOptions; paginationConfig: PaginationComponentOptions;
sortConfig: SortOptions; sortConfig: SortOptions;

View File

@@ -2,7 +2,7 @@
<div *ngIf="subCollectionsRD?.hasSucceeded" @fadeIn> <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 subCollectionsRD?.payload"> <li *ngFor="let collection of subCollectionsRD?.payload?.page">
<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>

View File

@@ -1,11 +1,12 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { CollectionDataService } from '../../core/data/collection-data.service'; import { CollectionDataService } from '../../core/data/collection-data.service';
import { PaginatedList } from '../../core/data/paginated-list';
import { RemoteData } from '../../core/data/remote-data'; 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',
@@ -14,7 +15,7 @@ import { Observable } from 'rxjs/Observable';
animations:[fadeIn] animations:[fadeIn]
}) })
export class CommunityPageSubCollectionListComponent implements OnInit { export class CommunityPageSubCollectionListComponent implements OnInit {
subCollectionsRDObs: Observable<RemoteData<Collection[]>>; subCollectionsRDObs: Observable<RemoteData<PaginatedList<Collection>>>;
constructor(private cds: CollectionDataService) { constructor(private cds: CollectionDataService) {

View File

@@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
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 { CommunityDataService } from '../../core/data/community-data.service';
import { PaginatedList } from '../../core/data/paginated-list';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
@@ -17,7 +18,7 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
animations: [fadeInOut] animations: [fadeInOut]
}) })
export class TopLevelCommunityListComponent { export class TopLevelCommunityListComponent {
communitiesRDObs: Observable<RemoteData<Community[]>>; communitiesRDObs: Observable<RemoteData<PaginatedList<Community>>>;
config: PaginationComponentOptions; config: PaginationComponentOptions;
sortConfig: SortOptions; sortConfig: SortOptions;

View File

@@ -8,7 +8,7 @@
[query]="query" [query]="query"
[scope]="(scopeObjectRDObs | async)?.payload" [scope]="(scopeObjectRDObs | async)?.payload"
[currentParams]="currentParams" [currentParams]="currentParams"
[scopes]="(scopeListRDObs | async)?.payload"> [scopes]="(scopeListRDObs | async)?.payload?.page">
</ds-search-form> </ds-search-form>
<div class="row"> <div class="row">
<div id="search-body" <div id="search-body"

View File

@@ -1,19 +1,20 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnDestroy, 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 { SortOptions } from '../core/cache/models/sort-options.model';
import { CommunityDataService } from '../core/data/community-data.service'; import { CommunityDataService } from '../core/data/community-data.service';
import { PaginatedList } from '../core/data/paginated-list';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { Community } from '../core/shared/community.model'; import { Community } from '../core/shared/community.model';
import { DSpaceObject } from '../core/shared/dspace-object.model'; import { DSpaceObject } from '../core/shared/dspace-object.model';
import { pushInOut } from '../shared/animations/push';
import { isNotEmpty } from '../shared/empty.util'; import { isNotEmpty } from '../shared/empty.util';
import { SearchOptions,ViewMode } from './search-options.model'; import { HostWindowService } from '../shared/host-window.service';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { SearchOptions, ViewMode } from './search-options.model';
import { SearchResult } from './search-result.model'; import { SearchResult } from './search-result.model';
import { SearchService } from './search-service/search.service'; import { SearchService } from './search-service/search.service';
import { pushInOut } from '../shared/animations/push';
import { HostWindowService } from '../shared/host-window.service';
import { SearchSidebarService } from './search-sidebar/search-sidebar.service'; import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { SortOptions } from '../core/cache/models/sort-options.model';
/** /**
* This component renders a simple item page. * This component renders a simple item page.
@@ -39,7 +40,7 @@ export class SearchPageComponent implements OnInit, OnDestroy {
currentParams = {}; currentParams = {};
searchOptions: SearchOptions; searchOptions: SearchOptions;
sortConfig: SortOptions; sortConfig: SortOptions;
scopeListRDObs: Observable<RemoteData<Community[]>>; scopeListRDObs: Observable<RemoteData<PaginatedList<Community>>>;
isMobileView: Observable<boolean>; isMobileView: Observable<boolean>;
constructor(private service: SearchService, constructor(private service: SearchService,

View File

@@ -1,23 +1,24 @@
import { Injectable, OnDestroy } from '@angular/core'; import { Injectable, OnDestroy } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data'; import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { SearchResult } from '../search-result.model';
import { ItemDataService } from '../../core/data/item-data.service';
import { PageInfo } from '../../core/shared/page-info.model';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { SearchOptions } from '../search-options.model';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { Metadatum } from '../../core/shared/metadatum.model';
import { Item } from '../../core/shared/item.model';
import { SearchFilterConfig } from './search-filter-config.model';
import { FilterType } from './filter-type.model';
import { FacetValue } from './facet-value.model';
import { ItemSearchResult } from '../../shared/object-collection/shared/item-search-result.model';
import { ViewMode } from '../../+search-page/search-options.model'; import { ViewMode } from '../../+search-page/search-options.model';
import { Router, NavigationExtras, ActivatedRoute } from '@angular/router';
import { RouteService } from '../../shared/route.service';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortOptions } from '../../core/cache/models/sort-options.model'; import { SortOptions } from '../../core/cache/models/sort-options.model';
import { ItemDataService } from '../../core/data/item-data.service';
import { PaginatedList } from '../../core/data/paginated-list';
import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { Item } from '../../core/shared/item.model';
import { Metadatum } from '../../core/shared/metadatum.model';
import { PageInfo } from '../../core/shared/page-info.model';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { ItemSearchResult } from '../../shared/object-collection/shared/item-search-result.model';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { RouteService } from '../../shared/route.service';
import { SearchOptions } from '../search-options.model';
import { SearchResult } from '../search-result.model';
import { FacetValue } from './facet-value.model';
import { FilterType } from './filter-type.model';
import { SearchFilterConfig } from './search-filter-config.model';
function shuffle(array: any[]) { function shuffle(array: any[]) {
let i = 0; let i = 0;
@@ -118,8 +119,7 @@ export class SearchService implements OnDestroy {
self += `&sortField=${searchOptions.sort.field}`; self += `&sortField=${searchOptions.sort.field}`;
} }
const errorMessage = undefined; const error = undefined;
const statusCode = '200';
const returningPageInfo = new PageInfo(); const returningPageInfo = new PageInfo();
if (isNotEmpty(searchOptions)) { if (isNotEmpty(searchOptions)) {
@@ -137,13 +137,12 @@ export class SearchService implements OnDestroy {
}); });
return itemsObs return itemsObs
.filter((rd: RemoteData<Item[]>) => rd.hasSucceeded) .filter((rd: RemoteData<PaginatedList<Item>>) => rd.hasSucceeded)
.map((rd: RemoteData<Item[]>) => { .map((rd: RemoteData<PaginatedList<Item>>) => {
const totalElements = rd.pageInfo.totalElements > 20 ? 20 : rd.pageInfo.totalElements; const totalElements = rd.payload.totalElements > 20 ? 20 : rd.payload.totalElements;
const pageInfo = Object.assign({}, rd.pageInfo, { totalElements: totalElements });
const payload = shuffle(rd.payload) const page = shuffle(rd.payload.page)
.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;
@@ -154,24 +153,20 @@ export class SearchService implements OnDestroy {
return mockResult; return mockResult;
}); });
const payload = Object.assign({}, rd.payload, { totalElements: totalElements, page });
return new RemoteData( return new RemoteData(
self,
rd.isRequestPending, rd.isRequestPending,
rd.isResponsePending, rd.isResponsePending,
rd.hasSucceeded, rd.hasSucceeded,
errorMessage, error,
statusCode,
pageInfo,
payload payload
) )
}).startWith(new RemoteData( }).startWith(new RemoteData(
'',
true, true,
false, false,
undefined, undefined,
undefined, undefined,
undefined,
undefined,
undefined undefined
)); ));
} }
@@ -180,17 +175,12 @@ export class SearchService implements OnDestroy {
const requestPending = false; const requestPending = false;
const responsePending = false; const responsePending = false;
const isSuccessful = true; const isSuccessful = true;
const errorMessage = undefined; const error = undefined;
const statusCode = '200';
const returningPageInfo = new PageInfo();
return Observable.of(new RemoteData( return Observable.of(new RemoteData(
'https://dspace7.4science.it/dspace-spring-rest/api/search',
requestPending, requestPending,
responsePending, responsePending,
isSuccessful, isSuccessful,
errorMessage, error,
statusCode,
returningPageInfo,
this.config this.config
)); ));
} }
@@ -198,12 +188,12 @@ export class SearchService implements OnDestroy {
getFacetValuesFor(searchFilterConfigName: string): Observable<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);
return this.routeService.getQueryParameterValues(filterConfig.paramName).map((selectedValues: string[]) => { return this.routeService.getQueryParameterValues(filterConfig.paramName).map((selectedValues: string[]) => {
const values: FacetValue[] = []; const payload: FacetValue[] = [];
const totalFilters = 13; const totalFilters = 13;
for (let i = 0; i < totalFilters; i++) { for (let i = 0; i < totalFilters; i++) {
const value = searchFilterConfigName + ' ' + (i + 1); const value = searchFilterConfigName + ' ' + (i + 1);
if (!selectedValues.includes(value)) { if (!selectedValues.includes(value)) {
values.push({ payload.push({
value: value, value: value,
count: Math.floor(Math.random() * 20) + 20 * (totalFilters - i), // make sure first results have the highest (random) count count: Math.floor(Math.random() * 20) + 20 * (totalFilters - i), // make sure first results have the highest (random) count
search: decodeURI(this.router.url) + (this.router.url.includes('?') ? '&' : '?') + filterConfig.paramName + '=' + value search: decodeURI(this.router.url) + (this.router.url.includes('?') ? '&' : '?') + filterConfig.paramName + '=' + value
@@ -213,18 +203,13 @@ export class SearchService implements OnDestroy {
const requestPending = false; const requestPending = false;
const responsePending = false; const responsePending = false;
const isSuccessful = true; const isSuccessful = true;
const errorMessage = undefined; const error = undefined;
const statusCode = '200';
const returningPageInfo = new PageInfo();
return new RemoteData( return new RemoteData(
'https://dspace7.4science.it/dspace-spring-rest/api/search',
requestPending, requestPending,
responsePending, responsePending,
isSuccessful, isSuccessful,
errorMessage, error,
statusCode, payload
returningPageInfo,
values
) )
} }
) )

View File

@@ -1,3 +1,5 @@
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
import { BrowseService } from './browse.service'; import { BrowseService } from './browse.service';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
@@ -73,20 +75,16 @@ describe('BrowseService', () => {
]; ];
function initMockResponseCacheService(isSuccessful: boolean) { function initMockResponseCacheService(isSuccessful: boolean) {
return jasmine.createSpyObj('responseCache', { const rcs = getMockResponseCacheService();
get: cold('b-', { (rcs.get as any).and.returnValue(cold('b-', {
b: { b: {
response: { response: {
isSuccessful, isSuccessful,
browseDefinitions, browseDefinitions,
}
} }
}) }
}); }));
} return rcs;
function initMockRequestService() {
return jasmine.createSpyObj('requestService', ['configure']);
} }
function initTestService() { function initTestService() {
@@ -106,7 +104,7 @@ describe('BrowseService', () => {
describe('if getEndpoint fires', () => { describe('if getEndpoint fires', () => {
beforeEach(() => { beforeEach(() => {
responseCache = initMockResponseCacheService(true); responseCache = initMockResponseCacheService(true);
requestService = initMockRequestService(); requestService = getMockRequestService();
service = initTestService(); service = initTestService();
spyOn(service, 'getEndpoint').and spyOn(service, 'getEndpoint').and
.returnValue(hot('--a-', { a: browsesEndpointURL })); .returnValue(hot('--a-', { a: browsesEndpointURL }));
@@ -157,7 +155,7 @@ describe('BrowseService', () => {
it('should configure a new BrowseEndpointRequest', () => { it('should configure a new BrowseEndpointRequest', () => {
const metadatumKey = 'dc.date.issued'; const metadatumKey = 'dc.date.issued';
const linkName = 'items'; const linkName = 'items';
const expected = new BrowseEndpointRequest(browsesEndpointURL); const expected = new BrowseEndpointRequest(requestService.generateRequestId(), browsesEndpointURL);
scheduler.schedule(() => service.getBrowseURLFor(metadatumKey, linkName).subscribe()); scheduler.schedule(() => service.getBrowseURLFor(metadatumKey, linkName).subscribe());
scheduler.flush(); scheduler.flush();
@@ -171,7 +169,7 @@ describe('BrowseService', () => {
describe('if getEndpoint doesn\'t fire', () => { describe('if getEndpoint doesn\'t fire', () => {
it('should return undefined', () => { it('should return undefined', () => {
responseCache = initMockResponseCacheService(true); responseCache = initMockResponseCacheService(true);
requestService = initMockRequestService(); requestService = getMockRequestService();
service = initTestService(); service = initTestService();
spyOn(service, 'getEndpoint').and spyOn(service, 'getEndpoint').and
.returnValue(hot('----')); .returnValue(hot('----'));
@@ -188,7 +186,7 @@ describe('BrowseService', () => {
describe('if the browses endpoint can\'t be retrieved', () => { describe('if the browses endpoint can\'t be retrieved', () => {
it('should throw an error', () => { it('should throw an error', () => {
responseCache = initMockResponseCacheService(false); responseCache = initMockResponseCacheService(false);
requestService = initMockRequestService(); requestService = getMockRequestService();
service = initTestService(); service = initTestService();
spyOn(service, 'getEndpoint').and spyOn(service, 'getEndpoint').and
.returnValue(hot('--a-', { a: browsesEndpointURL })); .returnValue(hot('--a-', { a: browsesEndpointURL }));

View File

@@ -40,7 +40,7 @@ export class BrowseService extends HALEndpointService {
return this.getEndpoint() return this.getEndpoint()
.filter((href: string) => isNotEmpty(href)) .filter((href: string) => isNotEmpty(href))
.distinctUntilChanged() .distinctUntilChanged()
.map((endpointURL: string) => new BrowseEndpointRequest(endpointURL)) .map((endpointURL: string) => new BrowseEndpointRequest(this.requestService.generateRequestId(), endpointURL))
.do((request: RestRequest) => this.requestService.configure(request)) .do((request: RestRequest) => this.requestService.configure(request))
.flatMap((request: RestRequest) => { .flatMap((request: RestRequest) => {
const [successResponse, errorResponse] = this.responseCache.get(request.href) const [successResponse, errorResponse] = this.responseCache.get(request.href)

View File

@@ -1,20 +1,21 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
import { PaginatedList } from '../../data/paginated-list';
import { RemoteData } from '../../data/remote-data';
import { RemoteDataError } from '../../data/remote-data-error';
import { GetRequest } from '../../data/request.models';
import { RequestEntry } from '../../data/request.reducer';
import { RequestService } from '../../data/request.service';
import { GenericConstructor } from '../../shared/generic-constructor';
import { NormalizedObjectFactory } from '../models/normalized-object-factory';
import { CacheableObject } from '../object-cache.reducer'; import { CacheableObject } from '../object-cache.reducer';
import { ObjectCacheService } from '../object-cache.service'; import { ObjectCacheService } from '../object-cache.service';
import { RequestService } from '../../data/request.service'; import { DSOSuccessResponse, ErrorResponse } from '../response-cache.models';
import { ResponseCacheService } from '../response-cache.service';
import { RequestEntry } from '../../data/request.reducer';
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
import { ResponseCacheEntry } from '../response-cache.reducer'; import { ResponseCacheEntry } from '../response-cache.reducer';
import { ErrorResponse, DSOSuccessResponse } from '../response-cache.models'; import { ResponseCacheService } from '../response-cache.service';
import { RemoteData } from '../../data/remote-data';
import { GenericConstructor } from '../../shared/generic-constructor';
import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators'; import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators';
import { NormalizedObjectFactory } from '../models/normalized-object-factory';
import { RestRequest } from '../../data/request.models';
import { PageInfo } from '../../shared/page-info.model';
@Injectable() @Injectable()
export class RemoteDataBuildService { export class RemoteDataBuildService {
@@ -37,10 +38,10 @@ export class RemoteDataBuildService {
this.objectCache.getRequestHrefBySelfLink(href)); this.objectCache.getRequestHrefBySelfLink(href));
const requestObs = Observable.race( const requestObs = Observable.race(
hrefObs.flatMap((href: string) => this.requestService.get(href)) hrefObs.flatMap((href: string) => this.requestService.getByHref(href))
.filter((entry) => hasValue(entry)), .filter((entry) => hasValue(entry)),
requestHrefObs.flatMap((requestHref) => requestHrefObs.flatMap((requestHref) =>
this.requestService.get(requestHref)).filter((entry) => hasValue(entry)) this.requestService.getByHref(requestHref)).filter((entry) => hasValue(entry))
); );
const responseCacheObs = Observable.race( const responseCacheObs = Observable.race(
@@ -87,33 +88,19 @@ export class RemoteDataBuildService {
(href: string, reqEntry: RequestEntry, resEntry: ResponseCacheEntry, payload: T) => { (href: string, reqEntry: RequestEntry, resEntry: ResponseCacheEntry, payload: T) => {
const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true; const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true;
const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false; const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false;
let isSuccessFul: boolean; let isSuccessful: boolean;
let errorMessage: string; let error: RemoteDataError;
let statusCode: string;
let pageInfo: PageInfo;
if (hasValue(resEntry) && hasValue(resEntry.response)) { if (hasValue(resEntry) && hasValue(resEntry.response)) {
isSuccessFul = resEntry.response.isSuccessful; isSuccessful = resEntry.response.isSuccessful;
errorMessage = isSuccessFul === false ? (resEntry.response as ErrorResponse).errorMessage : undefined; const errorMessage = isSuccessful === false ? (resEntry.response as ErrorResponse).errorMessage : undefined;
statusCode = resEntry.response.statusCode; error = new RemoteDataError(resEntry.response.statusCode, errorMessage);
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(
href,
requestPending, requestPending,
responsePending, responsePending,
isSuccessFul, isSuccessful,
errorMessage, error,
statusCode,
pageInfo,
payload payload
); );
}); });
@@ -122,17 +109,17 @@ export class RemoteDataBuildService {
buildList<TNormalized extends CacheableObject, TDomain>( buildList<TNormalized extends CacheableObject, TDomain>(
hrefObs: string | Observable<string>, hrefObs: string | Observable<string>,
normalizedType: GenericConstructor<TNormalized> normalizedType: GenericConstructor<TNormalized>
): Observable<RemoteData<TDomain[]>> { ): Observable<RemoteData<TDomain[] | PaginatedList<TDomain>>> {
if (typeof hrefObs === 'string') { if (typeof hrefObs === 'string') {
hrefObs = Observable.of(hrefObs); hrefObs = Observable.of(hrefObs);
} }
const requestObs = hrefObs.flatMap((href: string) => this.requestService.get(href)) const requestObs = hrefObs.flatMap((href: string) => this.requestService.getByHref(href))
.filter((entry) => hasValue(entry)); .filter((entry) => hasValue(entry));
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 payloadObs = responseCacheObs const tDomainListObs = 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[]) => {
@@ -146,6 +133,27 @@ export class RemoteDataBuildService {
.startWith([]) .startWith([])
.distinctUntilChanged(); .distinctUntilChanged();
const pageInfoObs = responseCacheObs
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => {
if (hasValue((entry.response as DSOSuccessResponse).pageInfo)) {
const resPageInfo = (entry.response as DSOSuccessResponse).pageInfo;
if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) {
return Object.assign({}, resPageInfo, { currentPage: resPageInfo.currentPage + 1 });
} else {
return resPageInfo;
}
}
});
const payloadObs = Observable.combineLatest(tDomainListObs, pageInfoObs, (tDomainList, pageInfo) => {
if (hasValue(pageInfo)) {
return new PaginatedList(pageInfo, tDomainList);
} else {
return tDomainList;
}
});
return this.toRemoteDataObservable(hrefObs, requestObs, responseCacheObs, payloadObs); return this.toRemoteDataObservable(hrefObs, requestObs, responseCacheObs, payloadObs);
} }
@@ -160,7 +168,7 @@ export class RemoteDataBuildService {
const resourceConstructor = NormalizedObjectFactory.getConstructor(resourceType); const resourceConstructor = NormalizedObjectFactory.getConstructor(resourceType);
if (Array.isArray(normalized[relationship])) { if (Array.isArray(normalized[relationship])) {
normalized[relationship].forEach((href: string) => { normalized[relationship].forEach((href: string) => {
this.requestService.configure(new RestRequest(href)) this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), href))
}); });
const rdArr = []; const rdArr = [];
@@ -174,7 +182,7 @@ export class RemoteDataBuildService {
links[relationship] = rdArr[0]; links[relationship] = rdArr[0];
} }
} else { } else {
this.requestService.configure(new RestRequest(normalized[relationship])); this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), normalized[relationship]));
// 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),
@@ -204,40 +212,37 @@ export class RemoteDataBuildService {
.map((d: RemoteData<T>) => d.isResponsePending) .map((d: RemoteData<T>) => d.isResponsePending)
.every((b: boolean) => b === true); .every((b: boolean) => b === true);
const isSuccessFul: boolean = arr const isSuccessful: boolean = arr
.map((d: RemoteData<T>) => d.hasSucceeded) .map((d: RemoteData<T>) => d.hasSucceeded)
.every((b: boolean) => b === true); .every((b: boolean) => b === true);
const errorMessage: string = arr const errorMessage: string = arr
.map((d: RemoteData<T>) => d.errorMessage) .map((d: RemoteData<T>) => d.error)
.map((e: string, idx: number) => { .map((e: RemoteDataError, idx: number) => {
if (hasValue(e)) { if (hasValue(e)) {
return `[${idx}]: ${e}`; return `[${idx}]: ${e.message}`;
} }
}).filter((e: string) => hasValue(e)) }).filter((e: string) => hasValue(e))
.join(', '); .join(', ');
const statusCode: string = arr const statusCode: string = arr
.map((d: RemoteData<T>) => d.statusCode) .map((d: RemoteData<T>) => d.error)
.map((c: string, idx: number) => { .map((e: RemoteDataError, idx: number) => {
if (hasValue(c)) { if (hasValue(e)) {
return `[${idx}]: ${c}`; return `[${idx}]: ${e.statusCode}`;
} }
}).filter((c: string) => hasValue(c)) }).filter((c: string) => hasValue(c))
.join(', '); .join(', ');
const pageInfo = undefined; const error = new RemoteDataError(statusCode, errorMessage);
const payload: T[] = arr.map((d: RemoteData<T>) => d.payload); const payload: T[] = arr.map((d: RemoteData<T>) => d.payload);
return new RemoteData( return new RemoteData(
`dspace-angular://aggregated/object/${new Date().getTime()}`,
requestPending, requestPending,
responsePending, responsePending,
isSuccessFul, isSuccessful,
errorMessage, error,
statusCode,
pageInfo,
payload payload
); );
}) })

View File

@@ -5,6 +5,12 @@ import {
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { CacheEntry } from './cache-entry'; import { CacheEntry } from './cache-entry';
export enum DirtyType {
Created = 'Created',
Updated = 'Updated',
Deleted = 'Deleted'
}
/** /**
* An interface to represent objects that can be cached * An interface to represent objects that can be cached
* *
@@ -13,6 +19,11 @@ import { CacheEntry } from './cache-entry';
export interface CacheableObject { export interface CacheableObject {
uuid?: string; uuid?: string;
self: string; self: string;
// isNew: boolean;
// dirtyType: DirtyType;
// hasDirtyAttributes: boolean;
// changedAttributes: AttributeDiffh;
// save(): void;
} }
/** /**

View File

@@ -2,20 +2,21 @@ import { Injectable } from '@angular/core';
import { MemoizedSelector, Store } from '@ngrx/store'; import { MemoizedSelector, Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { IndexName } from '../index/index.reducer';
import { ObjectCacheEntry, CacheableObject } from './object-cache.reducer'; import { ObjectCacheEntry, CacheableObject } from './object-cache.reducer';
import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions'; import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions';
import { hasNoValue } from '../../shared/empty.util'; import { hasNoValue } from '../../shared/empty.util';
import { GenericConstructor } from '../shared/generic-constructor'; import { GenericConstructor } from '../shared/generic-constructor';
import { CoreState } from '../core.reducers'; import { coreSelector, CoreState } from '../core.reducers';
import { keySelector } from '../shared/selectors'; import { pathSelector } from '../shared/selectors';
function selfLinkFromUuidSelector(uuid: string): MemoizedSelector<CoreState, string> { function selfLinkFromUuidSelector(uuid: string): MemoizedSelector<CoreState, string> {
return keySelector<string>('index/uuid', uuid); return pathSelector<CoreState, string>(coreSelector, 'index', IndexName.OBJECT, uuid);
} }
function entryFromSelfLinkSelector(selfLink: string): MemoizedSelector<CoreState, ObjectCacheEntry> { function entryFromSelfLinkSelector(selfLink: string): MemoizedSelector<CoreState, ObjectCacheEntry> {
return keySelector<ObjectCacheEntry>('data/object', selfLink); return pathSelector<CoreState, ObjectCacheEntry>(coreSelector, 'data/object', selfLink);
} }
/** /**
@@ -60,7 +61,7 @@ export class ObjectCacheService {
* the cached plain javascript object in to an instance of * the cached plain javascript object in to an instance of
* a class. * a class.
* *
* e.g. get('c96588c6-72d3-425d-9d47-fa896255a695', Item) * e.g. getByUUID('c96588c6-72d3-425d-9d47-fa896255a695', Item)
* *
* @param uuid * @param uuid
* The UUID of the object to get * The UUID of the object to get

View File

@@ -7,11 +7,11 @@ import { ResponseCacheEntry } from './response-cache.reducer';
import { hasNoValue } from '../../shared/empty.util'; import { hasNoValue } 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 { coreSelector, CoreState } from '../core.reducers';
import { keySelector } from '../shared/selectors'; import { pathSelector } from '../shared/selectors';
function entryFromKeySelector(key: string): MemoizedSelector<CoreState, ResponseCacheEntry> { function entryFromKeySelector(key: string): MemoizedSelector<CoreState, ResponseCacheEntry> {
return keySelector<ResponseCacheEntry>('data/response', key); return pathSelector<CoreState, ResponseCacheEntry>(coreSelector, 'data/response', key);
} }
/** /**

View File

@@ -1,6 +1,7 @@
import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/Rx'; import { TestScheduler } from 'rxjs/Rx';
import { GlobalConfig } from '../../../config'; import { GlobalConfig } from '../../../config';
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';
import { ConfigService } from './config.service'; import { ConfigService } from './config.service';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
@@ -38,10 +39,6 @@ describe('ConfigService', () => {
const scopedEndpoint = `${serviceEndpoint}/${scopeName}`; const scopedEndpoint = `${serviceEndpoint}/${scopeName}`;
const searchEndpoint = `${serviceEndpoint}/${BROWSE}?uuid=${scopeID}`; const searchEndpoint = `${serviceEndpoint}/${BROWSE}?uuid=${scopeID}`;
function initMockRequestService(): RequestService {
return jasmine.createSpyObj('requestService', ['configure']);
}
function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService { function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService {
return jasmine.createSpyObj('responseCache', { return jasmine.createSpyObj('responseCache', {
get: cold('c-', { get: cold('c-', {
@@ -60,7 +57,7 @@ describe('ConfigService', () => {
beforeEach(() => { beforeEach(() => {
responseCache = initMockResponseCacheService(true); responseCache = initMockResponseCacheService(true);
requestService = initMockRequestService(); requestService = getMockRequestService();
service = initTestService(); service = initTestService();
scheduler = getTestScheduler(); scheduler = getTestScheduler();
spyOn(service, 'getEndpoint').and spyOn(service, 'getEndpoint').and
@@ -70,7 +67,7 @@ describe('ConfigService', () => {
describe('getConfigByHref', () => { describe('getConfigByHref', () => {
it('should configure a new ConfigRequest', () => { it('should configure a new ConfigRequest', () => {
const expected = new ConfigRequest(scopedEndpoint); const expected = new ConfigRequest(requestService.generateRequestId(), scopedEndpoint);
scheduler.schedule(() => service.getConfigByHref(scopedEndpoint).subscribe()); scheduler.schedule(() => service.getConfigByHref(scopedEndpoint).subscribe());
scheduler.flush(); scheduler.flush();
@@ -81,7 +78,7 @@ describe('ConfigService', () => {
describe('getConfigByName', () => { describe('getConfigByName', () => {
it('should configure a new ConfigRequest', () => { it('should configure a new ConfigRequest', () => {
const expected = new ConfigRequest(scopedEndpoint); const expected = new ConfigRequest(requestService.generateRequestId(), scopedEndpoint);
scheduler.schedule(() => service.getConfigByName(scopeName).subscribe()); scheduler.schedule(() => service.getConfigByName(scopeName).subscribe());
scheduler.flush(); scheduler.flush();
@@ -93,7 +90,7 @@ describe('ConfigService', () => {
it('should configure a new ConfigRequest', () => { it('should configure a new ConfigRequest', () => {
findOptions.scopeID = scopeID; findOptions.scopeID = scopeID;
const expected = new ConfigRequest(searchEndpoint); const expected = new ConfigRequest(requestService.generateRequestId(), searchEndpoint);
scheduler.schedule(() => service.getConfigBySearch(findOptions).subscribe()); scheduler.schedule(() => service.getConfigBySearch(findOptions).subscribe());
scheduler.flush(); scheduler.flush();

View File

@@ -75,14 +75,14 @@ export abstract class ConfigService extends HALEndpointService {
return this.getEndpoint() return this.getEndpoint()
.filter((href: string) => isNotEmpty(href)) .filter((href: string) => isNotEmpty(href))
.distinctUntilChanged() .distinctUntilChanged()
.map((endpointURL: string) => new ConfigRequest(endpointURL)) .map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL))
.do((request: RestRequest) => this.requestService.configure(request)) .do((request: RestRequest) => this.requestService.configure(request))
.flatMap((request: RestRequest) => this.getConfig(request)) .flatMap((request: RestRequest) => this.getConfig(request))
.distinctUntilChanged(); .distinctUntilChanged();
} }
public getConfigByHref(href: string): Observable<ConfigData> { public getConfigByHref(href: string): Observable<ConfigData> {
const request = new ConfigRequest(href); const request = new ConfigRequest(this.requestService.generateRequestId(), href);
this.requestService.configure(request); this.requestService.configure(request);
return this.getConfig(request); return this.getConfig(request);
@@ -93,7 +93,7 @@ export abstract class ConfigService extends HALEndpointService {
.map((endpoint: string) => this.getConfigByNameHref(endpoint, name)) .map((endpoint: string) => this.getConfigByNameHref(endpoint, name))
.filter((href: string) => isNotEmpty(href)) .filter((href: string) => isNotEmpty(href))
.distinctUntilChanged() .distinctUntilChanged()
.map((endpointURL: string) => new ConfigRequest(endpointURL)) .map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL))
.do((request: RestRequest) => this.requestService.configure(request)) .do((request: RestRequest) => this.requestService.configure(request))
.flatMap((request: RestRequest) => this.getConfig(request)) .flatMap((request: RestRequest) => this.getConfig(request))
.distinctUntilChanged(); .distinctUntilChanged();
@@ -104,7 +104,7 @@ export abstract class ConfigService extends HALEndpointService {
.map((endpoint: string) => this.getConfigSearchHref(endpoint, options)) .map((endpoint: string) => this.getConfigSearchHref(endpoint, options))
.filter((href: string) => isNotEmpty(href)) .filter((href: string) => isNotEmpty(href))
.distinctUntilChanged() .distinctUntilChanged()
.map((endpointURL: string) => new ConfigRequest(endpointURL)) .map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL))
.do((request: RestRequest) => this.requestService.configure(request)) .do((request: RestRequest) => this.requestService.configure(request))
.flatMap((request: RestRequest) => this.getConfig(request)) .flatMap((request: RestRequest) => this.getConfig(request))
.distinctUntilChanged(); .distinctUntilChanged();

View File

@@ -1,7 +1,7 @@
import { ObjectCacheEffects } from './cache/object-cache.effects'; import { ObjectCacheEffects } from './cache/object-cache.effects';
import { ResponseCacheEffects } from './cache/response-cache.effects'; import { ResponseCacheEffects } from './cache/response-cache.effects';
import { UUIDIndexEffects } from './index/uuid-index.effects'; import { UUIDIndexEffects } from './index/index.effects';
import { RequestEffects } from './data/request.effects'; import { RequestEffects } from './data/request.effects';
export const coreEffects = [ export const coreEffects = [

View File

@@ -37,6 +37,7 @@ import { RouteService } from '../shared/route.service';
import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service'; import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service';
import { SubmissionFormsConfigService } from './config/submission-forms-config.service'; import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
import { SubmissionSectionsConfigService } from './config/submission-sections-config.service'; import { SubmissionSectionsConfigService } from './config/submission-sections-config.service';
import { UUIDService } from './shared/uuid.service';
const IMPORTS = [ const IMPORTS = [
CommonModule, CommonModule,
@@ -75,6 +76,7 @@ const PROVIDERS = [
SubmissionDefinitionsConfigService, SubmissionDefinitionsConfigService,
SubmissionFormsConfigService, SubmissionFormsConfigService,
SubmissionSectionsConfigService, SubmissionSectionsConfigService,
UUIDService,
{ provide: NativeWindowService, useFactory: NativeWindowFactory } { provide: NativeWindowService, useFactory: NativeWindowFactory }
]; ];

View File

@@ -2,21 +2,21 @@ import { ActionReducerMap, createFeatureSelector } from '@ngrx/store';
import { responseCacheReducer, ResponseCacheState } from './cache/response-cache.reducer'; import { responseCacheReducer, ResponseCacheState } from './cache/response-cache.reducer';
import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer'; import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer';
import { uuidIndexReducer, UUIDIndexState } from './index/uuid-index.reducer'; import { indexReducer, IndexState } from './index/index.reducer';
import { requestReducer, RequestState } from './data/request.reducer'; import { requestReducer, RequestState } from './data/request.reducer';
export interface CoreState { export interface CoreState {
'data/object': ObjectCacheState, 'data/object': ObjectCacheState,
'data/response': ResponseCacheState, 'data/response': ResponseCacheState,
'data/request': RequestState, 'data/request': RequestState,
'index/uuid': UUIDIndexState 'index': IndexState
} }
export const coreReducers: ActionReducerMap<CoreState> = { export const coreReducers: ActionReducerMap<CoreState> = {
'data/object': objectCacheReducer, 'data/object': objectCacheReducer,
'data/response': responseCacheReducer, 'data/response': responseCacheReducer,
'data/request': requestReducer, 'data/request': requestReducer,
'index/uuid': uuidIndexReducer 'index': indexReducer
}; };
export const coreSelector = createFeatureSelector<CoreState>('core'); export const coreSelector = createFeatureSelector<CoreState>('core');

View File

@@ -12,7 +12,7 @@ describe('BrowseResponseParsingService', () => {
}); });
describe('parse', () => { describe('parse', () => {
const validRequest = new BrowseEndpointRequest('https://rest.api/discover/browses'); const validRequest = new BrowseEndpointRequest('clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78', 'https://rest.api/discover/browses');
const validResponse = { const validResponse = {
payload: { payload: {

View File

@@ -2,6 +2,7 @@ import { Store } from '@ngrx/store';
import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/Rx'; import { TestScheduler } from 'rxjs/Rx';
import { GlobalConfig } from '../../../config'; import { GlobalConfig } from '../../../config';
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { NormalizedCommunity } from '../cache/models/normalized-community.model'; import { NormalizedCommunity } from '../cache/models/normalized-community.model';
import { CacheableObject } from '../cache/object-cache.reducer'; import { CacheableObject } from '../cache/object-cache.reducer';
@@ -62,10 +63,6 @@ describe('ComColDataService', () => {
}); });
} }
function initMockRequestService(): RequestService {
return jasmine.createSpyObj('requestService', ['configure']);
}
function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService { function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService {
return jasmine.createSpyObj('responseCache', { return jasmine.createSpyObj('responseCache', {
get: cold('c-', { get: cold('c-', {
@@ -105,12 +102,12 @@ describe('ComColDataService', () => {
it('should configure a new FindByIDRequest for the scope Community', () => { it('should configure a new FindByIDRequest for the scope Community', () => {
cds = initMockCommunityDataService(); cds = initMockCommunityDataService();
requestService = initMockRequestService(); requestService = getMockRequestService();
objectCache = initMockObjectCacheService(); objectCache = initMockObjectCacheService();
responseCache = initMockResponseCacheService(true); responseCache = initMockResponseCacheService(true);
service = initTestService(); service = initTestService();
const expected = new FindByIDRequest(communityEndpoint, scopeID); const expected = new FindByIDRequest(requestService.generateRequestId(), communityEndpoint, scopeID);
scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe()); scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe());
scheduler.flush(); scheduler.flush();
@@ -121,7 +118,7 @@ describe('ComColDataService', () => {
describe('if the scope Community can be found', () => { describe('if the scope Community can be found', () => {
beforeEach(() => { beforeEach(() => {
cds = initMockCommunityDataService(); cds = initMockCommunityDataService();
requestService = initMockRequestService(); requestService = getMockRequestService();
objectCache = initMockObjectCacheService(); objectCache = initMockObjectCacheService();
responseCache = initMockResponseCacheService(true); responseCache = initMockResponseCacheService(true);
service = initTestService(); service = initTestService();
@@ -144,7 +141,7 @@ describe('ComColDataService', () => {
describe('if the scope Community can\'t be found', () => { describe('if the scope Community can\'t be found', () => {
beforeEach(() => { beforeEach(() => {
cds = initMockCommunityDataService(); cds = initMockCommunityDataService();
requestService = initMockRequestService(); requestService = getMockRequestService();
objectCache = initMockObjectCacheService(); objectCache = initMockObjectCacheService();
responseCache = initMockResponseCacheService(false); responseCache = initMockResponseCacheService(false);
service = initTestService(); service = initTestService();
@@ -161,7 +158,7 @@ describe('ComColDataService', () => {
describe('if the scope is not specified', () => { describe('if the scope is not specified', () => {
beforeEach(() => { beforeEach(() => {
cds = initMockCommunityDataService(); cds = initMockCommunityDataService();
requestService = initMockRequestService(); requestService = getMockRequestService();
objectCache = initMockObjectCacheService(); objectCache = initMockObjectCacheService();
responseCache = initMockResponseCacheService(true); responseCache = initMockResponseCacheService(true);
service = initTestService(); service = initTestService();

View File

@@ -33,7 +33,7 @@ export abstract class ComColDataService<TNormalized extends CacheableObject, TDo
.filter((href: string) => isNotEmpty(href)) .filter((href: string) => isNotEmpty(href))
.take(1) .take(1)
.do((href: string) => { .do((href: string) => {
const request = new FindByIDRequest(href, scopeID); const request = new FindByIDRequest(this.requestService.generateRequestId(), href, scopeID);
this.requestService.configure(request); this.requestService.configure(request);
}); });

View File

@@ -1,4 +1,5 @@
import { ConfigSuccessResponse, ErrorResponse } from '../cache/response-cache.models'; import { ConfigSuccessResponse, ErrorResponse } from '../cache/response-cache.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { ConfigResponseParsingService } from './config-response-parsing.service'; import { ConfigResponseParsingService } from './config-response-parsing.service';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { GlobalConfig } from '../../../config/global-config.interface'; import { GlobalConfig } from '../../../config/global-config.interface';
@@ -21,7 +22,7 @@ describe('ConfigResponseParsingService', () => {
}); });
describe('parse', () => { describe('parse', () => {
const validRequest = new ConfigRequest('https://rest.api/config/submissiondefinitions/traditional'); const validRequest = new ConfigRequest('69f375b5-19f4-4453-8c7a-7dc5c55aafbb', 'https://rest.api/config/submissiondefinitions/traditional');
const validResponse = { const validResponse = {
payload: { payload: {

View File

@@ -8,10 +8,11 @@ import { ResponseCacheService } from '../cache/response-cache.service';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { GenericConstructor } from '../shared/generic-constructor'; import { GenericConstructor } from '../shared/generic-constructor';
import { HALEndpointService } from '../shared/hal-endpoint.service'; 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'; import { URLCombiner } from '../url-combiner/url-combiner';
import { PaginatedList } from './paginated-list';
import { RemoteData } from './remote-data';
import { FindAllOptions, FindAllRequest, FindByIDRequest, GetRequest } from './request.models';
import { RequestService } from './request.service';
export abstract class DataService<TNormalized extends CacheableObject, TDomain> extends HALEndpointService { export abstract class DataService<TNormalized extends CacheableObject, TDomain> extends HALEndpointService {
protected abstract responseCache: ResponseCacheService; protected abstract responseCache: ResponseCacheService;
@@ -63,7 +64,7 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
} }
} }
findAll(options: FindAllOptions = {}): Observable<RemoteData<TDomain[]>> { findAll(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<TDomain>>> {
const hrefObs = this.getEndpoint().filter((href: string) => isNotEmpty(href)) const hrefObs = this.getEndpoint().filter((href: string) => isNotEmpty(href))
.flatMap((endpoint: string) => this.getFindAllHref(endpoint, options)); .flatMap((endpoint: string) => this.getFindAllHref(endpoint, options));
@@ -71,11 +72,11 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
.filter((href: string) => hasValue(href)) .filter((href: string) => hasValue(href))
.take(1) .take(1)
.subscribe((href: string) => { .subscribe((href: string) => {
const request = new FindAllRequest(href, options); const request = new FindAllRequest(this.requestService.generateRequestId(), href, options);
this.requestService.configure(request); this.requestService.configure(request);
}); });
return this.rdbService.buildList<TNormalized, TDomain>(hrefObs, this.normalizedResourceType); return this.rdbService.buildList<TNormalized, TDomain>(hrefObs, this.normalizedResourceType) as Observable<RemoteData<PaginatedList<TDomain>>>;
} }
getFindByIDHref(endpoint, resourceID): string { getFindByIDHref(endpoint, resourceID): string {
@@ -90,7 +91,7 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
.filter((href: string) => hasValue(href)) .filter((href: string) => hasValue(href))
.take(1) .take(1)
.subscribe((href: string) => { .subscribe((href: string) => {
const request = new FindByIDRequest(href, id); const request = new FindByIDRequest(this.requestService.generateRequestId(), href, id);
this.requestService.configure(request); this.requestService.configure(request);
}); });
@@ -98,8 +99,25 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
} }
findByHref(href: string): Observable<RemoteData<TDomain>> { findByHref(href: string): Observable<RemoteData<TDomain>> {
this.requestService.configure(new RestRequest(href)); this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), href));
return this.rdbService.buildSingle<TNormalized, TDomain>(href, this.normalizedResourceType); return this.rdbService.buildSingle<TNormalized, TDomain>(href, this.normalizedResourceType);
} }
// TODO implement, after the structure of the REST server's POST response is finalized
// create(dso: DSpaceObject): Observable<RemoteData<TDomain>> {
// const postHrefObs = this.getEndpoint();
//
// // TODO ID is unknown at this point
// const idHrefObs = postHrefObs.map((href: string) => this.getFindByIDHref(href, dso.id));
//
// postHrefObs
// .filter((href: string) => hasValue(href))
// .take(1)
// .subscribe((href: string) => {
// const request = new RestRequest(this.requestService.generateRequestId(), href, RestRequestMethod.Post, dso);
// this.requestService.configure(request);
// });
//
// return this.rdbService.buildSingle<TNormalized, TDomain>(idHrefObs, this.normalizedResourceType);
// }
} }

View File

@@ -0,0 +1,42 @@
import { PageInfo } from '../shared/page-info.model';
export class PaginatedList<T> {
constructor(
private pageInfo: PageInfo,
public page: T[]
) {
}
get elementsPerPage(): number {
return this.pageInfo.elementsPerPage;
}
set elementsPerPage(value: number) {
this.pageInfo.elementsPerPage = value;
}
get totalElements(): number {
return this.pageInfo.totalElements;
}
set totalElements(value: number) {
this.pageInfo.totalElements = value;
}
get totalPages(): number {
return this.pageInfo.totalPages;
}
set totalPages(value: number) {
this.pageInfo.totalPages = value;
}
get currentPage(): number {
return this.pageInfo.currentPage;
}
set currentPage(value: number) {
this.pageInfo.currentPage = value;
}
}

View File

@@ -0,0 +1,7 @@
export class RemoteDataError {
constructor(
public statusCode: string,
public message: string
) {
}
}

View File

@@ -1,5 +1,5 @@
import { PageInfo } from '../shared/page-info.model';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { RemoteDataError } from './remote-data-error';
export enum RemoteDataState { export enum RemoteDataState {
RequestPending = 'RequestPending', RequestPending = 'RequestPending',
@@ -13,21 +13,18 @@ export enum RemoteDataState {
*/ */
export class RemoteData<T> { export class RemoteData<T> {
constructor( constructor(
public self: string,
private requestPending: boolean, private requestPending: boolean,
private responsePending: boolean, private responsePending: boolean,
private isSuccessFul: boolean, private isSuccessful: boolean,
public errorMessage: string, public error: RemoteDataError,
public statusCode: string,
public pageInfo: PageInfo,
public payload: T public payload: T
) { ) {
} }
get state(): RemoteDataState { get state(): RemoteDataState {
if (this.isSuccessFul === true && hasValue(this.payload)) { if (this.isSuccessful === true && hasValue(this.payload)) {
return RemoteDataState.Success return RemoteDataState.Success
} else if (this.isSuccessFul === false) { } else if (this.isSuccessful === false) {
return RemoteDataState.Failed return RemoteDataState.Failed
} else if (this.requestPending === true) { } else if (this.requestPending === true) {
return RemoteDataState.RequestPending return RemoteDataState.RequestPending

View File

@@ -27,8 +27,14 @@ export class RequestExecuteAction implements Action {
type = RequestActionTypes.EXECUTE; type = RequestActionTypes.EXECUTE;
payload: string; payload: string;
constructor(key: string) { /**
this.payload = key * Create a new RequestExecuteAction
*
* @param uuid
* the request's uuid
*/
constructor(uuid: string) {
this.payload = uuid
} }
} }
@@ -42,11 +48,11 @@ export class RequestCompleteAction implements Action {
/** /**
* Create a new RequestCompleteAction * Create a new RequestCompleteAction
* *
* @param key * @param uuid
* the key under which this request is stored, * the request's uuid
*/ */
constructor(key: string) { constructor(uuid: string) {
this.payload = key; this.payload = uuid;
} }
} }
/* tslint:enable:max-classes-per-file */ /* tslint:enable:max-classes-per-file */

View File

@@ -1,18 +1,23 @@
import { Inject, Injectable, Injector } from '@angular/core'; import { Inject, Injectable, Injector } from '@angular/core';
import { Request } from '@angular/http';
import { RequestArgs } from '@angular/http/src/interfaces';
import { Actions, Effect } from '@ngrx/effects'; import { Actions, Effect } from '@ngrx/effects';
// tslint:disable-next-line:import-blacklist // tslint:disable-next-line:import-blacklist
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
import { isNotEmpty } from '../../shared/empty.util';
import { ErrorResponse, RestResponse } from '../cache/response-cache.models'; import { ErrorResponse, RestResponse } from '../cache/response-cache.models';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service'; import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
import { RequestActionTypes, RequestCompleteAction, RequestExecuteAction } from './request.actions'; import { RequestActionTypes, RequestCompleteAction, RequestExecuteAction } from './request.actions';
import { RequestError } from './request.models'; import { RequestError, RestRequest } from './request.models';
import { RequestEntry } from './request.reducer'; import { RequestEntry } from './request.reducer';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory';
@Injectable() @Injectable()
export class RequestEffects { export class RequestEffects {
@@ -20,18 +25,24 @@ export class RequestEffects {
@Effect() execute = this.actions$ @Effect() execute = this.actions$
.ofType(RequestActionTypes.EXECUTE) .ofType(RequestActionTypes.EXECUTE)
.flatMap((action: RequestExecuteAction) => { .flatMap((action: RequestExecuteAction) => {
return this.requestService.get(action.payload) return this.requestService.getByUUID(action.payload)
.take(1); .take(1);
}) })
.flatMap((entry: RequestEntry) => { .map((entry: RequestEntry) => entry.request)
return this.restApi.get(entry.request.href) .flatMap((request: RestRequest) => {
let body;
if (isNotEmpty(request.body)) {
const serializer = new DSpaceRESTv2Serializer(NormalizedObjectFactory.getConstructor(request.body.type));
body = JSON.stringify(serializer.serialize(request.body));
}
return this.restApi.request(request.method, request.href, body)
.map((data: DSpaceRESTV2Response) => .map((data: DSpaceRESTV2Response) =>
this.injector.get(entry.request.getResponseParser()).parse(entry.request, data)) this.injector.get(request.getResponseParser()).parse(request, data))
.do((response: RestResponse) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive)) .do((response: RestResponse) => this.responseCache.add(request.href, response, this.EnvConfig.cache.msToLive))
.map((response: RestResponse) => new RequestCompleteAction(entry.request.href)) .map((response: RestResponse) => new RequestCompleteAction(request.uuid))
.catch((error: RequestError) => Observable.of(new ErrorResponse(error)) .catch((error: RequestError) => Observable.of(new ErrorResponse(error))
.do((response: RestResponse) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive)) .do((response: RestResponse) => this.responseCache.add(request.href, response, this.EnvConfig.cache.msToLive))
.map((response: RestResponse) => new RequestCompleteAction(entry.request.href))); .map((response: RestResponse) => new RequestCompleteAction(request.uuid)));
}); });
constructor( constructor(

View File

@@ -9,22 +9,117 @@ import { BrowseResponseParsingService } from './browse-response-parsing.service'
import { ConfigResponseParsingService } from './config-response-parsing.service'; import { ConfigResponseParsingService } from './config-response-parsing.service';
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
export class RestRequest {
/**
* Represents a Request Method.
*
* I didn't reuse the RequestMethod enum in @angular/http because
* it uses numbers. The string values here are more clear when
* debugging.
*
* The ones commented out are still unsupported in the rest of the codebase
*/
export enum RestRequestMethod {
Get = 'GET',
Post = 'POST',
Put = 'PUT',
Delete = 'DELETE',
Options = 'OPTIONS',
Head = 'HEAD',
Patch = 'PATCH'
}
export abstract class RestRequest {
constructor( constructor(
public uuid: string,
public href: string, public href: string,
) { } public method: RestRequestMethod = RestRequestMethod.Get,
public body?: any
) {
}
getResponseParser(): GenericConstructor<ResponseParsingService> { getResponseParser(): GenericConstructor<ResponseParsingService> {
return DSOResponseParsingService; return DSOResponseParsingService;
} }
} }
export class FindByIDRequest extends RestRequest { export class GetRequest extends RestRequest {
constructor( constructor(
public uuid: string,
public href: string,
public body?: any
) {
super(uuid, href, RestRequestMethod.Get, body)
}
}
export class PostRequest extends RestRequest {
constructor(
public uuid: string,
public href: string,
public body?: any
) {
super(uuid, href, RestRequestMethod.Post, body)
}
}
export class PutRequest extends RestRequest {
constructor(
public uuid: string,
public href: string,
public body?: any
) {
super(uuid, href, RestRequestMethod.Put, body)
}
}
export class DeleteRequest extends RestRequest {
constructor(
public uuid: string,
public href: string,
public body?: any
) {
super(uuid, href, RestRequestMethod.Delete, body)
}
}
export class OptionsRequest extends RestRequest {
constructor(
public uuid: string,
public href: string,
public body?: any
) {
super(uuid, href, RestRequestMethod.Options, body)
}
}
export class HeadRequest extends RestRequest {
constructor(
public uuid: string,
public href: string,
public body?: any
) {
super(uuid, href, RestRequestMethod.Head, body)
}
}
export class PatchRequest extends RestRequest {
constructor(
public uuid: string,
public href: string,
public body?: any
) {
super(uuid, href, RestRequestMethod.Patch, body)
}
}
export class FindByIDRequest extends GetRequest {
constructor(
uuid: string,
href: string, href: string,
public resourceID: string public resourceID: string
) { ) {
super(href); super(uuid, href);
} }
} }
@@ -35,19 +130,20 @@ export class FindAllOptions {
sort?: SortOptions; sort?: SortOptions;
} }
export class FindAllRequest extends RestRequest { export class FindAllRequest extends GetRequest {
constructor( constructor(
uuid: string,
href: string, href: string,
public options?: FindAllOptions, public options?: FindAllOptions,
) { ) {
super(href); super(uuid, href);
} }
} }
export class RootEndpointRequest extends RestRequest { export class RootEndpointRequest extends GetRequest {
constructor(EnvConfig: GlobalConfig) { constructor(uuid: string, EnvConfig: GlobalConfig) {
const href = new RESTURLCombiner(EnvConfig, '/').toString(); const href = new RESTURLCombiner(EnvConfig, '/').toString();
super(href); super(uuid, href);
} }
getResponseParser(): GenericConstructor<ResponseParsingService> { getResponseParser(): GenericConstructor<ResponseParsingService> {
@@ -55,9 +151,9 @@ export class RootEndpointRequest extends RestRequest {
} }
} }
export class BrowseEndpointRequest extends RestRequest { export class BrowseEndpointRequest extends GetRequest {
constructor(href: string) { constructor(uuid: string, href: string) {
super(href); super(uuid, href);
} }
getResponseParser(): GenericConstructor<ResponseParsingService> { getResponseParser(): GenericConstructor<ResponseParsingService> {
@@ -65,9 +161,9 @@ export class BrowseEndpointRequest extends RestRequest {
} }
} }
export class ConfigRequest extends RestRequest { export class ConfigRequest extends GetRequest {
constructor(href: string) { constructor(uuid: string, href: string) {
super(href); super(uuid, href);
} }
getResponseParser(): GenericConstructor<ResponseParsingService> { getResponseParser(): GenericConstructor<ResponseParsingService> {

View File

@@ -4,7 +4,7 @@ import { requestReducer, RequestState } from './request.reducer';
import { import {
RequestCompleteAction, RequestConfigureAction, RequestExecuteAction RequestCompleteAction, RequestConfigureAction, RequestExecuteAction
} from './request.actions'; } from './request.actions';
import { RestRequest } from './request.models'; import { GetRequest, RestRequest } from './request.models';
class NullAction extends RequestCompleteAction { class NullAction extends RequestCompleteAction {
type = null; type = null;
@@ -16,11 +16,13 @@ class NullAction extends RequestCompleteAction {
} }
describe('requestReducer', () => { describe('requestReducer', () => {
const id1 = 'clients/eca2ea1d-6a6a-4f62-8907-176d5fec5014';
const id2 = 'clients/eb7cde2e-a03f-4f0b-ac5d-888a4ef2b4eb';
const link1 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/567a639f-f5ff-4126-807c-b7d0910808c8'; const link1 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/567a639f-f5ff-4126-807c-b7d0910808c8';
const link2 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/1911e8a4-6939-490c-b58b-a5d70f8d91fb'; const link2 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/1911e8a4-6939-490c-b58b-a5d70f8d91fb';
const testState: RequestState = { const testState: RequestState = {
[link1]: { [id1]: {
request: new RestRequest(link1), request: new GetRequest(id1, link1),
requestPending: false, requestPending: false,
responsePending: false, responsePending: false,
completed: false completed: false
@@ -44,37 +46,40 @@ describe('requestReducer', () => {
it('should add the new RestRequest and set \'requestPending\' to true, \'responsePending\' to false and \'completed\' to false for the given RestRequest in the state, in response to a CONFIGURE action', () => { it('should add the new RestRequest and set \'requestPending\' to true, \'responsePending\' to false and \'completed\' to false for the given RestRequest in the state, in response to a CONFIGURE action', () => {
const state = testState; const state = testState;
const request = new RestRequest(link2); const request = new GetRequest(id2, link2);
const action = new RequestConfigureAction(request); const action = new RequestConfigureAction(request);
const newState = requestReducer(state, action); const newState = requestReducer(state, action);
expect(newState[link2].request.href).toEqual(link2); expect(newState[id2].request.uuid).toEqual(id2);
expect(newState[link2].requestPending).toEqual(true); expect(newState[id2].request.href).toEqual(link2);
expect(newState[link2].responsePending).toEqual(false); expect(newState[id2].requestPending).toEqual(true);
expect(newState[link2].completed).toEqual(false); expect(newState[id2].responsePending).toEqual(false);
expect(newState[id2].completed).toEqual(false);
}); });
it('should set \'requestPending\' to false, \'responsePending\' to true and leave \'completed\' untouched for the given RestRequest in the state, in response to an EXECUTE action', () => { it('should set \'requestPending\' to false, \'responsePending\' to true and leave \'completed\' untouched for the given RestRequest in the state, in response to an EXECUTE action', () => {
const state = testState; const state = testState;
const action = new RequestExecuteAction(link1); const action = new RequestExecuteAction(id1);
const newState = requestReducer(state, action); const newState = requestReducer(state, action);
expect(newState[link1].request.href).toEqual(link1); expect(newState[id1].request.uuid).toEqual(id1);
expect(newState[link1].requestPending).toEqual(false); expect(newState[id1].request.href).toEqual(link1);
expect(newState[link1].responsePending).toEqual(true); expect(newState[id1].requestPending).toEqual(false);
expect(newState[link1].completed).toEqual(state[link1].completed); expect(newState[id1].responsePending).toEqual(true);
expect(newState[id1].completed).toEqual(state[id1].completed);
}); });
it('should leave \'requestPending\' untouched, set \'responsePending\' to false and \'completed\' to true for the given RestRequest in the state, in response to a COMPLETE action', () => { it('should leave \'requestPending\' untouched, set \'responsePending\' to false and \'completed\' to true for the given RestRequest in the state, in response to a COMPLETE action', () => {
const state = testState; const state = testState;
const action = new RequestCompleteAction(link1); const action = new RequestCompleteAction(id1);
const newState = requestReducer(state, action); const newState = requestReducer(state, action);
expect(newState[link1].request.href).toEqual(link1); expect(newState[id1].request.uuid).toEqual(id1);
expect(newState[link1].requestPending).toEqual(state[link1].requestPending); expect(newState[id1].request.href).toEqual(link1);
expect(newState[link1].responsePending).toEqual(false); expect(newState[id1].requestPending).toEqual(state[id1].requestPending);
expect(newState[link1].completed).toEqual(true); expect(newState[id1].responsePending).toEqual(false);
expect(newState[id1].completed).toEqual(true);
}); });
}); });

View File

@@ -12,7 +12,7 @@ export class RequestEntry {
} }
export interface RequestState { export interface RequestState {
[key: string]: RequestEntry [uuid: string]: RequestEntry
} }
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`) // Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
@@ -41,7 +41,7 @@ export function requestReducer(state = initialState, action: RequestAction): Req
function configureRequest(state: RequestState, action: RequestConfigureAction): RequestState { function configureRequest(state: RequestState, action: RequestConfigureAction): RequestState {
return Object.assign({}, state, { return Object.assign({}, state, {
[action.payload.href]: { [action.payload.uuid]: {
request: action.payload, request: action.payload,
requestPending: true, requestPending: true,
responsePending: false, responsePending: false,

View File

@@ -0,0 +1,446 @@
import { Store } from '@ngrx/store';
import { cold, hot } from 'jasmine-marbles';
import { Observable } from 'rxjs/Observable';
import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service';
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
import { getMockStore } from '../../shared/mocks/mock-store';
import { defaultUUID, getMockUUIDService } from '../../shared/mocks/mock-uuid.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { CoreState } from '../core.reducers';
import { UUIDService } from '../shared/uuid.service';
import { RequestConfigureAction, RequestExecuteAction } from './request.actions';
import {
DeleteRequest,
GetRequest,
HeadRequest,
OptionsRequest,
PatchRequest,
PostRequest,
PutRequest, RestRequest
} from './request.models';
import { RequestService } from './request.service';
describe('RequestService', () => {
let service: RequestService;
let serviceAsAny: any;
let objectCache: ObjectCacheService;
let responseCache: ResponseCacheService;
let uuidService: UUIDService;
let store: Store<CoreState>;
const testUUID = '5f2a0d2a-effa-4d54-bd54-5663b960f9eb';
const testHref = 'https://rest.api/endpoint/selfLink';
const testGetRequest = new GetRequest(testUUID, testHref);
const testPostRequest = new PostRequest(testUUID, testHref);
const testPutRequest = new PutRequest(testUUID, testHref);
const testDeleteRequest = new DeleteRequest(testUUID, testHref);
const testOptionsRequest = new OptionsRequest(testUUID, testHref);
const testHeadRequest = new HeadRequest(testUUID, testHref);
const testPatchRequest = new PatchRequest(testUUID, testHref);
beforeEach(() => {
objectCache = getMockObjectCacheService();
(objectCache.hasBySelfLink as any).and.returnValue(false);
responseCache = getMockResponseCacheService();
(responseCache.has as any).and.returnValue(false);
(responseCache.get as any).and.returnValue(Observable.of(undefined));
uuidService = getMockUUIDService();
store = getMockStore<CoreState>();
(store.select as any).and.returnValue(Observable.of(undefined));
service = new RequestService(
objectCache,
responseCache,
uuidService,
store
);
serviceAsAny = service as any;
});
describe('generateRequestId', () => {
it('should generate a new request ID', () => {
const result = service.generateRequestId();
const expected = `client/${defaultUUID}`;
expect(result).toBe(expected);
});
});
describe('isPending', () => {
describe('before the request is configured', () => {
beforeEach(() => {
spyOn(service, 'getByHref').and.returnValue(Observable.of(undefined));
});
it('should return false', () => {
const result = service.isPending(testGetRequest);
const expected = false;
expect(result).toBe(expected);
});
});
describe('when the request has been configured but hasn\'t reached the store yet', () => {
beforeEach(() => {
spyOn(service, 'getByHref').and.returnValue(Observable.of(undefined));
serviceAsAny.requestsOnTheirWayToTheStore = [testHref];
});
it('should return true', () => {
const result = service.isPending(testGetRequest);
const expected = true;
expect(result).toBe(expected);
});
});
describe('when the request has reached the store, before the server responds', () => {
beforeEach(() => {
spyOn(service, 'getByHref').and.returnValue(Observable.of({
completed: false
}))
});
it('should return true', () => {
const result = service.isPending(testGetRequest);
const expected = true;
expect(result).toBe(expected);
});
});
describe('after the server responds', () => {
beforeEach(() => {
spyOn(service, 'getByHref').and.returnValues(Observable.of({
completed: true
}));
});
it('should return false', () => {
const result = service.isPending(testGetRequest);
const expected = false;
expect(result).toBe(expected);
});
});
});
describe('getByUUID', () => {
describe('if the request with the specified UUID exists in the store', () => {
beforeEach(() => {
(store.select as any).and.returnValues(hot('a', {
a: {
completed: true
}
}));
});
it('should return an Observable of the RequestEntry', () => {
const result = service.getByUUID(testUUID);
const expected = cold('b', {
b: {
completed: true
}
});
expect(result).toBeObservable(expected);
});
});
describe('if the request with the specified UUID doesn\'t exist in the store', () => {
beforeEach(() => {
(store.select as any).and.returnValues(hot('a', {
a: undefined
}));
});
it('should return an Observable of undefined', () => {
const result = service.getByUUID(testUUID);
const expected = cold('b', {
b: undefined
});
expect(result).toBeObservable(expected);
});
});
});
describe('getByHref', () => {
describe('when the request with the specified href exists in the store', () => {
beforeEach(() => {
(store.select as any).and.returnValues(hot('a', {
a: testUUID
}));
spyOn(service, 'getByUUID').and.returnValue(cold('b', {
b: {
completed: true
}
}));
});
it('should return an Observable of the RequestEntry', () => {
const result = service.getByHref(testHref);
const expected = cold('c', {
c: {
completed: true
}
});
expect(result).toBeObservable(expected);
});
});
describe('when the request with the specified href doesn\'t exist in the store', () => {
beforeEach(() => {
(store.select as any).and.returnValues(hot('a', {
a: undefined
}));
spyOn(service, 'getByUUID').and.returnValue(cold('b', {
b: undefined
}));
});
it('should return an Observable of undefined', () => {
const result = service.getByHref(testHref);
const expected = cold('c', {
c: undefined
});
expect(result).toBeObservable(expected);
});
});
});
describe('configure', () => {
beforeEach(() => {
spyOn(serviceAsAny, 'dispatchRequest');
});
describe('when the request is a GET request', () => {
let request: RestRequest;
beforeEach(() => {
request = testGetRequest;
});
describe('and it isn\'t cached or pending', () => {
beforeEach(() => {
spyOn(serviceAsAny, 'isCachedOrPending').and.returnValue(false);
});
it('should dispatch the request', () => {
service.configure(request);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(request);
});
});
describe('and it is already cached or pending', () => {
beforeEach(() => {
spyOn(serviceAsAny, 'isCachedOrPending').and.returnValue(true);
});
it('shouldn\'t dispatch the request', () => {
service.configure(request);
expect(serviceAsAny.dispatchRequest).not.toHaveBeenCalled();
});
});
});
describe('when the request isn\'t a GET request', () => {
it('should dispatch the request', () => {
service.configure(testPostRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPostRequest);
service.configure(testPutRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPutRequest);
service.configure(testDeleteRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testDeleteRequest);
service.configure(testOptionsRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testOptionsRequest);
service.configure(testHeadRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testHeadRequest);
service.configure(testPatchRequest);
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPatchRequest);
});
});
});
describe('isCachedOrPending', () => {
describe('when the request is cached', () => {
describe('in the ObjectCache', () => {
beforeEach(() => {
(objectCache.hasBySelfLink as any).and.returnValues(true);
});
it('should return true', () => {
const result = serviceAsAny.isCachedOrPending(testGetRequest);
const expected = true;
expect(result).toEqual(expected);
});
});
describe('in the responseCache', () => {
beforeEach(() => {
(responseCache.has as any).and.returnValues(true);
});
describe('and it\'s a DSOSuccessResponse', () => {
beforeEach(() => {
(responseCache.get as any).and.returnValues(Observable.of({
response: {
isSuccessful: true,
resourceSelfLinks: [
'https://rest.api/endpoint/selfLink1',
'https://rest.api/endpoint/selfLink2'
]
}
}
));
});
it('should return true if all top level links in the response are cached in the object cache', () => {
(objectCache.hasBySelfLink as any).and.returnValues(false, true, true);
const result = serviceAsAny.isCachedOrPending(testGetRequest);
const expected = true;
expect(result).toEqual(expected);
});
it('should return false if not all top level links in the response are cached in the object cache', () => {
(objectCache.hasBySelfLink as any).and.returnValues(false, true, false);
const result = serviceAsAny.isCachedOrPending(testGetRequest);
const expected = false;
expect(result).toEqual(expected);
});
});
describe('and it isn\'t a DSOSuccessResponse', () => {
beforeEach(() => {
(objectCache.hasBySelfLink as any).and.returnValues(false);
(responseCache.has as any).and.returnValues(true);
(responseCache.get as any).and.returnValues(Observable.of({
response: {
isSuccessful: true
}
}
));
});
it('should return true', () => {
const result = serviceAsAny.isCachedOrPending(testGetRequest);
const expected = true;
expect(result).toEqual(expected);
});
});
});
});
describe('when the request is pending', () => {
beforeEach(() => {
spyOn(service, 'isPending').and.returnValue(true);
});
it('should return true', () => {
const result = serviceAsAny.isCachedOrPending(testGetRequest);
const expected = true;
expect(result).toEqual(expected);
});
});
describe('when the request is neither cached nor pending', () => {
it('should return false', () => {
const result = serviceAsAny.isCachedOrPending(testGetRequest);
const expected = false;
expect(result).toEqual(expected);
});
});
});
describe('dispatchRequest', () => {
it('should dispatch a RequestConfigureAction', () => {
const request = testGetRequest;
serviceAsAny.dispatchRequest(request);
expect(store.dispatch).toHaveBeenCalledWith(new RequestConfigureAction(request));
});
it('should dispatch a RequestExecuteAction', () => {
const request = testGetRequest;
serviceAsAny.dispatchRequest(request);
expect(store.dispatch).toHaveBeenCalledWith(new RequestExecuteAction(request.uuid));
});
describe('when it\'s a GET request', () => {
let request: RestRequest;
beforeEach(() => {
request = testGetRequest;
});
it('should track it on it\'s way to the store', () => {
spyOn(serviceAsAny, 'trackRequestsOnTheirWayToTheStore');
serviceAsAny.dispatchRequest(request);
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).toHaveBeenCalledWith(request);
});
});
describe('when it\'s not a GET request', () => {
it('shouldn\'t track it', () => {
spyOn(serviceAsAny, 'trackRequestsOnTheirWayToTheStore');
serviceAsAny.dispatchRequest(testPostRequest);
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
serviceAsAny.dispatchRequest(testPutRequest);
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
serviceAsAny.dispatchRequest(testDeleteRequest);
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
serviceAsAny.dispatchRequest(testOptionsRequest);
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
serviceAsAny.dispatchRequest(testHeadRequest);
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
serviceAsAny.dispatchRequest(testPatchRequest);
expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled();
});
});
});
describe('trackRequestsOnTheirWayToTheStore', () => {
let request: GetRequest;
beforeEach(() => {
request = testGetRequest;
});
describe('when the method is called with a new request', () => {
it('should start tracking the request', () => {
expect(serviceAsAny.requestsOnTheirWayToTheStore.includes(request.href)).toBeFalsy();
serviceAsAny.trackRequestsOnTheirWayToTheStore(request);
expect(serviceAsAny.requestsOnTheirWayToTheStore.includes(request.href)).toBeTruthy();
});
});
describe('when the request is added to the store', () => {
it('should stop tracking the request', () => {
(store.select as any).and.returnValues(Observable.of({ request }));
serviceAsAny.trackRequestsOnTheirWayToTheStore(request);
expect(serviceAsAny.requestsOnTheirWayToTheStore.includes(request.href)).toBeFalsy();
});
});
});
});

View File

@@ -10,42 +10,45 @@ 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 { coreSelector, CoreState } from '../core.reducers'; import { coreSelector, CoreState } from '../core.reducers';
import { keySelector } from '../shared/selectors'; import { IndexName } from '../index/index.reducer';
import { pathSelector } from '../shared/selectors';
import { UUIDService } from '../shared/uuid.service';
import { RequestConfigureAction, RequestExecuteAction } from './request.actions'; import { RequestConfigureAction, RequestExecuteAction } from './request.actions';
import { RestRequest } from './request.models'; import { GetRequest, RestRequest, RestRequestMethod } from './request.models';
import { RequestEntry, RequestState } from './request.reducer'; import { RequestEntry, RequestState } from './request.reducer';
function entryFromHrefSelector(href: string): MemoizedSelector<CoreState, RequestEntry> {
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[] = []; private requestsOnTheirWayToTheStore: string[] = [];
constructor( constructor(private objectCache: ObjectCacheService,
private objectCache: ObjectCacheService, private responseCache: ResponseCacheService,
private responseCache: ResponseCacheService, private uuidService: UUIDService,
private store: Store<CoreState> private store: Store<CoreState>) {
) {
} }
isPending(href: string): boolean { private entryFromUUIDSelector(uuid: string): MemoizedSelector<CoreState, RequestEntry> {
return pathSelector<CoreState, RequestEntry>(coreSelector, 'data/request', uuid);
}
private uuidFromHrefSelector(href: string): MemoizedSelector<CoreState, string> {
return pathSelector<CoreState, string>(coreSelector, 'index', IndexName.REQUEST, href);
}
generateRequestId(): string {
return `client/${this.uuidService.generate()}`;
}
isPending(request: GetRequest): boolean {
// first check requests that haven't made it to the store yet // first check requests that haven't made it to the store yet
if (this.requestsOnTheirWayToTheStore.includes(href)) { if (this.requestsOnTheirWayToTheStore.includes(request.href)) {
return true; return true;
} }
// then check the store // then check the store
let isPending = false; let isPending = false;
this.store.select(entryFromHrefSelector(href)) this.getByHref(request.href)
.take(1) .take(1)
.subscribe((re: RequestEntry) => { .subscribe((re: RequestEntry) => {
isPending = (hasValue(re) && !re.completed) isPending = (hasValue(re) && !re.completed)
@@ -54,11 +57,22 @@ export class RequestService {
return isPending; return isPending;
} }
get(href: string): Observable<RequestEntry> { getByUUID(uuid: string): Observable<RequestEntry> {
return this.store.select(entryFromHrefSelector(href)); return this.store.select(this.entryFromUUIDSelector(uuid));
}
getByHref(href: string): Observable<RequestEntry> {
return this.store.select(this.uuidFromHrefSelector(href))
.flatMap((uuid: string) => this.getByUUID(uuid));
} }
configure<T extends CacheableObject>(request: RestRequest): void { configure<T extends CacheableObject>(request: RestRequest): void {
if (request.method !== RestRequestMethod.Get || !this.isCachedOrPending(request)) {
this.dispatchRequest(request);
}
}
private isCachedOrPending(request: GetRequest) {
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 [successResponse, errorResponse] = this.responseCache.get(request.href) const [successResponse, errorResponse] = this.responseCache.get(request.href)
@@ -82,29 +96,33 @@ export class RequestService {
).subscribe((c) => isCached = c); ).subscribe((c) => isCached = c);
} }
const isPending = this.isPending(request.href); const isPending = this.isPending(request);
if (!(isCached || isPending)) { return isCached || isPending;
this.store.dispatch(new RequestConfigureAction(request)); }
this.store.dispatch(new RequestExecuteAction(request.href));
this.trackRequestsOnTheirWayToTheStore(request.href); private dispatchRequest(request: RestRequest) {
this.store.dispatch(new RequestConfigureAction(request));
this.store.dispatch(new RequestExecuteAction(request.uuid));
if (request.method === RestRequestMethod.Get) {
this.trackRequestsOnTheirWayToTheStore(request);
} }
} }
/** /**
* ngrx action dispatches are asynchronous. But this.isPending needs to return true as soon as the * 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. * configure method for a GET 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 * This method will store the href of every GET request that gets configured in a local variable, and
* remove it as soon as it can be found in the store. * remove it as soon as it can be found in the store.
*/ */
private trackRequestsOnTheirWayToTheStore(href: string) { private trackRequestsOnTheirWayToTheStore(request: GetRequest) {
this.requestsOnTheirWayToTheStore = [...this.requestsOnTheirWayToTheStore, href]; this.requestsOnTheirWayToTheStore = [...this.requestsOnTheirWayToTheStore, request.href];
this.store.select(entryFromHrefSelector(href)) this.store.select(this.entryFromUUIDSelector(request.href))
.filter((re: RequestEntry) => hasValue(re)) .filter((re: RequestEntry) => hasValue(re))
.take(1) .take(1)
.subscribe((re: RequestEntry) => { .subscribe((re: RequestEntry) => {
this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((pendingHref: string) => pendingHref !== href) this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((pendingHref: string) => pendingHref !== request.href)
}); });
} }
} }

View File

@@ -1,6 +1,6 @@
export interface DSpaceRESTV2Response { export interface DSpaceRESTV2Response {
payload: { payload: {
[name: string]: string; [name: string]: any;
_embedded?: any; _embedded?: any;
_links?: any; _links?: any;
page?: any; page?: any;

View File

@@ -1,6 +1,8 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpResponse } from '@angular/common/http'; import { Request } from '@angular/http';
import { HttpClient, HttpResponse } from '@angular/common/http'
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { RestRequestMethod } from '../data/request.models';
import { DSpaceRESTV2Response } from './dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from './dspace-rest-v2-response.model';
@@ -19,10 +21,8 @@ export class DSpaceRESTv2Service {
* *
* @param absoluteURL * @param absoluteURL
* A URL * A URL
* @param options * @return {Observable<string>}
* An object, with options for the http call. * An Observable<string> containing the response from the server
* @return {Observable<DSpaceRESTV2Response>}
* An Observable<DSpaceRESTV2Response> containing the response from the server
*/ */
get(absoluteURL: string): Observable<DSpaceRESTV2Response> { get(absoluteURL: string): Observable<DSpaceRESTV2Response> {
return this.http.get(absoluteURL, { observe: 'response' }) return this.http.get(absoluteURL, { observe: 'response' })
@@ -33,4 +33,25 @@ export class DSpaceRESTv2Service {
}); });
} }
/**
* Performs a request to the REST API.
*
* @param method
* the HTTP method for the request
* @param url
* the URL for the request
* @param body
* an optional body for the request
* @return {Observable<string>}
* An Observable<string> containing the response from the server
*/
request(method: RestRequestMethod, url: string, body?: any): Observable<DSpaceRESTV2Response> {
return this.http.request(method, url, { body, observe: 'response' })
.map((res) => ({ payload: res.body, statusCode: res.statusText }))
.catch((err) => {
console.log('Error: ', err);
return Observable.throw(err);
});
}
} }

View File

@@ -0,0 +1,69 @@
import { Action } from '@ngrx/store';
import { type } from '../../shared/ngrx/type';
import { IndexName } from './index.reducer';
/**
* The list of HrefIndexAction type definitions
*/
export const IndexActionTypes = {
ADD: type('dspace/core/index/ADD'),
REMOVE_BY_VALUE: type('dspace/core/index/REMOVE_BY_VALUE')
};
/* tslint:disable:max-classes-per-file */
/**
* An ngrx action to add an value to the index
*/
export class AddToIndexAction implements Action {
type = IndexActionTypes.ADD;
payload: {
name: IndexName;
value: string;
key: string;
};
/**
* Create a new AddToIndexAction
*
* @param name
* the name of the index to add to
* @param key
* the key to add
* @param value
* the self link of the resource the key belongs to
*/
constructor(name: IndexName, key: string, value: string) {
this.payload = { name, key, value };
}
}
/**
* An ngrx action to remove an value from the index
*/
export class RemoveFromIndexByValueAction implements Action {
type = IndexActionTypes.REMOVE_BY_VALUE;
payload: {
name: IndexName,
value: string
};
/**
* Create a new RemoveFromIndexByValueAction
*
* @param name
* the name of the index to remove from
* @param value
* the value to remove the UUID for
*/
constructor(name: IndexName, value: string) {
this.payload = { name, value };
}
}
/* tslint:enable:max-classes-per-file */
/**
* A type to encompass all HrefIndexActions
*/
export type IndexAction = AddToIndexAction | RemoveFromIndexByValueAction;

View File

@@ -0,0 +1,61 @@
import { Injectable } from '@angular/core';
import { Effect, Actions } from '@ngrx/effects';
import {
ObjectCacheActionTypes, AddToObjectCacheAction,
RemoveFromObjectCacheAction
} from '../cache/object-cache.actions';
import { RequestActionTypes, RequestConfigureAction } from '../data/request.actions';
import { RestRequestMethod } from '../data/request.models';
import { AddToIndexAction, RemoveFromIndexByValueAction } from './index.actions';
import { hasValue } from '../../shared/empty.util';
import { IndexName } from './index.reducer';
@Injectable()
export class UUIDIndexEffects {
@Effect() addObject$ = this.actions$
.ofType(ObjectCacheActionTypes.ADD)
.filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.uuid))
.map((action: AddToObjectCacheAction) => {
return new AddToIndexAction(
IndexName.OBJECT,
action.payload.objectToCache.uuid,
action.payload.objectToCache.self
);
});
@Effect() removeObject$ = this.actions$
.ofType(ObjectCacheActionTypes.REMOVE)
.map((action: RemoveFromObjectCacheAction) => {
return new RemoveFromIndexByValueAction(
IndexName.OBJECT,
action.payload
);
});
@Effect() addRequest$ = this.actions$
.ofType(RequestActionTypes.CONFIGURE)
.filter((action: RequestConfigureAction) => action.payload.method === RestRequestMethod.Get)
.map((action: RequestConfigureAction) => {
return new AddToIndexAction(
IndexName.REQUEST,
action.payload.href,
action.payload.uuid
);
});
// @Effect() removeRequest$ = this.actions$
// .ofType(ObjectCacheActionTypes.REMOVE)
// .map((action: RemoveFromObjectCacheAction) => {
// return new RemoveFromIndexByValueAction(
// IndexName.OBJECT,
// action.payload
// );
// });
constructor(private actions$: Actions) {
}
}

View File

@@ -0,0 +1,58 @@
import * as deepFreeze from 'deep-freeze';
import { IndexName, indexReducer, IndexState } from './index.reducer';
import { AddToIndexAction, RemoveFromIndexByValueAction } from './index.actions';
class NullAction extends AddToIndexAction {
type = null;
payload = null;
constructor() {
super(null, null, null);
}
}
describe('requestReducer', () => {
const key1 = '567a639f-f5ff-4126-807c-b7d0910808c8';
const key2 = '1911e8a4-6939-490c-b58b-a5d70f8d91fb';
const val1 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/567a639f-f5ff-4126-807c-b7d0910808c8';
const val2 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/1911e8a4-6939-490c-b58b-a5d70f8d91fb';
const testState: IndexState = {
[IndexName.OBJECT]: {
[key1]: val1
}
};
deepFreeze(testState);
it('should return the current state when no valid actions have been made', () => {
const action = new NullAction();
const newState = indexReducer(testState, action);
expect(newState).toEqual(testState);
});
it('should start with an empty state', () => {
const action = new NullAction();
const initialState = indexReducer(undefined, action);
expect(initialState).toEqual(Object.create(null));
});
it('should add the \'key\' with the corresponding \'value\' to the correct substate, in response to an ADD action', () => {
const state = testState;
const action = new AddToIndexAction(IndexName.REQUEST, key2, val2);
const newState = indexReducer(state, action);
expect(newState[IndexName.REQUEST][key2]).toEqual(val2);
});
it('should remove the given \'value\' from its corresponding \'key\' in the correct substate, in response to a REMOVE_BY_VALUE action', () => {
const state = testState;
const action = new RemoveFromIndexByValueAction(IndexName.OBJECT, val1);
const newState = indexReducer(state, action);
expect(newState[IndexName.OBJECT][key1]).toBeUndefined();
});
});

View File

@@ -0,0 +1,62 @@
import {
IndexAction,
IndexActionTypes,
AddToIndexAction,
RemoveFromIndexByValueAction
} from './index.actions';
export enum IndexName {
OBJECT = 'object/uuid-to-self-link',
REQUEST = 'get-request/href-to-uuid'
}
export interface IndexState {
// TODO this should be `[name in IndexName]: {` but that's currently broken,
// see https://github.com/Microsoft/TypeScript/issues/13042
[name: string]: {
[key: string]: string
}
}
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
const initialState: IndexState = Object.create(null);
export function indexReducer(state = initialState, action: IndexAction): IndexState {
switch (action.type) {
case IndexActionTypes.ADD: {
return addToIndex(state, action as AddToIndexAction);
}
case IndexActionTypes.REMOVE_BY_VALUE: {
return removeFromIndexByValue(state, action as RemoveFromIndexByValueAction)
}
default: {
return state;
}
}
}
function addToIndex(state: IndexState, action: AddToIndexAction): IndexState {
const subState = state[action.payload.name];
const newSubState = Object.assign({}, subState, {
[action.payload.key]: action.payload.value
});
return Object.assign({}, state, {
[action.payload.name]: newSubState
})
}
function removeFromIndexByValue(state: IndexState, action: RemoveFromIndexByValueAction): IndexState {
const subState = state[action.payload.name];
const newSubState = Object.create(null);
for (const value in subState) {
if (subState[value] !== action.payload.value) {
newSubState[value] = subState[value];
}
}
return Object.assign({}, state, {
[action.payload.name]: newSubState
});
}

View File

@@ -1,60 +0,0 @@
import { Action } from '@ngrx/store';
import { type } from '../../shared/ngrx/type';
/**
* The list of HrefIndexAction type definitions
*/
export const UUIDIndexActionTypes = {
ADD: type('dspace/core/index/uuid/ADD'),
REMOVE_HREF: type('dspace/core/index/uuid/REMOVE_HREF')
};
/* tslint:disable:max-classes-per-file */
/**
* An ngrx action to add an href to the index
*/
export class AddToUUIDIndexAction implements Action {
type = UUIDIndexActionTypes.ADD;
payload: {
href: string;
uuid: string;
};
/**
* Create a new AddToUUIDIndexAction
*
* @param uuid
* the uuid to add
* @param href
* the self link of the resource the uuid belongs to
*/
constructor(uuid: string, href: string) {
this.payload = { href, uuid };
}
}
/**
* An ngrx action to remove an href from the index
*/
export class RemoveHrefFromUUIDIndexAction implements Action {
type = UUIDIndexActionTypes.REMOVE_HREF;
payload: string;
/**
* Create a new RemoveHrefFromUUIDIndexAction
*
* @param href
* the href to remove the UUID for
*/
constructor(href: string) {
this.payload = href;
}
}
/* tslint:enable:max-classes-per-file */
/**
* A type to encompass all HrefIndexActions
*/
export type UUIDIndexAction = AddToUUIDIndexAction | RemoveHrefFromUUIDIndexAction;

View File

@@ -1,34 +0,0 @@
import { Injectable } from '@angular/core';
import { Effect, Actions } from '@ngrx/effects';
import {
ObjectCacheActionTypes, AddToObjectCacheAction,
RemoveFromObjectCacheAction
} from '../cache/object-cache.actions';
import { AddToUUIDIndexAction, RemoveHrefFromUUIDIndexAction } from './uuid-index.actions';
import { hasValue } from '../../shared/empty.util';
@Injectable()
export class UUIDIndexEffects {
@Effect() add$ = this.actions$
.ofType(ObjectCacheActionTypes.ADD)
.filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.uuid))
.map((action: AddToObjectCacheAction) => {
return new AddToUUIDIndexAction(
action.payload.objectToCache.uuid,
action.payload.objectToCache.self
);
});
@Effect() remove$ = this.actions$
.ofType(ObjectCacheActionTypes.REMOVE)
.map((action: RemoveFromObjectCacheAction) => {
return new RemoveHrefFromUUIDIndexAction(action.payload);
});
constructor(private actions$: Actions) {
}
}

View File

@@ -1,56 +0,0 @@
import * as deepFreeze from 'deep-freeze';
import { uuidIndexReducer, UUIDIndexState } from './uuid-index.reducer';
import { AddToUUIDIndexAction, RemoveHrefFromUUIDIndexAction } from './uuid-index.actions';
class NullAction extends AddToUUIDIndexAction {
type = null;
payload = null;
constructor() {
super(null, null);
}
}
describe('requestReducer', () => {
const link1 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/567a639f-f5ff-4126-807c-b7d0910808c8';
const link2 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/1911e8a4-6939-490c-b58b-a5d70f8d91fb';
const uuid1 = '567a639f-f5ff-4126-807c-b7d0910808c8';
const uuid2 = '1911e8a4-6939-490c-b58b-a5d70f8d91fb';
const testState: UUIDIndexState = {
[uuid1]: link1
};
deepFreeze(testState);
it('should return the current state when no valid actions have been made', () => {
const action = new NullAction();
const newState = uuidIndexReducer(testState, action);
expect(newState).toEqual(testState);
});
it('should start with an empty state', () => {
const action = new NullAction();
const initialState = uuidIndexReducer(undefined, action);
expect(initialState).toEqual(Object.create(null));
});
it('should add the \'uuid\' with the corresponding \'href\' to the state, in response to an ADD action', () => {
const state = testState;
const action = new AddToUUIDIndexAction(uuid2, link2);
const newState = uuidIndexReducer(state, action);
expect(newState[uuid2]).toEqual(link2);
});
it('should remove the given \'href\' from its corresponding \'uuid\' in the state, in response to a REMOVE_HREF action', () => {
const state = testState;
const action = new RemoveHrefFromUUIDIndexAction(link1);
const newState = uuidIndexReducer(state, action);
expect(newState[uuid1]).toBeUndefined();
});
});

View File

@@ -1,46 +0,0 @@
import {
UUIDIndexAction,
UUIDIndexActionTypes,
AddToUUIDIndexAction,
RemoveHrefFromUUIDIndexAction
} from './uuid-index.actions';
export interface UUIDIndexState {
[uuid: string]: string
}
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
const initialState: UUIDIndexState = Object.create(null);
export function uuidIndexReducer(state = initialState, action: UUIDIndexAction): UUIDIndexState {
switch (action.type) {
case UUIDIndexActionTypes.ADD: {
return addToUUIDIndex(state, action as AddToUUIDIndexAction);
}
case UUIDIndexActionTypes.REMOVE_HREF: {
return removeHrefFromUUIDIndex(state, action as RemoveHrefFromUUIDIndexAction)
}
default: {
return state;
}
}
}
function addToUUIDIndex(state: UUIDIndexState, action: AddToUUIDIndexAction): UUIDIndexState {
return Object.assign({}, state, {
[action.payload.uuid]: action.payload.href
});
}
function removeHrefFromUUIDIndex(state: UUIDIndexState, action: RemoveHrefFromUUIDIndexAction): UUIDIndexState {
const newState = Object.create(null);
for (const uuid in state) {
if (state[uuid] !== action.payload) {
newState[uuid] = state[uuid];
}
}
return newState;
}

View File

@@ -11,6 +11,8 @@ import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { Store, StoreModule } from '@ngrx/store'; import { Store, StoreModule } from '@ngrx/store';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { RemoteDataError } from '../data/remote-data-error';
import { UUIDService } from '../shared/uuid.service';
import { MetadataService } from './metadata.service'; import { MetadataService } from './metadata.service';
@@ -64,6 +66,7 @@ describe('MetadataService', () => {
let objectCacheService: ObjectCacheService; let objectCacheService: ObjectCacheService;
let responseCacheService: ResponseCacheService; let responseCacheService: ResponseCacheService;
let requestService: RequestService; let requestService: RequestService;
let uuidService: UUIDService;
let remoteDataBuildService: RemoteDataBuildService; let remoteDataBuildService: RemoteDataBuildService;
let itemDataService: ItemDataService; let itemDataService: ItemDataService;
@@ -82,7 +85,8 @@ describe('MetadataService', () => {
objectCacheService = new ObjectCacheService(store); objectCacheService = new ObjectCacheService(store);
responseCacheService = new ResponseCacheService(store); responseCacheService = new ResponseCacheService(store);
requestService = new RequestService(objectCacheService, responseCacheService, store); uuidService = new UUIDService();
requestService = new RequestService(objectCacheService, responseCacheService, uuidService, store);
remoteDataBuildService = new RemoteDataBuildService(objectCacheService, responseCacheService, requestService); remoteDataBuildService = new RemoteDataBuildService(objectCacheService, responseCacheService, requestService);
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -178,13 +182,10 @@ describe('MetadataService', () => {
const mockRemoteData = (mockItem: Item): Observable<RemoteData<Item>> => { const mockRemoteData = (mockItem: Item): Observable<RemoteData<Item>> => {
return Observable.of(new RemoteData<Item>( return Observable.of(new RemoteData<Item>(
'',
false, false,
false, false,
true, true,
'', undefined,
'200',
{} as PageInfo,
MockItem MockItem
)); ));
} }

View File

@@ -1,5 +1,6 @@
import { cold, hot } from 'jasmine-marbles'; import { cold, hot } from 'jasmine-marbles';
import { GlobalConfig } from '../../../config/global-config.interface'; import { GlobalConfig } from '../../../config/global-config.interface';
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';
import { RootEndpointRequest } from '../data/request.models'; import { RootEndpointRequest } from '../data/request.models';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
@@ -38,7 +39,7 @@ describe('HALEndpointService', () => {
}) })
}); });
requestService = jasmine.createSpyObj('requestService', ['configure']); requestService = getMockRequestService();
envConfig = { envConfig = {
rest: { baseUrl: 'https://rest.api/' } rest: { baseUrl: 'https://rest.api/' }
@@ -53,7 +54,7 @@ describe('HALEndpointService', () => {
it('should configure a new RootEndpointRequest', () => { it('should configure a new RootEndpointRequest', () => {
(service as any).getEndpointMap(); (service as any).getEndpointMap();
const expected = new RootEndpointRequest(envConfig); const expected = new RootEndpointRequest(requestService.generateRequestId(), envConfig);
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(requestService.configure).toHaveBeenCalledWith(expected);
}); });

View File

@@ -14,7 +14,7 @@ export abstract class HALEndpointService {
protected abstract EnvConfig: GlobalConfig; protected abstract EnvConfig: GlobalConfig;
protected getEndpointMap(): Observable<EndpointMap> { protected getEndpointMap(): Observable<EndpointMap> {
const request = new RootEndpointRequest(this.EnvConfig); const request = new RootEndpointRequest(this.requestService.generateRequestId(), this.EnvConfig);
this.requestService.configure(request); this.requestService.configure(request);
return this.responseCache.get(request.href) return this.responseCache.get(request.href)
.map((entry: ResponseCacheEntry) => entry.response) .map((entry: ResponseCacheEntry) => entry.response)

View File

@@ -104,13 +104,10 @@ describe('Item', () => {
function createRemoteDataObject(object: any) { function createRemoteDataObject(object: any) {
return Observable.of(new RemoteData( return Observable.of(new RemoteData(
'',
false, false,
false, false,
true, true,
undefined, undefined,
'200',
new PageInfo(),
object object
)); ));

View File

@@ -1,13 +1,17 @@
import { createSelector, MemoizedSelector } from '@ngrx/store'; import { createSelector, MemoizedSelector } from '@ngrx/store';
import { coreSelector, CoreState } from '../core.reducers'; import { hasNoValue, isEmpty } from '../../shared/empty.util';
import { hasValue } from '../../shared/empty.util';
export function keySelector<T>(subState: string, key: string): MemoizedSelector<CoreState, T> { export function pathSelector<From, To>(selector: MemoizedSelector<any, From>, ...path: string[]): MemoizedSelector<any, To> {
return createSelector(coreSelector, (state: CoreState) => { return createSelector(selector, (state: any) => getSubState(state, path));
if (hasValue(state[subState])) { }
return state[subState][key];
} else { function getSubState(state: any, path: string[]) {
return undefined; const current = path[0];
} const remainingPath = path.slice(1);
}); const subState = state[current];
if (hasNoValue(subState) || isEmpty(remainingPath)) {
return subState;
} else {
return getSubState(subState, remainingPath);
}
} }

View File

@@ -0,0 +1,9 @@
import { Injectable } from '@angular/core';
import * as uuidv4 from 'uuid/v4';
@Injectable()
export class UUIDService {
generate(): string {
return uuidv4();
}
}

View File

@@ -13,7 +13,7 @@ export const MockItem: Item = Object.assign(new Item(), {
self: 'dspace-angular://aggregated/object/1507836003548', self: 'dspace-angular://aggregated/object/1507836003548',
requestPending: false, requestPending: false,
responsePending: false, responsePending: false,
isSuccessFul: true, isSuccessful: true,
errorMessage: '', errorMessage: '',
statusCode: '202', statusCode: '202',
pageInfo: {}, pageInfo: {},
@@ -25,7 +25,7 @@ export const MockItem: Item = Object.assign(new Item(), {
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/10', self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/10',
requestPending: false, requestPending: false,
responsePending: false, responsePending: false,
isSuccessFul: true, isSuccessful: true,
errorMessage: '', errorMessage: '',
statusCode: '202', statusCode: '202',
pageInfo: {}, pageInfo: {},
@@ -60,7 +60,7 @@ export const MockItem: Item = Object.assign(new Item(), {
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/4', self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/4',
requestPending: false, requestPending: false,
responsePending: false, responsePending: false,
isSuccessFul: true, isSuccessful: true,
errorMessage: '', errorMessage: '',
statusCode: '202', statusCode: '202',
pageInfo: {}, pageInfo: {},
@@ -191,7 +191,7 @@ export const MockItem: Item = Object.assign(new Item(), {
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb', self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb',
requestPending: false, requestPending: false,
responsePending: false, responsePending: false,
isSuccessFul: true, isSuccessful: true,
errorMessage: '', errorMessage: '',
statusCode: '202', statusCode: '202',
pageInfo: {}, pageInfo: {},

View File

@@ -0,0 +1,16 @@
import { ObjectCacheService } from '../../core/cache/object-cache.service';
export function getMockObjectCacheService(): ObjectCacheService {
return jasmine.createSpyObj('objectCacheService', [
'add',
'remove',
'getByUUID',
'getBySelfLink',
'getRequestHrefBySelfLink',
'getRequestHrefByUUID',
'getList',
'hasByUUID',
'hasBySelfLink'
]);
}

View File

@@ -0,0 +1,8 @@
import { RequestService } from '../../core/data/request.service';
export function getMockRequestService(): RequestService {
return jasmine.createSpyObj('requestService', {
configure: () => false,
generateRequestId: () => 'clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78'
});
}

View File

@@ -0,0 +1,10 @@
import { ResponseCacheService } from '../../core/cache/response-cache.service';
export function getMockResponseCacheService(): ResponseCacheService {
return jasmine.createSpyObj('ResponseCacheService', [
'add',
'get',
'has',
]);
}

View File

@@ -1,23 +1,15 @@
import { Action } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
export class MockStore<T> extends BehaviorSubject<T> {
constructor(private _initialState: T) {
super(_initialState);
}
dispatch = (action: Action): void => {
console.info();
}
select = <R>(pathOrMapFn: any): Observable<T> => {
return Observable.of(this.getValue());
}
nextState(_newState: T) {
this.next(_newState);
}
export function getMockStore<T>(): Store<T> {
return jasmine.createSpyObj('store', [
'select',
'dispatch',
'lift',
'next',
'error',
'complete',
'addReducer',
'removeReducer'
]);
} }

View File

@@ -0,0 +1,9 @@
import { UUIDService } from '../../core/shared/uuid.service';
export const defaultUUID = 'c4ce6905-290b-478f-979d-a333bbd7820f';
export function getMockUUIDService(uuid = defaultUUID): UUIDService {
return jasmine.createSpyObj('uuidService', {
generate: uuid,
});
}

View File

@@ -1,7 +1,7 @@
<ds-pagination <ds-pagination
[paginationOptions]="config" [paginationOptions]="config"
[pageInfoState]="pageInfo" [pageInfoState]="objects?.payload"
[collectionSize]="pageInfo?.totalElements" [collectionSize]="objects?.payload?.totalElements"
[sortOptions]="sortConfig" [sortOptions]="sortConfig"
[hideGear]="hideGear" [hideGear]="hideGear"
[hidePagerWhenSinglePage]="hidePagerWhenSinglePage" [hidePagerWhenSinglePage]="hidePagerWhenSinglePage"
@@ -12,7 +12,7 @@
(paginationChange)="onPaginationChange($event)"> (paginationChange)="onPaginationChange($event)">
<div class="row mt-2" *ngIf="objects?.hasSucceeded" @fadeIn> <div class="row mt-2" *ngIf="objects?.hasSucceeded" @fadeIn>
<div class="col-lg-4 col-sm-6 col-xs-12 " <div class="col-lg-4 col-sm-6 col-xs-12 "
*ngFor="let object of objects?.payload"> *ngFor="let object of objects?.payload?.page">
<ds-wrapper-grid-element [object]="object"></ds-wrapper-grid-element> <ds-wrapper-grid-element [object]="object"></ds-wrapper-grid-element>
</div> </div>
</div> </div>

View File

@@ -1,20 +1,20 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, EventEmitter, Component,
EventEmitter,
Input, Input,
Output, Output,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { PaginatedList } from '../../core/data/paginated-list';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { PageInfo } from '../../core/shared/page-info.model';
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
import { SortOptions, SortDirection } from '../../core/cache/models/sort-options.model';
import { fadeIn } from '../animations/fade'; import { fadeIn } from '../animations/fade';
import { ListableObject } from '../object-collection/shared/listable-object.model'; import { ListableObject } from '../object-collection/shared/listable-object.model';
import { hasValue } from '../empty.util';
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.Default, changeDetection: ChangeDetectionStrategy.Default,
@@ -31,13 +31,9 @@ export class ObjectGridComponent {
@Input() sortConfig: SortOptions; @Input() sortConfig: SortOptions;
@Input() hideGear = false; @Input() hideGear = false;
@Input() hidePagerWhenSinglePage = true; @Input() hidePagerWhenSinglePage = true;
private _objects: RemoteData<ListableObject[]>; private _objects: RemoteData<PaginatedList<ListableObject>>;
pageInfo: PageInfo; @Input() set objects(objects: RemoteData<PaginatedList<ListableObject>>) {
@Input() set objects(objects: RemoteData<ListableObject[]>) {
this._objects = objects; this._objects = objects;
if (hasValue(objects)) {
this.pageInfo = objects.pageInfo;
}
} }
get objects() { get objects() {
return this._objects; return this._objects;

View File

@@ -1,7 +1,7 @@
<ds-pagination <ds-pagination
[paginationOptions]="config" [paginationOptions]="config"
[pageInfoState]="pageInfo" [pageInfoState]="objects?.payload"
[collectionSize]="pageInfo?.totalElements" [collectionSize]="objects?.payload?.totalElements"
[sortOptions]="sortConfig" [sortOptions]="sortConfig"
[hideGear]="hideGear" [hideGear]="hideGear"
[hidePagerWhenSinglePage]="hidePagerWhenSinglePage" [hidePagerWhenSinglePage]="hidePagerWhenSinglePage"
@@ -11,7 +11,7 @@
(sortFieldChange)="onSortFieldChange($event)" (sortFieldChange)="onSortFieldChange($event)"
(paginationChange)="onPaginationChange($event)"> (paginationChange)="onPaginationChange($event)">
<ul *ngIf="objects?.hasSucceeded"> <!--class="list-unstyled"--> <ul *ngIf="objects?.hasSucceeded"> <!--class="list-unstyled"-->
<li *ngFor="let object of objects?.payload"> <li *ngFor="let object of objects?.payload?.page">
<ds-wrapper-list-element [object]="object"></ds-wrapper-list-element> <ds-wrapper-list-element [object]="object"></ds-wrapper-list-element>
</li> </li>
</ul> </ul>

View File

@@ -6,17 +6,12 @@ import {
Output, Output,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { PaginatedList } from '../../core/data/paginated-list';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { PageInfo } from '../../core/shared/page-info.model';
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
import { SortOptions, SortDirection } from '../../core/cache/models/sort-options.model';
import { fadeIn } from '../animations/fade'; import { fadeIn } from '../animations/fade';
import { ListableObject } from '../object-collection/shared/listable-object.model'; import { ListableObject } from '../object-collection/shared/listable-object.model';
import { hasValue } from '../empty.util'; import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.Default, changeDetection: ChangeDetectionStrategy.Default,
@@ -32,14 +27,11 @@ export class ObjectListComponent {
@Input() sortConfig: SortOptions; @Input() sortConfig: SortOptions;
@Input() hideGear = false; @Input() hideGear = false;
@Input() hidePagerWhenSinglePage = true; @Input() hidePagerWhenSinglePage = true;
private _objects: RemoteData<ListableObject[]>; private _objects: RemoteData<PaginatedList<ListableObject>>;
pageInfo: PageInfo; @Input() set objects(objects: RemoteData<PaginatedList<ListableObject>>) {
@Input() set objects(objects: RemoteData<ListableObject[]>) {
this._objects = objects; this._objects = objects;
if (hasValue(objects)) {
this.pageInfo = objects.pageInfo;
}
} }
get objects() { get objects() {
return this._objects; return this._objects;
} }
@@ -82,6 +74,7 @@ export class ObjectListComponent {
*/ */
@Output() sortFieldChange: EventEmitter<string> = new EventEmitter<string>(); @Output() sortFieldChange: EventEmitter<string> = new EventEmitter<string>();
data: any = {}; data: any = {};
onPageChange(event) { onPageChange(event) {
this.pageChange.emit(event); this.pageChange.emit(event);
} }
@@ -101,4 +94,5 @@ export class ObjectListComponent {
onPaginationChange(event) { onPaginationChange(event) {
this.paginationChange.emit(event); this.paginationChange.emit(event);
} }
} }

View File

@@ -225,6 +225,12 @@
version "0.5.1" version "0.5.1"
resolved "https://registry.yarnpkg.com/@types/source-map/-/source-map-0.5.1.tgz#7e74db5d06ab373a712356eebfaea2fad0ea2367" resolved "https://registry.yarnpkg.com/@types/source-map/-/source-map-0.5.1.tgz#7e74db5d06ab373a712356eebfaea2fad0ea2367"
"@types/uuid@^3.4.3":
version "3.4.3"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.3.tgz#121ace265f5569ce40f4f6d0ff78a338c732a754"
dependencies:
"@types/node" "*"
"@types/webfontloader@1.6.29": "@types/webfontloader@1.6.29":
version "1.6.29" version "1.6.29"
resolved "https://registry.yarnpkg.com/@types/webfontloader/-/webfontloader-1.6.29.tgz#c6b5f6eb8ca31d0aae6b02b6c1300349dd93ea8e" resolved "https://registry.yarnpkg.com/@types/webfontloader/-/webfontloader-1.6.29.tgz#c6b5f6eb8ca31d0aae6b02b6c1300349dd93ea8e"