mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge branch 'master' into w2p-46063_truncation-implementation
Conflicts: src/app/object-list/item-list-element/item-list-element.component.ts src/app/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.scss src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.ts src/styles/_custom_variables.scss src/styles/_mixins.scss
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,6 +14,7 @@ npm-debug.log
|
|||||||
/coverage/
|
/coverage/
|
||||||
|
|
||||||
.idea
|
.idea
|
||||||
|
*.iml
|
||||||
*.ngfactory.ts
|
*.ngfactory.ts
|
||||||
*.css.shim.ts
|
*.css.shim.ts
|
||||||
*.scss.shim.ts
|
*.scss.shim.ts
|
||||||
|
15
.travis.yml
15
.travis.yml
@@ -1,11 +1,7 @@
|
|||||||
sudo: required
|
sudo: required
|
||||||
dist: trusty
|
dist: trusty
|
||||||
addons:
|
addons:
|
||||||
apt:
|
- chrome: stable
|
||||||
sources:
|
|
||||||
- google-chrome
|
|
||||||
packages:
|
|
||||||
- google-chrome-stable
|
|
||||||
|
|
||||||
language: node_js
|
language: node_js
|
||||||
|
|
||||||
@@ -24,14 +20,7 @@ before_install:
|
|||||||
install:
|
install:
|
||||||
- travis_retry yarn install
|
- travis_retry yarn install
|
||||||
|
|
||||||
before_script:
|
|
||||||
- travis_wait yarn run lint
|
|
||||||
- travis_wait yarn run build
|
|
||||||
- export CHROME_BIN=chromium-browser
|
|
||||||
- export DISPLAY=:99.0
|
|
||||||
- sh -e /etc/init.d/xvfb start
|
|
||||||
- sleep 3
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
|
- yarn run build
|
||||||
- yarn run ci
|
- yarn run ci
|
||||||
- cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
|
- cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
|
||||||
|
@@ -146,11 +146,6 @@ module.exports = function (config) {
|
|||||||
],
|
],
|
||||||
|
|
||||||
customLaunchers: {
|
customLaunchers: {
|
||||||
// Continuous integraation with Chrome - launcher
|
|
||||||
'ChromeTravisCi': {
|
|
||||||
base: 'Chrome',
|
|
||||||
flags: ['--no-sandbox']
|
|
||||||
},
|
|
||||||
// Remote Selenium Server with Chrome - launcher
|
// Remote Selenium Server with Chrome - launcher
|
||||||
'SeleniumChrome': {
|
'SeleniumChrome': {
|
||||||
base: 'WebDriver',
|
base: 'WebDriver',
|
||||||
@@ -173,9 +168,5 @@ module.exports = function (config) {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (process.env.TRAVIS) {
|
|
||||||
configuration.browsers = ['ChromeTravisCi'];
|
|
||||||
}
|
|
||||||
|
|
||||||
config.set(configuration);
|
config.set(configuration);
|
||||||
};
|
};
|
||||||
|
@@ -55,15 +55,16 @@
|
|||||||
"debug:server": "node-nightly --inspect --debug-brk dist/server.js",
|
"debug:server": "node-nightly --inspect --debug-brk dist/server.js",
|
||||||
"debug:build": "node-nightly --inspect --debug-brk node_modules/webpack/bin/webpack.js",
|
"debug:build": "node-nightly --inspect --debug-brk node_modules/webpack/bin/webpack.js",
|
||||||
"debug:build:prod": "node-nightly --inspect --debug-brk node_modules/webpack/bin/webpack.js --env.aot --env.client --env.server -p",
|
"debug:build:prod": "node-nightly --inspect --debug-brk node_modules/webpack/bin/webpack.js --env.aot --env.client --env.server -p",
|
||||||
"ci": "yarn run lint && yarn run build:aot && yarn run test && npm-run-all -p -r server e2e",
|
"ci": "yarn run lint && yarn run build:aot && yarn run test:headless && npm-run-all -p -r server e2e",
|
||||||
"protractor": "node node_modules/protractor/bin/protractor",
|
"protractor": "node node_modules/protractor/bin/protractor",
|
||||||
"pree2e": "yarn run webdriver:update",
|
"pree2e": "yarn run webdriver:update",
|
||||||
"e2e": "yarn run protractor",
|
"e2e": "yarn run protractor",
|
||||||
"test": "karma start --single-run",
|
"test": "karma start --single-run",
|
||||||
|
"test:headless": "karma start --single-run --browsers ChromeHeadless",
|
||||||
"test:watch": "karma start --no-single-run --auto-watch",
|
"test:watch": "karma start --no-single-run --auto-watch",
|
||||||
"webdriver:start": "node node_modules/protractor/bin/webdriver-manager start --seleniumPort 4444",
|
"webdriver:start": "node node_modules/protractor/bin/webdriver-manager start --seleniumPort 4444",
|
||||||
"webdriver:update": "node node_modules/protractor/bin/webdriver-manager update --standalone",
|
"webdriver:update": "node node_modules/protractor/bin/webdriver-manager update --standalone",
|
||||||
"lint": "tslint \"src/**/*.ts\" || true && tslint \"e2e/**/*.ts\" || true",
|
"lint": "tslint \"src/**/*.ts\" && tslint \"e2e/**/*.ts\"",
|
||||||
"docs": "typedoc --options typedoc.json ./src/",
|
"docs": "typedoc --options typedoc.json ./src/",
|
||||||
"coverage": "http-server -c-1 -o -p 9875 ./coverage"
|
"coverage": "http-server -c-1 -o -p 9875 ./coverage"
|
||||||
},
|
},
|
||||||
@@ -106,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"
|
||||||
},
|
},
|
||||||
@@ -125,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",
|
||||||
|
@@ -31,7 +31,10 @@ exports.config = {
|
|||||||
capabilities: {
|
capabilities: {
|
||||||
'browserName': 'chrome',
|
'browserName': 'chrome',
|
||||||
'version': '',
|
'version': '',
|
||||||
'platform': 'ANY'
|
'platform': 'ANY',
|
||||||
|
'chromeOptions': {
|
||||||
|
'args': [ '--headless', '--disable-gpu' ]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
// Browser and Capabilities: Firefox
|
// Browser and Capabilities: Firefox
|
||||||
|
@@ -41,13 +41,13 @@
|
|||||||
<ng-container *ngVar="(itemRDObs | async) as itemRD">
|
<ng-container *ngVar="(itemRDObs | async) as itemRD">
|
||||||
<div *ngIf="itemRD?.hasSucceeded" @fadeIn>
|
<div *ngIf="itemRD?.hasSucceeded" @fadeIn>
|
||||||
<h2>{{'collection.page.browse.recent.head' | translate}}</h2>
|
<h2>{{'collection.page.browse.recent.head' | translate}}</h2>
|
||||||
<ds-object-list
|
<ds-viewable-collection
|
||||||
[config]="paginationConfig"
|
[config]="paginationConfig"
|
||||||
[sortConfig]="sortConfig"
|
[sortConfig]="sortConfig"
|
||||||
[objects]="itemRD"
|
[objects]="itemRD"
|
||||||
[hideGear]="false"
|
[hideGear]="false"
|
||||||
(paginationChange)="onPaginationChange($event)">
|
(paginationChange)="onPaginationChange($event)">
|
||||||
</ds-object-list>
|
</ds-viewable-collection>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.recent-submissions' | translate}}"></ds-error>
|
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.recent-submissions' | translate}}"></ds-error>
|
||||||
<ds-loading *ngIf="!itemRD || itemRD.isLoading" message="{{'loading.recent-submissions' | translate}}"></ds-loading>
|
<ds-loading *ngIf="!itemRD || itemRD.isLoading" message="{{'loading.recent-submissions' | translate}}"></ds-loading>
|
||||||
|
@@ -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;
|
||||||
|
@@ -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>
|
||||||
|
@@ -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) {
|
||||||
|
|
||||||
|
@@ -2,14 +2,12 @@
|
|||||||
<div *ngIf="communitiesRD?.hasSucceeded " @fadeInOut>
|
<div *ngIf="communitiesRD?.hasSucceeded " @fadeInOut>
|
||||||
<h2>{{'home.top-level-communities.head' | translate}}</h2>
|
<h2>{{'home.top-level-communities.head' | translate}}</h2>
|
||||||
<p class="lead">{{'home.top-level-communities.help' | translate}}</p>
|
<p class="lead">{{'home.top-level-communities.help' | translate}}</p>
|
||||||
<ds-object-list
|
<ds-viewable-collection
|
||||||
[config]="config"
|
[config]="config"
|
||||||
[sortConfig]="sortConfig"
|
[sortConfig]="sortConfig"
|
||||||
[objects]="communitiesRD"
|
[objects]="communitiesRD"
|
||||||
[hideGear]="true"
|
|
||||||
(paginationChange)="updatePage($event)">
|
(paginationChange)="updatePage($event)">
|
||||||
</ds-object-list>
|
</ds-viewable-collection>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="communitiesRD?.hasFailed " message="{{'error.top-level-communites' | translate}}"></ds-error>
|
<ds-error *ngIf="communitiesRD?.hasFailed " message="{{'error.top-level-communites' | translate}}"></ds-error>
|
||||||
<ds-loading *ngIf="communitiesRD?.isLoading" message="{{'loading.top-level-communities' | translate}}"></ds-loading>
|
<ds-loading *ngIf="communitiesRD?.isLoading " message="{{'loading.top-level-communities' | translate}}"></ds-loading></ng-container>
|
||||||
</ng-container>
|
|
||||||
|
@@ -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;
|
||||||
|
|
||||||
|
@@ -41,7 +41,7 @@ describe('SearchFacetFilterComponent', () => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
let filterService;
|
let filterService;
|
||||||
let page = Observable.of(0)
|
const page = Observable.of(0)
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, FormsModule],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, FormsModule],
|
||||||
@@ -147,8 +147,8 @@ describe('SearchFacetFilterComponent', () => {
|
|||||||
|
|
||||||
it('should return the correct number of items shown (this equals the page count x page size)', () => {
|
it('should return the correct number of items shown (this equals the page count x page size)', () => {
|
||||||
const sub = count.subscribe((c) => {
|
const sub = count.subscribe((c) => {
|
||||||
const subsub = comp.currentPage.subscribe((page) => {
|
const subsub = comp.currentPage.subscribe((currentPage) => {
|
||||||
expect(c).toBe(page * mockFilterConfig.pageSize);
|
expect(c).toBe(currentPage * mockFilterConfig.pageSize);
|
||||||
});
|
});
|
||||||
subsub.unsubscribe()
|
subsub.unsubscribe()
|
||||||
});
|
});
|
||||||
|
@@ -166,6 +166,4 @@ describe('SearchFilterComponent', () => {
|
|||||||
sub.unsubscribe();
|
sub.unsubscribe();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -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"
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ds-search-results [searchResults]="resultsRDObs | async"
|
<ds-search-results [searchResults]="resultsRDObs | async"
|
||||||
[searchConfig]="searchOptions"></ds-search-results>
|
[searchConfig]="searchOptions" [sortConfig]="sortConfig"></ds-search-results>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -109,10 +109,10 @@ describe('SearchPageComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('when update search results is called', () => {
|
describe('when update search results is called', () => {
|
||||||
let pagination;
|
let paginationUpdate;
|
||||||
let sort;
|
let sortUpdate;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
pagination = Object.assign(
|
paginationUpdate = Object.assign(
|
||||||
{},
|
{},
|
||||||
new PaginationComponentOptions(),
|
new PaginationComponentOptions(),
|
||||||
{
|
{
|
||||||
@@ -120,7 +120,7 @@ describe('SearchPageComponent', () => {
|
|||||||
pageSize: 15
|
pageSize: 15
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
sort = Object.assign({},
|
sortUpdate = Object.assign({},
|
||||||
new SortOptions(),
|
new SortOptions(),
|
||||||
{
|
{
|
||||||
direction: SortDirection.Ascending,
|
direction: SortDirection.Ascending,
|
||||||
|
@@ -1,16 +1,19 @@
|
|||||||
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 } 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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,7 +39,8 @@ export class SearchPageComponent implements OnInit, OnDestroy {
|
|||||||
resultsRDObs: Observable<RemoteData<Array<SearchResult<DSpaceObject>>>>;
|
resultsRDObs: Observable<RemoteData<Array<SearchResult<DSpaceObject>>>>;
|
||||||
currentParams = {};
|
currentParams = {};
|
||||||
searchOptions: SearchOptions;
|
searchOptions: SearchOptions;
|
||||||
scopeListRDObs: Observable<RemoteData<Community[]>>;
|
sortConfig: SortOptions;
|
||||||
|
scopeListRDObs: Observable<RemoteData<PaginatedList<Community>>>;
|
||||||
isMobileView: Observable<boolean>;
|
isMobileView: Observable<boolean>;
|
||||||
|
|
||||||
constructor(private service: SearchService,
|
constructor(private service: SearchService,
|
||||||
@@ -51,6 +55,13 @@ export class SearchPageComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
this.scopeListRDObs = communityService.findAll();
|
this.scopeListRDObs = communityService.findAll();
|
||||||
// Initial pagination config
|
// Initial pagination config
|
||||||
|
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
||||||
|
pagination.id = 'search-results-pagination';
|
||||||
|
pagination.currentPage = 1;
|
||||||
|
pagination.pageSize = 10;
|
||||||
|
|
||||||
|
const sort: SortOptions = new SortOptions();
|
||||||
|
this.sortConfig = sort;
|
||||||
this.searchOptions = this.service.searchOptions;
|
this.searchOptions = this.service.searchOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,16 +74,31 @@ export class SearchPageComponent implements OnInit, OnDestroy {
|
|||||||
this.query = params.query || '';
|
this.query = params.query || '';
|
||||||
this.scope = params.scope;
|
this.scope = params.scope;
|
||||||
const page = +params.page || this.searchOptions.pagination.currentPage;
|
const page = +params.page || this.searchOptions.pagination.currentPage;
|
||||||
const pageSize = +params.pageSize || this.searchOptions.pagination.pageSize;
|
let pageSize = +params.pageSize || this.searchOptions.pagination.pageSize;
|
||||||
|
let pageSizeOptions: number[] = [5, 10, 20, 40, 60, 80, 100];
|
||||||
|
|
||||||
|
if (isNotEmpty(params.view) && params.view === ViewMode.Grid) {
|
||||||
|
pageSizeOptions = [12, 24, 36, 48 , 50, 62, 74, 84];
|
||||||
|
if (pageSizeOptions.indexOf(pageSize) === -1) {
|
||||||
|
pageSize = 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isNotEmpty(params.view) && params.view === ViewMode.List) {
|
||||||
|
if (pageSizeOptions.indexOf(pageSize) === -1) {
|
||||||
|
pageSize = 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sortDirection = +params.sortDirection || this.searchOptions.sort.direction;
|
const sortDirection = +params.sortDirection || this.searchOptions.sort.direction;
|
||||||
const pagination = Object.assign({},
|
const pagination = Object.assign({},
|
||||||
this.searchOptions.pagination,
|
this.searchOptions.pagination,
|
||||||
{ currentPage: page, pageSize: pageSize }
|
{ currentPage: page, pageSize: pageSize, pageSizeOptions: pageSizeOptions}
|
||||||
);
|
);
|
||||||
const sort = Object.assign({},
|
const sort = Object.assign({},
|
||||||
this.searchOptions.sort,
|
this.searchOptions.sort,
|
||||||
{ direction: sortDirection, field: params.sortField }
|
{ direction: sortDirection, field: params.sortField }
|
||||||
);
|
);
|
||||||
|
|
||||||
this.updateSearchResults({
|
this.updateSearchResults({
|
||||||
pagination: pagination,
|
pagination: pagination,
|
||||||
sort: sort
|
sort: sort
|
||||||
@@ -88,6 +114,7 @@ export class SearchPageComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private updateSearchResults(searchOptions) {
|
private updateSearchResults(searchOptions) {
|
||||||
this.resultsRDObs = this.service.search(this.query, this.scope, searchOptions);
|
this.resultsRDObs = this.service.search(this.query, this.scope, searchOptions);
|
||||||
|
this.searchOptions = searchOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
@@ -4,9 +4,12 @@ import { SharedModule } from '../shared/shared.module';
|
|||||||
import { SearchPageRoutingModule } from './search-page-routing.module';
|
import { SearchPageRoutingModule } from './search-page-routing.module';
|
||||||
import { SearchPageComponent } from './search-page.component';
|
import { SearchPageComponent } from './search-page.component';
|
||||||
import { SearchResultsComponent } from './search-results/search-results.component';
|
import { SearchResultsComponent } from './search-results/search-results.component';
|
||||||
import { ItemSearchResultListElementComponent } from '../object-list/search-result-list-element/item-search-result/item-search-result-list-element.component';
|
import { ItemSearchResultListElementComponent } from '../shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component';
|
||||||
import { CollectionSearchResultListElementComponent } from '../object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component';
|
import { CollectionSearchResultListElementComponent } from '../shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component';
|
||||||
import { CommunitySearchResultListElementComponent } from '../object-list/search-result-list-element/community-search-result/community-search-result-list-element.component';
|
import { CommunitySearchResultListElementComponent } from '../shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component';
|
||||||
|
import { ItemSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component';
|
||||||
|
import { CommunitySearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'
|
||||||
|
import { CollectionSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component';
|
||||||
import { SearchService } from './search-service/search.service';
|
import { SearchService } from './search-service/search.service';
|
||||||
import { SearchSidebarComponent } from './search-sidebar/search-sidebar.component';
|
import { SearchSidebarComponent } from './search-sidebar/search-sidebar.component';
|
||||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||||
@@ -37,6 +40,10 @@ const effects = [
|
|||||||
ItemSearchResultListElementComponent,
|
ItemSearchResultListElementComponent,
|
||||||
CollectionSearchResultListElementComponent,
|
CollectionSearchResultListElementComponent,
|
||||||
CommunitySearchResultListElementComponent,
|
CommunitySearchResultListElementComponent,
|
||||||
|
ItemSearchResultGridElementComponent,
|
||||||
|
CollectionSearchResultGridElementComponent,
|
||||||
|
CommunitySearchResultGridElementComponent,
|
||||||
|
CommunitySearchResultListElementComponent,
|
||||||
SearchFiltersComponent,
|
SearchFiltersComponent,
|
||||||
SearchFilterComponent,
|
SearchFilterComponent,
|
||||||
SearchFacetFilterComponent
|
SearchFacetFilterComponent
|
||||||
@@ -49,7 +56,11 @@ const effects = [
|
|||||||
entryComponents: [
|
entryComponents: [
|
||||||
ItemSearchResultListElementComponent,
|
ItemSearchResultListElementComponent,
|
||||||
CollectionSearchResultListElementComponent,
|
CollectionSearchResultListElementComponent,
|
||||||
CommunitySearchResultListElementComponent
|
CommunitySearchResultListElementComponent,
|
||||||
|
ItemSearchResultGridElementComponent,
|
||||||
|
CollectionSearchResultGridElementComponent,
|
||||||
|
CommunitySearchResultGridElementComponent,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class SearchPageModule { }
|
export class SearchPageModule {
|
||||||
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||||
import { Metadatum } from '../core/shared/metadatum.model';
|
import { Metadatum } from '../core/shared/metadatum.model';
|
||||||
import { ListableObject } from '../object-list/listable-object/listable-object.model';
|
import { ListableObject } from '../shared/object-collection/shared/listable-object.model';
|
||||||
|
|
||||||
export class SearchResult<T extends DSpaceObject> implements ListableObject {
|
export class SearchResult<T extends DSpaceObject> implements ListableObject {
|
||||||
|
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
<div *ngIf="searchResults?.hasSucceeded && !searchResults?.isLoading" @fadeIn>
|
<div *ngIf="searchResults?.hasSucceeded && !searchResults?.isLoading" @fadeIn>
|
||||||
<h2 *ngIf="searchResults?.payload ?.length > 0">{{ 'search.results.head' | translate }}</h2>
|
<h2 *ngIf="searchResults?.payload ?.length > 0">{{ 'search.results.head' | translate }}</h2>
|
||||||
<ds-object-list
|
<ds-viewable-collection
|
||||||
[config]="searchConfig.pagination"
|
[config]="searchConfig.pagination"
|
||||||
[sortConfig]="searchConfig.sort"
|
[sortConfig]="searchConfig.sort"
|
||||||
[objects]="searchResults"
|
[objects]="searchResults"
|
||||||
[hideGear]="true">
|
[hideGear]="true">
|
||||||
</ds-object-list></div>
|
</ds-viewable-collection></div>
|
||||||
<ds-loading *ngIf="searchResults?.isLoading" message="{{'loading.search-results' | translate}}"></ds-loading>
|
<ds-loading *ngIf="searchResults?.isLoading" message="{{'loading.search-results' | translate}}"></ds-loading>
|
||||||
<ds-error *ngIf="searchResults?.hasFailed" message="{{'error.search-results' | translate}}"></ds-error>
|
<ds-error *ngIf="searchResults?.hasFailed" message="{{'error.search-results' | translate}}"></ds-error>
|
||||||
|
@@ -2,7 +2,8 @@ import { Component, Input } from '@angular/core';
|
|||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
||||||
import { SearchOptions } from '../search-options.model';
|
import { SearchOptions, ViewMode } from '../search-options.model';
|
||||||
|
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { SearchResult } from '../search-result.model';
|
import { SearchResult } from '../search-result.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,4 +22,6 @@ import { SearchResult } from '../search-result.model';
|
|||||||
export class SearchResultsComponent {
|
export class SearchResultsComponent {
|
||||||
@Input() searchResults: RemoteData<Array<SearchResult<DSpaceObject>>>;
|
@Input() searchResults: RemoteData<Array<SearchResult<DSpaceObject>>>;
|
||||||
@Input() searchConfig: SearchOptions;
|
@Input() searchConfig: SearchOptions;
|
||||||
|
@Input() sortConfig: SortOptions;
|
||||||
|
@Input() viewMode: ViewMode;
|
||||||
}
|
}
|
||||||
|
@@ -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 { ItemSearchResult } from '../../object-list/search-result-list-element/item-search-result/item-search-result.model';
|
|
||||||
import { SearchFilterConfig } from './search-filter-config.model';
|
|
||||||
import { FilterType } from './filter-type.model';
|
|
||||||
import { FacetValue } from './facet-value.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;
|
||||||
@@ -100,7 +101,7 @@ export class SearchService implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
search(query: string, scopeId?: string, searchOptions?: SearchOptions): Observable<RemoteData<Array<SearchResult<DSpaceObject>>>> {
|
search(query: string, scopeId?: string, searchOptions?: SearchOptions): Observable<RemoteData<Array<SearchResult<DSpaceObject>>>> {
|
||||||
this.searchOptions = this.searchOptions;
|
this.searchOptions = searchOptions;
|
||||||
let self = `https://dspace7.4science.it/dspace-spring-rest/api/search?query=${query}`;
|
let self = `https://dspace7.4science.it/dspace-spring-rest/api/search?query=${query}`;
|
||||||
if (hasValue(scopeId)) {
|
if (hasValue(scopeId)) {
|
||||||
self += `&scope=${scopeId}`;
|
self += `&scope=${scopeId}`;
|
||||||
@@ -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
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
<h5>{{ 'search.sidebar.settings.rpp' | translate}}</h5>
|
<h5>{{ 'search.sidebar.settings.rpp' | translate}}</h5>
|
||||||
|
|
||||||
<select class="form-control" (change)="reloadRPP($event)">
|
<select class="form-control" (change)="reloadRPP($event)">
|
||||||
<option *ngFor="let item of searchOptions.pagination.pageSizeOptions" [value]="item"
|
<option *ngFor="let item of pageSizeOptions" [value]="item"
|
||||||
[selected]="item === searchOptions.pagination.pageSize ? 'selected': null">
|
[selected]="item === searchOptions.pagination.pageSize ? 'selected': null">
|
||||||
{{item}}
|
{{item}}
|
||||||
</option>
|
</option>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
import { SearchService } from '../search-service/search.service';
|
import { SearchService } from '../search-service/search.service';
|
||||||
import { SearchOptions } from '../search-options.model';
|
import { SearchOptions, ViewMode } from '../search-options.model';
|
||||||
import { SortDirection } from '../../core/cache/models/sort-options.model';
|
import { SortDirection } from '../../core/cache/models/sort-options.model';
|
||||||
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
|
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
|
||||||
|
|
||||||
@@ -20,6 +20,9 @@ export class SearchSettingsComponent implements OnInit {
|
|||||||
* Number of items per page.
|
* Number of items per page.
|
||||||
*/
|
*/
|
||||||
public pageSize;
|
public pageSize;
|
||||||
|
@Input() public pageSizeOptions;
|
||||||
|
public listPageSizeOptions: number[] = [5, 10, 20, 40, 60, 80, 100];
|
||||||
|
public gridPageSizeOptions: number[] = [12, 24, 36, 48 , 50, 62, 74, 84];
|
||||||
|
|
||||||
private sub;
|
private sub;
|
||||||
private scope: string;
|
private scope: string;
|
||||||
@@ -36,6 +39,7 @@ export class SearchSettingsComponent implements OnInit {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.searchOptions = this.service.searchOptions;
|
this.searchOptions = this.service.searchOptions;
|
||||||
this.pageSize = this.searchOptions.pagination.pageSize;
|
this.pageSize = this.searchOptions.pagination.pageSize;
|
||||||
|
this.pageSizeOptions = this.searchOptions.pagination.pageSizeOptions;
|
||||||
this.sub = this.route
|
this.sub = this.route
|
||||||
.queryParams
|
.queryParams
|
||||||
.subscribe((params) => {
|
.subscribe((params) => {
|
||||||
@@ -45,6 +49,11 @@ export class SearchSettingsComponent implements OnInit {
|
|||||||
this.page = +params.page || this.searchOptions.pagination.currentPage;
|
this.page = +params.page || this.searchOptions.pagination.currentPage;
|
||||||
this.pageSize = +params.pageSize || this.searchOptions.pagination.pageSize;
|
this.pageSize = +params.pageSize || this.searchOptions.pagination.pageSize;
|
||||||
this.direction = +params.sortDirection || this.searchOptions.sort.direction;
|
this.direction = +params.sortDirection || this.searchOptions.sort.direction;
|
||||||
|
if (params.view === ViewMode.Grid) {
|
||||||
|
this.pageSizeOptions = this.gridPageSizeOptions;
|
||||||
|
} else {
|
||||||
|
this.pageSizeOptions = this.listPageSizeOptions;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -15,6 +15,4 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
|
|||||||
export class SearchSidebarComponent {
|
export class SearchSidebarComponent {
|
||||||
@Input() resultCount;
|
@Input() resultCount;
|
||||||
@Output() toggleSidebar = new EventEmitter<boolean>();
|
@Output() toggleSidebar = new EventEmitter<boolean>();
|
||||||
constructor() {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CommonModule, APP_BASE_HREF } from '@angular/common';
|
import { CommonModule, APP_BASE_HREF } from '@angular/common';
|
||||||
import { HttpModule } from '@angular/http';
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
|
||||||
import { EffectsModule } from '@ngrx/effects';
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
import { StoreModule, MetaReducer, META_REDUCERS } from '@ngrx/store';
|
import { StoreModule, MetaReducer, META_REDUCERS } from '@ngrx/store';
|
||||||
@@ -51,7 +51,7 @@ if (!ENV_CONFIG.production) {
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
HttpModule,
|
HttpClientModule,
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
CoreModule.forRoot(),
|
CoreModule.forRoot(),
|
||||||
NgbModule.forRoot(),
|
NgbModule.forRoot(),
|
||||||
|
@@ -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 }));
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
38
src/app/core/cache/object-cache.effects.spec.ts
vendored
Normal file
38
src/app/core/cache/object-cache.effects.spec.ts
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { provideMockActions } from '@ngrx/effects/testing';
|
||||||
|
import { cold, hot } from 'jasmine-marbles';
|
||||||
|
import { ObjectCacheEffects } from './object-cache.effects';
|
||||||
|
import { ResetObjectCacheTimestampsAction } from './object-cache.actions';
|
||||||
|
import { StoreActionTypes } from '../../store.actions';
|
||||||
|
|
||||||
|
describe('ObjectCacheEffects', () => {
|
||||||
|
let cacheEffects: ObjectCacheEffects;
|
||||||
|
let actions: Observable<any>;
|
||||||
|
const timestamp = 10000;
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
ObjectCacheEffects,
|
||||||
|
provideMockActions(() => actions),
|
||||||
|
// other providers
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
cacheEffects = TestBed.get(ObjectCacheEffects);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fixTimestampsOnRehydrate$', () => {
|
||||||
|
|
||||||
|
it('should return a RESET_TIMESTAMPS action in response to a REHYDRATE action', () => {
|
||||||
|
spyOn(Date.prototype, 'getTime').and.callFake(() => {
|
||||||
|
return timestamp;
|
||||||
|
});
|
||||||
|
actions = hot('--a-', { a: { type: StoreActionTypes.REHYDRATE, payload: {} } });
|
||||||
|
|
||||||
|
const expected = cold('--b-', { b: new ResetObjectCacheTimestampsAction(new Date().getTime()) });
|
||||||
|
|
||||||
|
expect(cacheEffects.fixTimestampsOnRehydrate).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { Actions, Effect } from '@ngrx/effects';
|
import { Actions, Effect } from '@ngrx/effects';
|
||||||
|
|
||||||
import { StoreActionTypes } from '../../store.actions';
|
import { StoreActionTypes } from '../../store.actions';
|
||||||
import { ResetObjectCacheTimestampsAction } from '../cache/object-cache.actions';
|
import { ResetObjectCacheTimestampsAction } from './object-cache.actions';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ObjectCacheEffects {
|
export class ObjectCacheEffects {
|
11
src/app/core/cache/object-cache.reducer.ts
vendored
11
src/app/core/cache/object-cache.reducer.ts
vendored
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
11
src/app/core/cache/object-cache.service.ts
vendored
11
src/app/core/cache/object-cache.service.ts
vendored
@@ -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
|
||||||
|
38
src/app/core/cache/response-cache.effects.spec.ts
vendored
Normal file
38
src/app/core/cache/response-cache.effects.spec.ts
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { provideMockActions } from '@ngrx/effects/testing';
|
||||||
|
import { cold, hot } from 'jasmine-marbles';
|
||||||
|
import { StoreActionTypes } from '../../store.actions';
|
||||||
|
import { ResponseCacheEffects } from './response-cache.effects';
|
||||||
|
import { ResetResponseCacheTimestampsAction } from './response-cache.actions';
|
||||||
|
|
||||||
|
describe('ResponseCacheEffects', () => {
|
||||||
|
let cacheEffects: ResponseCacheEffects;
|
||||||
|
let actions: Observable<any>;
|
||||||
|
const timestamp = 10000;
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
ResponseCacheEffects,
|
||||||
|
provideMockActions(() => actions),
|
||||||
|
// other providers
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
cacheEffects = TestBed.get(ResponseCacheEffects);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fixTimestampsOnRehydrate$', () => {
|
||||||
|
|
||||||
|
it('should return a RESET_TIMESTAMPS action in response to a REHYDRATE action', () => {
|
||||||
|
spyOn(Date.prototype, 'getTime').and.callFake(() => {
|
||||||
|
return timestamp;
|
||||||
|
});
|
||||||
|
actions = hot('--a-', { a: { type: StoreActionTypes.REHYDRATE, payload: {} } });
|
||||||
|
|
||||||
|
const expected = cold('--b-', { b: new ResetResponseCacheTimestampsAction(new Date().getTime()) });
|
||||||
|
|
||||||
|
expect(cacheEffects.fixTimestampsOnRehydrate).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -1,11 +1,11 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Actions, Effect } from '@ngrx/effects';
|
import { Actions, Effect } from '@ngrx/effects';
|
||||||
|
|
||||||
import { ResetResponseCacheTimestampsAction } from '../cache/response-cache.actions';
|
import { ResetResponseCacheTimestampsAction } from './response-cache.actions';
|
||||||
import { StoreActionTypes } from '../../store.actions';
|
import { StoreActionTypes } from '../../store.actions';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RequestCacheEffects {
|
export class ResponseCacheEffects {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the store is rehydrated in the browser, set all cache
|
* When the store is rehydrated in the browser, set all cache
|
11
src/app/core/cache/response-cache.models.ts
vendored
11
src/app/core/cache/response-cache.models.ts
vendored
@@ -1,6 +1,7 @@
|
|||||||
import { RequestError } from '../data/request.models';
|
import { RequestError } from '../data/request.models';
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
|
import { ConfigObject } from '../shared/config/config.model';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
export class RestResponse {
|
export class RestResponse {
|
||||||
@@ -51,4 +52,14 @@ export class ErrorResponse extends RestResponse {
|
|||||||
this.errorMessage = error.message;
|
this.errorMessage = error.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ConfigSuccessResponse extends RestResponse {
|
||||||
|
constructor(
|
||||||
|
public configDefinition: ConfigObject[],
|
||||||
|
public statusCode: string,
|
||||||
|
public pageInfo?: PageInfo
|
||||||
|
) {
|
||||||
|
super(true, statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
/* tslint:enable:max-classes-per-file */
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
317
src/app/core/cache/response-cache.reducer.spec.ts
vendored
317
src/app/core/cache/response-cache.reducer.spec.ts
vendored
@@ -4,8 +4,9 @@ import { responseCacheReducer, ResponseCacheState } from './response-cache.reduc
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ResponseCacheRemoveAction,
|
ResponseCacheRemoveAction,
|
||||||
ResetResponseCacheTimestampsAction
|
ResetResponseCacheTimestampsAction, ResponseCacheAddAction
|
||||||
} from './response-cache.actions';
|
} from './response-cache.actions';
|
||||||
|
import { RestResponse } from './response-cache.models';
|
||||||
|
|
||||||
class NullAction extends ResponseCacheRemoveAction {
|
class NullAction extends ResponseCacheRemoveAction {
|
||||||
type = null;
|
type = null;
|
||||||
@@ -16,212 +17,108 @@ class NullAction extends ResponseCacheRemoveAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// describe('responseCacheReducer', () => {
|
describe('responseCacheReducer', () => {
|
||||||
// const keys = ['125c17f89046283c5f0640722aac9feb', 'a06c3006a41caec5d635af099b0c780c'];
|
const keys = ['125c17f89046283c5f0640722aac9feb', 'a06c3006a41caec5d635af099b0c780c'];
|
||||||
// const services = [new OpaqueToken('service1'), new OpaqueToken('service2')];
|
const msToLive = 900000;
|
||||||
// const msToLive = 900000;
|
const uuids = [
|
||||||
// const uuids = [
|
'9e32a2e2-6b91-4236-a361-995ccdc14c60',
|
||||||
// '9e32a2e2-6b91-4236-a361-995ccdc14c60',
|
'598ce822-c357-46f3-ab70-63724d02d6ad',
|
||||||
// '598ce822-c357-46f3-ab70-63724d02d6ad',
|
'be8325f7-243b-49f4-8a4b-df2b793ff3b5'
|
||||||
// 'be8325f7-243b-49f4-8a4b-df2b793ff3b5'
|
];
|
||||||
// ];
|
const testState: ResponseCacheState = {
|
||||||
// const resourceID = '9978';
|
[keys[0]]: {
|
||||||
// const paginationOptions = { 'resultsPerPage': 10, 'currentPage': 1 };
|
key: keys[0],
|
||||||
// const sortOptions = { 'field': 'id', 'direction': 0 };
|
response: new RestResponse(true, '200'),
|
||||||
// const testState = {
|
timeAdded: new Date().getTime(),
|
||||||
// [keys[0]]: {
|
msToLive: msToLive
|
||||||
// 'key': keys[0],
|
},
|
||||||
// 'service': services[0],
|
[keys[1]]: {
|
||||||
// 'resourceUUIDs': [uuids[0], uuids[1]],
|
key: keys[1],
|
||||||
// 'isLoading': false,
|
response: new RestResponse(true, '200'),
|
||||||
// 'paginationOptions': paginationOptions,
|
timeAdded: new Date().getTime(),
|
||||||
// 'sortOptions': sortOptions,
|
msToLive: msToLive
|
||||||
// 'timeAdded': new Date().getTime(),
|
}
|
||||||
// 'msToLive': msToLive
|
};
|
||||||
// },
|
deepFreeze(testState);
|
||||||
// [keys[1]]: {
|
const errorState: {} = {
|
||||||
// 'key': keys[1],
|
[keys[0]]: {
|
||||||
// 'service': services[1],
|
errorMessage: 'error',
|
||||||
// 'resourceID': resourceID,
|
resourceUUIDs: uuids
|
||||||
// 'resourceUUIDs': [uuids[2]],
|
}
|
||||||
// 'isLoading': false,
|
};
|
||||||
// 'timeAdded': new Date().getTime(),
|
deepFreeze(errorState);
|
||||||
// 'msToLive': msToLive
|
|
||||||
// }
|
it('should return the current state when no valid actions have been made', () => {
|
||||||
// };
|
const action = new NullAction();
|
||||||
// deepFreeze(testState);
|
const newState = responseCacheReducer(testState, action);
|
||||||
// const errorState: {} = {
|
|
||||||
// [keys[0]]: {
|
expect(newState).toEqual(testState);
|
||||||
// errorMessage: 'error',
|
});
|
||||||
// resourceUUIDs: uuids
|
|
||||||
// }
|
it('should start with an empty cache', () => {
|
||||||
// };
|
const action = new NullAction();
|
||||||
// deepFreeze(errorState);
|
const initialState = responseCacheReducer(undefined, action);
|
||||||
//
|
|
||||||
//
|
expect(initialState).toEqual(Object.create(null));
|
||||||
// it('should return the current state when no valid actions have been made', () => {
|
});
|
||||||
// const action = new NullAction();
|
|
||||||
// const newState = responseCacheReducer(testState, action);
|
describe('ADD', () => {
|
||||||
//
|
const addTimeAdded = new Date().getTime();
|
||||||
// expect(newState).toEqual(testState);
|
const addMsToLive = 5;
|
||||||
// });
|
const addResponse = new RestResponse(true, '200');
|
||||||
//
|
const action = new ResponseCacheAddAction(keys[0], addResponse, addTimeAdded, addMsToLive);
|
||||||
// it('should start with an empty cache', () => {
|
|
||||||
// const action = new NullAction();
|
it('should perform the action without affecting the previous state', () => {
|
||||||
// const initialState = responseCacheReducer(undefined, action);
|
// testState has already been frozen above
|
||||||
//
|
responseCacheReducer(testState, action);
|
||||||
// expect(initialState).toEqual(Object.create(null));
|
});
|
||||||
// });
|
|
||||||
//
|
it('should add the response to the cached request', () => {
|
||||||
// describe('FIND_BY_ID', () => {
|
const newState = responseCacheReducer(testState, action);
|
||||||
// const action = new ResponseCacheFindByIDAction(keys[0], services[0], resourceID);
|
expect(newState[keys[0]].timeAdded).toBe(addTimeAdded);
|
||||||
//
|
expect(newState[keys[0]].msToLive).toBe(addMsToLive);
|
||||||
// it('should perform the action without affecting the previous state', () => {
|
expect(newState[keys[0]].response).toBe(addResponse);
|
||||||
// //testState has already been frozen above
|
});
|
||||||
// responseCacheReducer(testState, action);
|
});
|
||||||
// });
|
|
||||||
//
|
describe('REMOVE', () => {
|
||||||
// it('should add the request to the cache', () => {
|
it('should perform the action without affecting the previous state', () => {
|
||||||
// const state = Object.create(null);
|
const action = new ResponseCacheRemoveAction(keys[0]);
|
||||||
// const newState = responseCacheReducer(state, action);
|
// testState has already been frozen above
|
||||||
// expect(newState[keys[0]].key).toBe(keys[0]);
|
responseCacheReducer(testState, action);
|
||||||
// expect(newState[keys[0]].service).toEqual(services[0]);
|
});
|
||||||
// expect(newState[keys[0]].resourceID).toBe(resourceID);
|
|
||||||
// });
|
it('should remove the specified request from the cache', () => {
|
||||||
//
|
const action = new ResponseCacheRemoveAction(keys[0]);
|
||||||
// it('should set responsePending to true', () => {
|
const newState = responseCacheReducer(testState, action);
|
||||||
// const state = Object.create(null);
|
expect(testState[keys[0]]).not.toBeUndefined();
|
||||||
// const newState = responseCacheReducer(state, action);
|
expect(newState[keys[0]]).toBeUndefined();
|
||||||
// expect(newState[keys[0]].responsePending).toBe(true);
|
});
|
||||||
// });
|
|
||||||
//
|
it('shouldn\'t do anything when the specified key isn\'t cached', () => {
|
||||||
// it('should remove any previous error message or resourceUUID for the request', () => {
|
const wrongKey = 'this isn\'t cached';
|
||||||
// const newState = responseCacheReducer(errorState, action);
|
const action = new ResponseCacheRemoveAction(wrongKey);
|
||||||
// expect(newState[keys[0]].resourceUUIDs.length).toBe(0);
|
const newState = responseCacheReducer(testState, action);
|
||||||
// expect(newState[keys[0]].errorMessage).toBeUndefined();
|
expect(testState[wrongKey]).toBeUndefined();
|
||||||
// });
|
expect(newState).toEqual(testState);
|
||||||
// });
|
});
|
||||||
//
|
});
|
||||||
// describe('FIND_ALL', () => {
|
|
||||||
// const action = new ResponseCacheFindAllAction(keys[0], services[0], resourceID, paginationOptions, sortOptions);
|
describe('RESET_TIMESTAMPS', () => {
|
||||||
//
|
const newTimeStamp = new Date().getTime();
|
||||||
// it('should perform the action without affecting the previous state', () => {
|
const action = new ResetResponseCacheTimestampsAction(newTimeStamp);
|
||||||
// //testState has already been frozen above
|
|
||||||
// responseCacheReducer(testState, action);
|
it('should perform the action without affecting the previous state', () => {
|
||||||
// });
|
// testState has already been frozen above
|
||||||
//
|
responseCacheReducer(testState, action);
|
||||||
// it('should add the request to the cache', () => {
|
});
|
||||||
// const state = Object.create(null);
|
|
||||||
// const newState = responseCacheReducer(state, action);
|
it('should set the timestamp of all requests in the cache', () => {
|
||||||
// expect(newState[keys[0]].key).toBe(keys[0]);
|
const newState = responseCacheReducer(testState, action);
|
||||||
// expect(newState[keys[0]].service).toEqual(services[0]);
|
Object.keys(newState).forEach((key) => {
|
||||||
// expect(newState[keys[0]].scopeID).toBe(resourceID);
|
expect(newState[key].timeAdded).toEqual(newTimeStamp);
|
||||||
// expect(newState[keys[0]].paginationOptions).toEqual(paginationOptions);
|
});
|
||||||
// expect(newState[keys[0]].sortOptions).toEqual(sortOptions);
|
});
|
||||||
// });
|
|
||||||
//
|
});
|
||||||
// it('should set responsePending to true', () => {
|
});
|
||||||
// const state = Object.create(null);
|
|
||||||
// const newState = responseCacheReducer(state, action);
|
|
||||||
// expect(newState[keys[0]].responsePending).toBe(true);
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// it('should remove any previous error message or resourceUUIDs for the request', () => {
|
|
||||||
// const newState = responseCacheReducer(errorState, action);
|
|
||||||
// expect(newState[keys[0]].resourceUUIDs.length).toBe(0);
|
|
||||||
// expect(newState[keys[0]].errorMessage).toBeUndefined();
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// describe('SUCCESS', () => {
|
|
||||||
// const successUUIDs = [uuids[0], uuids[2]];
|
|
||||||
// const successTimeAdded = new Date().getTime();
|
|
||||||
// const successMsToLive = 5;
|
|
||||||
// const action = new ResponseCacheSuccessAction(keys[0], successUUIDs, successTimeAdded, successMsToLive);
|
|
||||||
//
|
|
||||||
// it('should perform the action without affecting the previous state', () => {
|
|
||||||
// //testState has already been frozen above
|
|
||||||
// responseCacheReducer(testState, action);
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// it('should add the response to the cached request', () => {
|
|
||||||
// const newState = responseCacheReducer(testState, action);
|
|
||||||
// expect(newState[keys[0]].resourceUUIDs).toBe(successUUIDs);
|
|
||||||
// expect(newState[keys[0]].timeAdded).toBe(successTimeAdded);
|
|
||||||
// expect(newState[keys[0]].msToLive).toBe(successMsToLive);
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// it('should set responsePending to false', () => {
|
|
||||||
// const newState = responseCacheReducer(testState, action);
|
|
||||||
// expect(newState[keys[0]].responsePending).toBe(false);
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// it('should remove any previous error message for the request', () => {
|
|
||||||
// const newState = responseCacheReducer(errorState, action);
|
|
||||||
// expect(newState[keys[0]].errorMessage).toBeUndefined();
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// describe('ERROR', () => {
|
|
||||||
// const errorMsg = 'errorMsg';
|
|
||||||
// const action = new ResponseCacheErrorAction(keys[0], errorMsg);
|
|
||||||
//
|
|
||||||
// it('should perform the action without affecting the previous state', () => {
|
|
||||||
// //testState has already been frozen above
|
|
||||||
// responseCacheReducer(testState, action);
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// it('should set an error message for the request', () => {
|
|
||||||
// const newState = responseCacheReducer(errorState, action);
|
|
||||||
// expect(newState[keys[0]].errorMessage).toBe(errorMsg);
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// it('should set responsePending to false', () => {
|
|
||||||
// const newState = responseCacheReducer(testState, action);
|
|
||||||
// expect(newState[keys[0]].responsePending).toBe(false);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// describe('REMOVE', () => {
|
|
||||||
// it('should perform the action without affecting the previous state', () => {
|
|
||||||
// const action = new ResponseCacheRemoveAction(keys[0]);
|
|
||||||
// //testState has already been frozen above
|
|
||||||
// responseCacheReducer(testState, action);
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// it('should remove the specified request from the cache', () => {
|
|
||||||
// const action = new ResponseCacheRemoveAction(keys[0]);
|
|
||||||
// const newState = responseCacheReducer(testState, action);
|
|
||||||
// expect(testState[keys[0]]).not.toBeUndefined();
|
|
||||||
// expect(newState[keys[0]]).toBeUndefined();
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// it('shouldn't do anything when the specified key isn't cached', () => {
|
|
||||||
// const wrongKey = 'this isn't cached';
|
|
||||||
// const action = new ResponseCacheRemoveAction(wrongKey);
|
|
||||||
// const newState = responseCacheReducer(testState, action);
|
|
||||||
// expect(testState[wrongKey]).toBeUndefined();
|
|
||||||
// expect(newState).toEqual(testState);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// describe('RESET_TIMESTAMPS', () => {
|
|
||||||
// const newTimeStamp = new Date().getTime();
|
|
||||||
// const action = new ResetResponseCacheTimestampsAction(newTimeStamp);
|
|
||||||
//
|
|
||||||
// it('should perform the action without affecting the previous state', () => {
|
|
||||||
// //testState has already been frozen above
|
|
||||||
// responseCacheReducer(testState, action);
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// it('should set the timestamp of all requests in the cache', () => {
|
|
||||||
// const newState = responseCacheReducer(testState, action);
|
|
||||||
// Object.keys(newState).forEach((key) => {
|
|
||||||
// expect(newState[key].timeAdded).toEqual(newTimeStamp);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// });
|
|
||||||
|
222
src/app/core/cache/response-cache.service.spec.ts
vendored
222
src/app/core/cache/response-cache.service.spec.ts
vendored
@@ -1,147 +1,83 @@
|
|||||||
import { OpaqueToken } from '@angular/core';
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
import { ResponseCacheService } from './response-cache.service';
|
import { ResponseCacheService } from './response-cache.service';
|
||||||
import { ResponseCacheState, ResponseCacheEntry } from './response-cache.reducer';
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { RestResponse } from './response-cache.models';
|
||||||
|
import { ResponseCacheEntry } from './response-cache.reducer';
|
||||||
|
|
||||||
// describe('ResponseCacheService', () => {
|
describe('ResponseCacheService', () => {
|
||||||
// let service: ResponseCacheService;
|
let service: ResponseCacheService;
|
||||||
// let store: Store<ResponseCacheState>;
|
let store: Store<CoreState>;
|
||||||
//
|
|
||||||
// const keys = ['125c17f89046283c5f0640722aac9feb', 'a06c3006a41caec5d635af099b0c780c'];
|
const keys = ['125c17f89046283c5f0640722aac9feb', 'a06c3006a41caec5d635af099b0c780c'];
|
||||||
// const serviceTokens = [new OpaqueToken('service1'), new OpaqueToken('service2')];
|
const timestamp = new Date().getTime();
|
||||||
// const resourceID = '9978';
|
const validCacheEntry = (key) => {
|
||||||
// const paginationOptions = { 'resultsPerPage': 10, 'currentPage': 1 };
|
return {
|
||||||
// const sortOptions = { 'field': 'id', 'direction': 0 };
|
key: key,
|
||||||
// const timestamp = new Date().getTime();
|
response: new RestResponse(true, '200'),
|
||||||
// const validCacheEntry = (key) => {
|
timeAdded: timestamp,
|
||||||
// return {
|
msToLive: 24 * 60 * 60 * 1000 // a day
|
||||||
// key: key,
|
}
|
||||||
// timeAdded: timestamp,
|
};
|
||||||
// msToLive: 24 * 60 * 60 * 1000 // a day
|
const invalidCacheEntry = (key) => {
|
||||||
// }
|
return {
|
||||||
// };
|
key: key,
|
||||||
// const invalidCacheEntry = (key) => {
|
response: new RestResponse(true, '200'),
|
||||||
// return {
|
timeAdded: 0,
|
||||||
// key: key,
|
msToLive: 0
|
||||||
// timeAdded: 0,
|
}
|
||||||
// msToLive: 0
|
};
|
||||||
// }
|
|
||||||
// };
|
beforeEach(() => {
|
||||||
//
|
store = new Store<CoreState>(undefined, undefined, undefined);
|
||||||
// beforeEach(() => {
|
spyOn(store, 'dispatch');
|
||||||
// store = new Store<ResponseCacheState>(undefined, undefined, undefined);
|
service = new ResponseCacheService(store);
|
||||||
// spyOn(store, 'dispatch');
|
spyOn(Date.prototype, 'getTime').and.callFake(() => {
|
||||||
// service = new ResponseCacheService(store);
|
return timestamp;
|
||||||
// spyOn(window, 'Date').and.returnValue({ getTime: () => timestamp });
|
});
|
||||||
// });
|
});
|
||||||
//
|
|
||||||
// describe('findAll', () => {
|
describe('get', () => {
|
||||||
// beforeEach(() => {
|
it('should return an observable of the cached request with the specified key', () => {
|
||||||
// spyOn(service, 'get').and.callFake((key) => Observable.of({key: key}));
|
spyOn(store, 'select').and.callFake((...args: any[]) => {
|
||||||
// });
|
return Observable.of(validCacheEntry(keys[1]));
|
||||||
// describe('if the key isn't cached', () => {
|
});
|
||||||
// beforeEach(() => {
|
|
||||||
// spyOn(service, 'has').and.returnValue(false);
|
let testObj: ResponseCacheEntry;
|
||||||
// });
|
service.get(keys[1]).first().subscribe((entry) => {
|
||||||
// it('should dispatch a FIND_ALL action with the key, service, scopeID, paginationOptions and sortOptions', () => {
|
console.log(entry);
|
||||||
// service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions);
|
testObj = entry;
|
||||||
// expect(store.dispatch).toHaveBeenCalledWith(new ResponseCacheFindAllAction(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions))
|
});
|
||||||
// });
|
expect(testObj.key).toEqual(keys[1]);
|
||||||
// it('should return an observable of the newly cached request with the specified key', () => {
|
});
|
||||||
// let result: ResponseCacheEntry;
|
|
||||||
// service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions).take(1).subscribe(entry => result = entry);
|
it('should not return a cached request that has exceeded its time to live', () => {
|
||||||
// expect(result.key).toEqual(keys[0]);
|
spyOn(store, 'select').and.callFake((...args: any[]) => {
|
||||||
// });
|
return Observable.of(invalidCacheEntry(keys[1]));
|
||||||
// });
|
});
|
||||||
// describe('if the key is already cached', () => {
|
|
||||||
// beforeEach(() => {
|
let getObsHasFired = false;
|
||||||
// spyOn(service, 'has').and.returnValue(true);
|
const subscription = service.get(keys[1]).subscribe((entry) => getObsHasFired = true);
|
||||||
// });
|
expect(getObsHasFired).toBe(false);
|
||||||
// it('shouldn't dispatch anything', () => {
|
subscription.unsubscribe();
|
||||||
// service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions);
|
});
|
||||||
// expect(store.dispatch).not.toHaveBeenCalled();
|
});
|
||||||
// });
|
|
||||||
// it('should return an observable of the existing cached request with the specified key', () => {
|
describe('has', () => {
|
||||||
// let result: ResponseCacheEntry;
|
it('should return true if the request with the supplied key is cached and still valid', () => {
|
||||||
// service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions).take(1).subscribe(entry => result = entry);
|
spyOn(store, 'select').and.returnValue(Observable.of(validCacheEntry(keys[1])));
|
||||||
// expect(result.key).toEqual(keys[0]);
|
expect(service.has(keys[1])).toBe(true);
|
||||||
// });
|
});
|
||||||
// });
|
|
||||||
// });
|
it('should return false if the request with the supplied key isn\'t cached', () => {
|
||||||
//
|
spyOn(store, 'select').and.returnValue(Observable.of(undefined));
|
||||||
// describe('findById', () => {
|
expect(service.has(keys[1])).toBe(false);
|
||||||
// beforeEach(() => {
|
});
|
||||||
// spyOn(service, 'get').and.callFake((key) => Observable.of({key: key}));
|
|
||||||
// });
|
it('should return false if the request with the supplied key is cached but has exceeded its time to live', () => {
|
||||||
// describe('if the key isn't cached', () => {
|
spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry(keys[1])));
|
||||||
// beforeEach(() => {
|
expect(service.has(keys[1])).toBe(false);
|
||||||
// spyOn(service, 'has').and.returnValue(false);
|
});
|
||||||
// });
|
});
|
||||||
// it('should dispatch a FIND_BY_ID action with the key, service, and resourceID', () => {
|
});
|
||||||
// service.findById(keys[0], serviceTokens[0], resourceID);
|
|
||||||
// expect(store.dispatch).toHaveBeenCalledWith(new ResponseCacheFindByIDAction(keys[0], serviceTokens[0], resourceID))
|
|
||||||
// });
|
|
||||||
// it('should return an observable of the newly cached request with the specified key', () => {
|
|
||||||
// let result: ResponseCacheEntry;
|
|
||||||
// service.findById(keys[0], serviceTokens[0], resourceID).take(1).subscribe(entry => result = entry);
|
|
||||||
// expect(result.key).toEqual(keys[0]);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// describe('if the key is already cached', () => {
|
|
||||||
// beforeEach(() => {
|
|
||||||
// spyOn(service, 'has').and.returnValue(true);
|
|
||||||
// });
|
|
||||||
// it('shouldn't dispatch anything', () => {
|
|
||||||
// service.findById(keys[0], serviceTokens[0], resourceID);
|
|
||||||
// expect(store.dispatch).not.toHaveBeenCalled();
|
|
||||||
// });
|
|
||||||
// it('should return an observable of the existing cached request with the specified key', () => {
|
|
||||||
// let result: ResponseCacheEntry;
|
|
||||||
// service.findById(keys[0], serviceTokens[0], resourceID).take(1).subscribe(entry => result = entry);
|
|
||||||
// expect(result.key).toEqual(keys[0]);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// describe('get', () => {
|
|
||||||
// it('should return an observable of the cached request with the specified key', () => {
|
|
||||||
// spyOn(store, 'select').and.callFake((...args:Array<any>) => {
|
|
||||||
// return Observable.of(validCacheEntry(args[args.length - 1]));
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// let testObj: ResponseCacheEntry;
|
|
||||||
// service.get(keys[1]).take(1).subscribe(entry => testObj = entry);
|
|
||||||
// expect(testObj.key).toEqual(keys[1]);
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// it('should not return a cached request that has exceeded its time to live', () => {
|
|
||||||
// spyOn(store, 'select').and.callFake((...args:Array<any>) => {
|
|
||||||
// return Observable.of(invalidCacheEntry(args[args.length - 1]));
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// let getObsHasFired = false;
|
|
||||||
// const subscription = service.get(keys[1]).subscribe(entry => getObsHasFired = true);
|
|
||||||
// expect(getObsHasFired).toBe(false);
|
|
||||||
// subscription.unsubscribe();
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// describe('has', () => {
|
|
||||||
// it('should return true if the request with the supplied key is cached and still valid', () => {
|
|
||||||
// spyOn(store, 'select').and.returnValue(Observable.of(validCacheEntry(keys[1])));
|
|
||||||
// expect(service.has(keys[1])).toBe(true);
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// it('should return false if the request with the supplied key isn't cached', () => {
|
|
||||||
// spyOn(store, 'select').and.returnValue(Observable.of(undefined));
|
|
||||||
// expect(service.has(keys[1])).toBe(false);
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// it('should return false if the request with the supplied key is cached but has exceeded its time to live', () => {
|
|
||||||
// spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry(keys[1])));
|
|
||||||
// expect(service.has(keys[1])).toBe(false);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
7
src/app/core/cache/response-cache.service.ts
vendored
7
src/app/core/cache/response-cache.service.ts
vendored
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,7 +25,6 @@ export class ResponseCacheService {
|
|||||||
|
|
||||||
add(key: string, response: RestResponse, msToLive: number): Observable<ResponseCacheEntry> {
|
add(key: string, response: RestResponse, msToLive: number): Observable<ResponseCacheEntry> {
|
||||||
if (!this.has(key)) {
|
if (!this.has(key)) {
|
||||||
// this.store.dispatch(new ResponseCacheFindAllAction(key, service, scopeID, paginationOptions, sortOptions));
|
|
||||||
this.store.dispatch(new ResponseCacheAddAction(key, response, new Date().getTime(), msToLive));
|
this.store.dispatch(new ResponseCacheAddAction(key, response, new Date().getTime(), msToLive));
|
||||||
}
|
}
|
||||||
return this.get(key);
|
return this.get(key);
|
||||||
|
13
src/app/core/config/config-data.ts
Normal file
13
src/app/core/config/config-data.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { ConfigObject } from '../shared/config/config.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class to represent the data retrieved by a configuration service
|
||||||
|
*/
|
||||||
|
export class ConfigData {
|
||||||
|
constructor(
|
||||||
|
public pageInfo: PageInfo,
|
||||||
|
public payload: ConfigObject[]
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
100
src/app/core/config/config.service.spec.ts
Normal file
100
src/app/core/config/config.service.spec.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
|
import { TestScheduler } from 'rxjs/Rx';
|
||||||
|
import { GlobalConfig } from '../../../config';
|
||||||
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { ConfigService } from './config.service';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { ConfigRequest, FindAllOptions } from '../data/request.models';
|
||||||
|
|
||||||
|
const LINK_NAME = 'test';
|
||||||
|
const BROWSE = 'search/findByCollection';
|
||||||
|
|
||||||
|
class TestService extends ConfigService {
|
||||||
|
protected linkName = LINK_NAME;
|
||||||
|
protected browseEndpoint = BROWSE;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected responseCache: ResponseCacheService,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected EnvConfig: GlobalConfig
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ConfigService', () => {
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
let service: TestService;
|
||||||
|
let responseCache: ResponseCacheService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
|
||||||
|
const envConfig = {} as GlobalConfig;
|
||||||
|
const findOptions: FindAllOptions = new FindAllOptions();
|
||||||
|
|
||||||
|
const scopeName = 'traditional';
|
||||||
|
const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d';
|
||||||
|
const configEndpoint = 'https://rest.api/config';
|
||||||
|
const serviceEndpoint = `${configEndpoint}/${LINK_NAME}`;
|
||||||
|
const scopedEndpoint = `${serviceEndpoint}/${scopeName}`;
|
||||||
|
const searchEndpoint = `${serviceEndpoint}/${BROWSE}?uuid=${scopeID}`;
|
||||||
|
|
||||||
|
function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService {
|
||||||
|
return jasmine.createSpyObj('responseCache', {
|
||||||
|
get: cold('c-', {
|
||||||
|
c: { response: { isSuccessful } }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTestService(): TestService {
|
||||||
|
return new TestService(
|
||||||
|
responseCache,
|
||||||
|
requestService,
|
||||||
|
envConfig
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
responseCache = initMockResponseCacheService(true);
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
service = initTestService();
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
spyOn(service, 'getEndpoint').and
|
||||||
|
.returnValue(hot('--a-', { a: serviceEndpoint }));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getConfigByHref', () => {
|
||||||
|
|
||||||
|
it('should configure a new ConfigRequest', () => {
|
||||||
|
const expected = new ConfigRequest(requestService.generateRequestId(), scopedEndpoint);
|
||||||
|
scheduler.schedule(() => service.getConfigByHref(scopedEndpoint).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getConfigByName', () => {
|
||||||
|
|
||||||
|
it('should configure a new ConfigRequest', () => {
|
||||||
|
const expected = new ConfigRequest(requestService.generateRequestId(), scopedEndpoint);
|
||||||
|
scheduler.schedule(() => service.getConfigByName(scopeName).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getConfigBySearch', () => {
|
||||||
|
|
||||||
|
it('should configure a new ConfigRequest', () => {
|
||||||
|
findOptions.scopeID = scopeID;
|
||||||
|
const expected = new ConfigRequest(requestService.generateRequestId(), searchEndpoint);
|
||||||
|
scheduler.schedule(() => service.getConfigBySearch(findOptions).subscribe());
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
113
src/app/core/config/config.service.ts
Normal file
113
src/app/core/config/config.service.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
import { ConfigSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
||||||
|
import { ConfigRequest, FindAllOptions, RestRequest } from '../data/request.models';
|
||||||
|
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||||
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { ConfigData } from './config-data';
|
||||||
|
|
||||||
|
export abstract class ConfigService extends HALEndpointService {
|
||||||
|
protected request: ConfigRequest;
|
||||||
|
protected abstract responseCache: ResponseCacheService;
|
||||||
|
protected abstract requestService: RequestService;
|
||||||
|
protected abstract linkName: string;
|
||||||
|
protected abstract EnvConfig: GlobalConfig;
|
||||||
|
protected abstract browseEndpoint: string;
|
||||||
|
|
||||||
|
protected getConfig(request: RestRequest): Observable<ConfigData> {
|
||||||
|
const [successResponse, errorResponse] = this.responseCache.get(request.href)
|
||||||
|
.map((entry: ResponseCacheEntry) => entry.response)
|
||||||
|
.partition((response: RestResponse) => response.isSuccessful);
|
||||||
|
return Observable.merge(
|
||||||
|
errorResponse.flatMap((response: ErrorResponse) =>
|
||||||
|
Observable.throw(new Error(`Couldn't retrieve the config`))),
|
||||||
|
successResponse
|
||||||
|
.filter((response: ConfigSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.configDefinition))
|
||||||
|
.map((response: ConfigSuccessResponse) => new ConfigData(response.pageInfo, response.configDefinition))
|
||||||
|
.distinctUntilChanged());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getConfigByNameHref(endpoint, resourceName): string {
|
||||||
|
return `${endpoint}/${resourceName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getConfigSearchHref(endpoint, options: FindAllOptions = {}): string {
|
||||||
|
let result;
|
||||||
|
const args = [];
|
||||||
|
|
||||||
|
if (hasValue(options.scopeID)) {
|
||||||
|
result = `${endpoint}/${this.browseEndpoint}`;
|
||||||
|
args.push(`uuid=${options.scopeID}`);
|
||||||
|
} else {
|
||||||
|
result = endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
|
||||||
|
/* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */
|
||||||
|
args.push(`page=${options.currentPage - 1}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasValue(options.elementsPerPage)) {
|
||||||
|
args.push(`size=${options.elementsPerPage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasValue(options.sort)) {
|
||||||
|
let direction = 'asc';
|
||||||
|
if (options.sort.direction === 1) {
|
||||||
|
direction = 'desc';
|
||||||
|
}
|
||||||
|
args.push(`sort=${options.sort.field},${direction}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNotEmpty(args)) {
|
||||||
|
result = `${result}?${args.join('&')}`;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getConfigAll(): Observable<ConfigData> {
|
||||||
|
return this.getEndpoint()
|
||||||
|
.filter((href: string) => isNotEmpty(href))
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL))
|
||||||
|
.do((request: RestRequest) => this.requestService.configure(request))
|
||||||
|
.flatMap((request: RestRequest) => this.getConfig(request))
|
||||||
|
.distinctUntilChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getConfigByHref(href: string): Observable<ConfigData> {
|
||||||
|
const request = new ConfigRequest(this.requestService.generateRequestId(), href);
|
||||||
|
this.requestService.configure(request);
|
||||||
|
|
||||||
|
return this.getConfig(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getConfigByName(name: string): Observable<ConfigData> {
|
||||||
|
return this.getEndpoint()
|
||||||
|
.map((endpoint: string) => this.getConfigByNameHref(endpoint, name))
|
||||||
|
.filter((href: string) => isNotEmpty(href))
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL))
|
||||||
|
.do((request: RestRequest) => this.requestService.configure(request))
|
||||||
|
.flatMap((request: RestRequest) => this.getConfig(request))
|
||||||
|
.distinctUntilChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getConfigBySearch(options: FindAllOptions = {}): Observable<ConfigData> {
|
||||||
|
return this.getEndpoint()
|
||||||
|
.map((endpoint: string) => this.getConfigSearchHref(endpoint, options))
|
||||||
|
.filter((href: string) => isNotEmpty(href))
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL))
|
||||||
|
.do((request: RestRequest) => this.requestService.configure(request))
|
||||||
|
.flatMap((request: RestRequest) => this.getConfig(request))
|
||||||
|
.distinctUntilChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
21
src/app/core/config/submission-definitions-config.service.ts
Normal file
21
src/app/core/config/submission-definitions-config.service.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { ConfigService } from './config.service';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SubmissionDefinitionsConfigService extends ConfigService {
|
||||||
|
protected linkName = 'submissiondefinitions';
|
||||||
|
protected browseEndpoint = 'search/findByCollection';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected responseCache: ResponseCacheService,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
21
src/app/core/config/submission-forms-config.service.ts
Normal file
21
src/app/core/config/submission-forms-config.service.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { ConfigService } from './config.service';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SubmissionFormsConfigService extends ConfigService {
|
||||||
|
protected linkName = 'submissionforms';
|
||||||
|
protected browseEndpoint = '';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected responseCache: ResponseCacheService,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
21
src/app/core/config/submission-sections-config.service.ts
Normal file
21
src/app/core/config/submission-sections-config.service.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { ConfigService } from './config.service';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SubmissionSectionsConfigService extends ConfigService {
|
||||||
|
protected linkName = 'submissionsections';
|
||||||
|
protected browseEndpoint = '';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected responseCache: ResponseCacheService,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,11 +1,11 @@
|
|||||||
|
|
||||||
import { ObjectCacheEffects } from './data/object-cache.effects';
|
import { ObjectCacheEffects } from './cache/object-cache.effects';
|
||||||
import { RequestCacheEffects } from './data/request-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 = [
|
||||||
RequestCacheEffects,
|
ResponseCacheEffects,
|
||||||
RequestEffects,
|
RequestEffects,
|
||||||
ObjectCacheEffects,
|
ObjectCacheEffects,
|
||||||
UUIDIndexEffects,
|
UUIDIndexEffects,
|
||||||
|
@@ -32,7 +32,12 @@ import { ServerResponseService } from '../shared/server-response.service';
|
|||||||
import { NativeWindowFactory, NativeWindowService } from '../shared/window.service';
|
import { NativeWindowFactory, NativeWindowService } from '../shared/window.service';
|
||||||
import { BrowseService } from './browse/browse.service';
|
import { BrowseService } from './browse/browse.service';
|
||||||
import { BrowseResponseParsingService } from './data/browse-response-parsing.service';
|
import { BrowseResponseParsingService } from './data/browse-response-parsing.service';
|
||||||
|
import { ConfigResponseParsingService } from './data/config-response-parsing.service';
|
||||||
import { RouteService } from '../shared/route.service';
|
import { RouteService } from '../shared/route.service';
|
||||||
|
import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service';
|
||||||
|
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
|
||||||
|
import { SubmissionSectionsConfigService } from './config/submission-sections-config.service';
|
||||||
|
import { UUIDService } from './shared/uuid.service';
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -66,7 +71,12 @@ const PROVIDERS = [
|
|||||||
ServerResponseService,
|
ServerResponseService,
|
||||||
BrowseResponseParsingService,
|
BrowseResponseParsingService,
|
||||||
BrowseService,
|
BrowseService,
|
||||||
|
ConfigResponseParsingService,
|
||||||
RouteService,
|
RouteService,
|
||||||
|
SubmissionDefinitionsConfigService,
|
||||||
|
SubmissionFormsConfigService,
|
||||||
|
SubmissionSectionsConfigService,
|
||||||
|
UUIDService,
|
||||||
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
|
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -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');
|
||||||
|
135
src/app/core/data/base-response-parsing.service.ts
Normal file
135
src/app/core/data/base-response-parsing.service.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||||
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
|
|
||||||
|
function isObjectLevel(halObj: any) {
|
||||||
|
return isNotEmpty(halObj._links) && hasValue(halObj._links.self);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPaginatedResponse(halObj: any) {
|
||||||
|
return isNotEmpty(halObj.page) && hasValue(halObj._embedded);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
|
class ProcessRequestDTO<ObjectDomain> {
|
||||||
|
[key: string]: ObjectDomain[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class BaseResponseParsingService {
|
||||||
|
protected abstract EnvConfig: GlobalConfig;
|
||||||
|
protected abstract objectCache: ObjectCacheService;
|
||||||
|
protected abstract objectFactory: any;
|
||||||
|
protected abstract toCache: boolean;
|
||||||
|
|
||||||
|
protected process<ObjectDomain,ObjectType>(data: any, requestHref: string): ProcessRequestDTO<ObjectDomain> {
|
||||||
|
|
||||||
|
if (isNotEmpty(data)) {
|
||||||
|
if (isPaginatedResponse(data)) {
|
||||||
|
return this.process(data._embedded, requestHref);
|
||||||
|
} else if (isObjectLevel(data)) {
|
||||||
|
return { topLevel: this.deserializeAndCache(data, requestHref) };
|
||||||
|
} else {
|
||||||
|
const result = new ProcessRequestDTO<ObjectDomain>();
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
result.topLevel = [];
|
||||||
|
data.forEach((datum) => {
|
||||||
|
if (isPaginatedResponse(datum)) {
|
||||||
|
const obj = this.process(datum, requestHref);
|
||||||
|
result.topLevel = [...result.topLevel, ...this.flattenSingleKeyObject(obj)];
|
||||||
|
} else {
|
||||||
|
result.topLevel = [...result.topLevel, ...this.deserializeAndCache<ObjectDomain,ObjectType>(datum, requestHref)];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Object.keys(data)
|
||||||
|
.filter((property) => data.hasOwnProperty(property))
|
||||||
|
.filter((property) => hasValue(data[property]))
|
||||||
|
.forEach((property) => {
|
||||||
|
if (isPaginatedResponse(data[property])) {
|
||||||
|
const obj = this.process(data[property], requestHref);
|
||||||
|
result[property] = this.flattenSingleKeyObject(obj);
|
||||||
|
} else {
|
||||||
|
result[property] = this.deserializeAndCache(data[property], requestHref);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected deserializeAndCache<ObjectDomain,ObjectType>(obj, requestHref: string): ObjectDomain[] {
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
let result = [];
|
||||||
|
obj.forEach((o) => result = [...result, ...this.deserializeAndCache<ObjectDomain,ObjectType>(o, requestHref)]);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type: ObjectType = obj.type;
|
||||||
|
if (hasValue(type)) {
|
||||||
|
const normObjConstructor = this.objectFactory.getConstructor(type) as GenericConstructor<ObjectDomain>;
|
||||||
|
|
||||||
|
if (hasValue(normObjConstructor)) {
|
||||||
|
const serializer = new DSpaceRESTv2Serializer(normObjConstructor);
|
||||||
|
|
||||||
|
let processed;
|
||||||
|
if (isNotEmpty(obj._embedded)) {
|
||||||
|
processed = this.process<ObjectDomain,ObjectType>(obj._embedded, requestHref);
|
||||||
|
}
|
||||||
|
const normalizedObj: any = serializer.deserialize(obj);
|
||||||
|
|
||||||
|
if (isNotEmpty(processed)) {
|
||||||
|
const processedList = {};
|
||||||
|
Object.keys(processed).forEach((key) => {
|
||||||
|
processedList[key] = processed[key].map((no: NormalizedObject) => (this.toCache) ? no.self : no);
|
||||||
|
});
|
||||||
|
Object.assign(normalizedObj, processedList);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.toCache) {
|
||||||
|
this.addToObjectCache(normalizedObj, requestHref);
|
||||||
|
}
|
||||||
|
return [normalizedObj] as any;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// TODO: move check to Validator?
|
||||||
|
// throw new Error(`The server returned an object with an unknown a known type: ${type}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// TODO: move check to Validator
|
||||||
|
// throw new Error(`The server returned an object without a type: ${JSON.stringify(obj)}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addToObjectCache(co: CacheableObject, requestHref: string): void {
|
||||||
|
if (hasNoValue(co) || hasNoValue(co.self)) {
|
||||||
|
throw new Error('The server returned an invalid object');
|
||||||
|
}
|
||||||
|
this.objectCache.add(co, this.EnvConfig.cache.msToLive, requestHref);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processPageInfo(pageObj: any): PageInfo {
|
||||||
|
if (isNotEmpty(pageObj)) {
|
||||||
|
return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected flattenSingleKeyObject(obj: any): any {
|
||||||
|
const keys = Object.keys(obj);
|
||||||
|
if (keys.length !== 1) {
|
||||||
|
throw new Error(`Expected an object with a single key, got: ${JSON.stringify(obj)}`);
|
||||||
|
}
|
||||||
|
return obj[keys[0]];
|
||||||
|
}
|
||||||
|
}
|
@@ -2,6 +2,7 @@ import { BrowseResponseParsingService } from './browse-response-parsing.service'
|
|||||||
import { BrowseEndpointRequest } from './request.models';
|
import { BrowseEndpointRequest } from './request.models';
|
||||||
import { BrowseSuccessResponse, ErrorResponse } from '../cache/response-cache.models';
|
import { BrowseSuccessResponse, ErrorResponse } from '../cache/response-cache.models';
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
|
||||||
describe('BrowseResponseParsingService', () => {
|
describe('BrowseResponseParsingService', () => {
|
||||||
let service: BrowseResponseParsingService;
|
let service: BrowseResponseParsingService;
|
||||||
@@ -11,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: {
|
||||||
@@ -48,7 +49,7 @@ describe('BrowseResponseParsingService', () => {
|
|||||||
_links: { self: { href: 'https://rest.api/discover/browses' } },
|
_links: { self: { href: 'https://rest.api/discover/browses' } },
|
||||||
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
||||||
}, statusCode: '200'
|
}, statusCode: '200'
|
||||||
};
|
} as DSpaceRESTV2Response;
|
||||||
|
|
||||||
const invalidResponse1 = {
|
const invalidResponse1 = {
|
||||||
payload: {
|
payload: {
|
||||||
@@ -71,22 +72,21 @@ describe('BrowseResponseParsingService', () => {
|
|||||||
_links: { self: { href: 'https://rest.api/discover/browses' } },
|
_links: { self: { href: 'https://rest.api/discover/browses' } },
|
||||||
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
||||||
}, statusCode: '200'
|
}, statusCode: '200'
|
||||||
};
|
} as DSpaceRESTV2Response;
|
||||||
|
|
||||||
const invalidResponse2 = {
|
const invalidResponse2 = {
|
||||||
payload: {
|
payload: {
|
||||||
browses: [{}, {}],
|
|
||||||
_links: { self: { href: 'https://rest.api/discover/browses' } },
|
_links: { self: { href: 'https://rest.api/discover/browses' } },
|
||||||
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
||||||
}, statusCode: '200'
|
}, statusCode: '200'
|
||||||
};
|
} as DSpaceRESTV2Response ;
|
||||||
|
|
||||||
const invalidResponse3 = {
|
const invalidResponse3 = {
|
||||||
payload: {
|
payload: {
|
||||||
_links: { self: { href: 'https://rest.api/discover/browses' } },
|
_links: { self: { href: 'https://rest.api/discover/browses' } },
|
||||||
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
||||||
}, statusCode: '500'
|
}, statusCode: '500'
|
||||||
};
|
} as DSpaceRESTV2Response;
|
||||||
|
|
||||||
const definitions = [
|
const definitions = [
|
||||||
Object.assign(new BrowseDefinition(), {
|
Object.assign(new BrowseDefinition(), {
|
||||||
|
@@ -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,11 +63,7 @@ describe('ComColDataService', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initMockRequestService(): RequestService {
|
function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService {
|
||||||
return jasmine.createSpyObj('requestService', ['configure']);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initMockResponceCacheService(isSuccessful: boolean): ResponseCacheService {
|
|
||||||
return jasmine.createSpyObj('responseCache', {
|
return jasmine.createSpyObj('responseCache', {
|
||||||
get: cold('c-', {
|
get: cold('c-', {
|
||||||
c: { response: { isSuccessful } }
|
c: { response: { isSuccessful } }
|
||||||
@@ -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 = initMockResponceCacheService(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,9 +118,9 @@ 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 = initMockResponceCacheService(true);
|
responseCache = initMockResponseCacheService(true);
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -144,9 +141,9 @@ 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 = initMockResponceCacheService(false);
|
responseCache = initMockResponseCacheService(false);
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -161,14 +158,14 @@ 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 = initMockResponceCacheService(true);
|
responseCache = initMockResponseCacheService(true);
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return this.getEndpoint()', () => {
|
it('should return this.getEndpoint()', () => {
|
||||||
spyOn(service, 'getEndpoint').and.returnValue(cold('--e-', { e: serviceEndpoint }))
|
spyOn(service, 'getEndpoint').and.returnValue(cold('--e-', { e: serviceEndpoint }));
|
||||||
|
|
||||||
const result = service.getScopedEndpoint(undefined);
|
const result = service.getScopedEndpoint(undefined);
|
||||||
const expected = cold('--f-', { f: serviceEndpoint });
|
const expected = cold('--f-', { f: serviceEndpoint });
|
||||||
|
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
241
src/app/core/data/config-response-parsing.service.spec.ts
Normal file
241
src/app/core/data/config-response-parsing.service.spec.ts
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
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 { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
import { ConfigRequest } from './request.models';
|
||||||
|
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { SubmissionDefinitionsModel } from '../shared/config/config-submission-definitions.model';
|
||||||
|
import { SubmissionSectionModel } from '../shared/config/config-submission-section.model';
|
||||||
|
|
||||||
|
describe('ConfigResponseParsingService', () => {
|
||||||
|
let service: ConfigResponseParsingService;
|
||||||
|
|
||||||
|
const EnvConfig = {} as GlobalConfig;
|
||||||
|
const store = {} as Store<CoreState>;
|
||||||
|
const objectCacheService = new ObjectCacheService(store);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new ConfigResponseParsingService(EnvConfig, objectCacheService);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parse', () => {
|
||||||
|
const validRequest = new ConfigRequest('69f375b5-19f4-4453-8c7a-7dc5c55aafbb', 'https://rest.api/config/submissiondefinitions/traditional');
|
||||||
|
|
||||||
|
const validResponse = {
|
||||||
|
payload: {
|
||||||
|
id:'traditional',
|
||||||
|
name:'traditional',
|
||||||
|
type:'submissiondefinition',
|
||||||
|
isDefault:true,
|
||||||
|
_links:{
|
||||||
|
sections:{
|
||||||
|
href:'https://rest.api/config/submissiondefinitions/traditional/sections'
|
||||||
|
},self:{
|
||||||
|
href:'https://rest.api/config/submissiondefinitions/traditional'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_embedded:{
|
||||||
|
sections:{
|
||||||
|
page:{
|
||||||
|
number:0,
|
||||||
|
size:4,
|
||||||
|
totalPages:1,totalElements:4
|
||||||
|
},
|
||||||
|
_embedded:[
|
||||||
|
{
|
||||||
|
id:'traditionalpageone',header:'submit.progressbar.describe.stepone',
|
||||||
|
mandatory:true,
|
||||||
|
sectionType:'submission-form',
|
||||||
|
visibility:{
|
||||||
|
main:null,
|
||||||
|
other:'READONLY'
|
||||||
|
},
|
||||||
|
type:'submissionsection',
|
||||||
|
_links:{
|
||||||
|
self:{
|
||||||
|
href:'https://rest.api/config/submissionsections/traditionalpageone'
|
||||||
|
},
|
||||||
|
config:{
|
||||||
|
href:'https://rest.api/config/submissionforms/traditionalpageone'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
id:'traditionalpagetwo',
|
||||||
|
header:'submit.progressbar.describe.steptwo',
|
||||||
|
mandatory:true,
|
||||||
|
sectionType:'submission-form',
|
||||||
|
visibility:{
|
||||||
|
main:null,
|
||||||
|
other:'READONLY'
|
||||||
|
},
|
||||||
|
type:'submissionsection',
|
||||||
|
_links:{
|
||||||
|
self:{
|
||||||
|
href:'https://rest.api/config/submissionsections/traditionalpagetwo'
|
||||||
|
},
|
||||||
|
config:{
|
||||||
|
href:'https://rest.api/config/submissionforms/traditionalpagetwo'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
id:'upload',
|
||||||
|
header:'submit.progressbar.upload',
|
||||||
|
mandatory:false,
|
||||||
|
sectionType:'upload',
|
||||||
|
visibility:{
|
||||||
|
main:null,
|
||||||
|
other:'READONLY'
|
||||||
|
},
|
||||||
|
type:'submissionsection',
|
||||||
|
_links:{
|
||||||
|
self:{
|
||||||
|
href:'https://rest.api/config/submissionsections/upload'
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
href:'https://rest.api/config/submissionuploads/upload'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
id:'license',
|
||||||
|
header:'submit.progressbar.license',
|
||||||
|
mandatory:true,
|
||||||
|
sectionType:'license',
|
||||||
|
visibility:{
|
||||||
|
main:null,
|
||||||
|
other:'READONLY'
|
||||||
|
},
|
||||||
|
type:'submissionsection',
|
||||||
|
_links:{
|
||||||
|
self:{
|
||||||
|
href:'https://rest.api/config/submissionsections/license'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
_links:{
|
||||||
|
self:'https://rest.api/config/submissiondefinitions/traditional/sections'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
statusCode:'200'
|
||||||
|
};
|
||||||
|
|
||||||
|
const invalidResponse1 = {
|
||||||
|
payload: {},
|
||||||
|
statusCode:'200'
|
||||||
|
};
|
||||||
|
|
||||||
|
const invalidResponse2 = {
|
||||||
|
payload: {
|
||||||
|
id:'traditional',
|
||||||
|
name:'traditional',
|
||||||
|
type:'submissiondefinition',
|
||||||
|
isDefault:true,
|
||||||
|
_links:{},
|
||||||
|
_embedded:{
|
||||||
|
sections:{
|
||||||
|
page:{
|
||||||
|
number:0,
|
||||||
|
size:4,
|
||||||
|
totalPages:1,totalElements:4
|
||||||
|
},
|
||||||
|
_embedded:[{},{}],
|
||||||
|
_links:{
|
||||||
|
self:'https://rest.api/config/submissiondefinitions/traditional/sections'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
statusCode:'200'
|
||||||
|
};
|
||||||
|
|
||||||
|
const invalidResponse3 = {
|
||||||
|
payload: {
|
||||||
|
_links: { self: { href: 'https://rest.api/config/submissiondefinitions/traditional' } },
|
||||||
|
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
|
||||||
|
}, statusCode: '500'
|
||||||
|
};
|
||||||
|
|
||||||
|
const definitions = [
|
||||||
|
Object.assign(new SubmissionDefinitionsModel(), {
|
||||||
|
isDefault: true,
|
||||||
|
name: 'traditional',
|
||||||
|
type: 'submissiondefinition',
|
||||||
|
_links: {},
|
||||||
|
sections: [
|
||||||
|
Object.assign(new SubmissionSectionModel(), {
|
||||||
|
header: 'submit.progressbar.describe.stepone',
|
||||||
|
mandatory: true,
|
||||||
|
sectionType: 'submission-form',
|
||||||
|
visibility:{
|
||||||
|
main:null,
|
||||||
|
other:'READONLY'
|
||||||
|
},
|
||||||
|
type: 'submissionsection',
|
||||||
|
_links: {}
|
||||||
|
}),
|
||||||
|
Object.assign(new SubmissionSectionModel(), {
|
||||||
|
header: 'submit.progressbar.describe.steptwo',
|
||||||
|
mandatory: true,
|
||||||
|
sectionType: 'submission-form',
|
||||||
|
visibility:{
|
||||||
|
main:null,
|
||||||
|
other:'READONLY'
|
||||||
|
},
|
||||||
|
type: 'submissionsection',
|
||||||
|
_links: {}
|
||||||
|
}),
|
||||||
|
Object.assign(new SubmissionSectionModel(), {
|
||||||
|
header: 'submit.progressbar.upload',
|
||||||
|
mandatory: false,
|
||||||
|
sectionType: 'upload',
|
||||||
|
visibility:{
|
||||||
|
main:null,
|
||||||
|
other:'READONLY'
|
||||||
|
},
|
||||||
|
type: 'submissionsection',
|
||||||
|
_links: {}
|
||||||
|
}),
|
||||||
|
Object.assign(new SubmissionSectionModel(), {
|
||||||
|
header: 'submit.progressbar.license',
|
||||||
|
mandatory: true,
|
||||||
|
sectionType: 'license',
|
||||||
|
visibility:{
|
||||||
|
main:null,
|
||||||
|
other:'READONLY'
|
||||||
|
},
|
||||||
|
type: 'submissionsection',
|
||||||
|
_links: {}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
it('should return a ConfigSuccessResponse if data contains a valid config endpoint response', () => {
|
||||||
|
const response = service.parse(validRequest, validResponse);
|
||||||
|
expect(response.constructor).toBe(ConfigSuccessResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an ErrorResponse if data contains an invalid config endpoint response', () => {
|
||||||
|
const response1 = service.parse(validRequest, invalidResponse1);
|
||||||
|
const response2 = service.parse(validRequest, invalidResponse2);
|
||||||
|
expect(response1.constructor).toBe(ErrorResponse);
|
||||||
|
expect(response2.constructor).toBe(ErrorResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an ErrorResponse if data contains a statuscode other than 200', () => {
|
||||||
|
const response = service.parse(validRequest, invalidResponse3);
|
||||||
|
expect(response.constructor).toBe(ErrorResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a ConfigSuccessResponse with the ConfigDefinitions in data', () => {
|
||||||
|
const response = service.parse(validRequest, validResponse);
|
||||||
|
expect((response as any).configDefinition).toEqual(definitions);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
43
src/app/core/data/config-response-parsing.service.ts
Normal file
43
src/app/core/data/config-response-parsing.service.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { RestRequest } from './request.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { ConfigSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { ConfigObjectFactory } from '../shared/config/config-object-factory';
|
||||||
|
|
||||||
|
import { ConfigObject } from '../shared/config/config.model';
|
||||||
|
import { ConfigType } from '../shared/config/config-type';
|
||||||
|
import { BaseResponseParsingService } from './base-response-parsing.service';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ConfigResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
||||||
|
|
||||||
|
protected objectFactory = ConfigObjectFactory;
|
||||||
|
protected toCache = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
) { super();
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && data.statusCode === '200') {
|
||||||
|
const configDefinition = this.process<ConfigObject,ConfigType>(data.payload, request.href);
|
||||||
|
return new ConfigSuccessResponse(configDefinition[Object.keys(configDefinition)[0]], data.statusCode, this.processPageInfo(data.payload.page));
|
||||||
|
} else {
|
||||||
|
return new ErrorResponse(
|
||||||
|
Object.assign(
|
||||||
|
new Error('Unexpected response from config endpoint'),
|
||||||
|
{statusText: data.statusCode}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -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);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
@@ -1,149 +1,34 @@
|
|||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
|
||||||
import { Inject, Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
import { GLOBAL_CONFIG } from '../../../config';
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
import { NormalizedObject } from '../cache/models/normalized-object.model';
|
||||||
import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util';
|
|
||||||
import { ResourceType } from '../shared/resource-type';
|
import { ResourceType } from '../shared/resource-type';
|
||||||
import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory';
|
import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory';
|
||||||
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
|
||||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
import { RestResponse, DSOSuccessResponse } from '../cache/response-cache.models';
|
import { RestResponse, DSOSuccessResponse } from '../cache/response-cache.models';
|
||||||
import { RestRequest } from './request.models';
|
import { RestRequest } from './request.models';
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
|
||||||
import { ResponseParsingService } from './parsing.service';
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { BaseResponseParsingService } from './base-response-parsing.service';
|
||||||
function isObjectLevel(halObj: any) {
|
|
||||||
return isNotEmpty(halObj._links) && hasValue(halObj._links.self);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPaginatedResponse(halObj: any) {
|
|
||||||
return isNotEmpty(halObj.page) && hasValue(halObj._embedded);
|
|
||||||
}
|
|
||||||
|
|
||||||
function flattenSingleKeyObject(obj: any): any {
|
|
||||||
const keys = Object.keys(obj);
|
|
||||||
if (keys.length !== 1) {
|
|
||||||
throw new Error(`Expected an object with a single key, got: ${JSON.stringify(obj)}`);
|
|
||||||
}
|
|
||||||
return obj[keys[0]];
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
|
||||||
class ProcessRequestDTO {
|
|
||||||
[key: string]: NormalizedObject[]
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DSOResponseParsingService implements ResponseParsingService {
|
export class DSOResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
||||||
|
|
||||||
|
protected objectFactory = NormalizedObjectFactory;
|
||||||
|
protected toCache = true;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig,
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
private objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
) {
|
) { super();
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
const processRequestDTO = this.process(data.payload, request.href);
|
const processRequestDTO = this.process<NormalizedObject,ResourceType>(data.payload, request.href);
|
||||||
const selfLinks = flattenSingleKeyObject(processRequestDTO).map((no) => no.self);
|
const selfLinks = this.flattenSingleKeyObject(processRequestDTO).map((no) => no.self);
|
||||||
return new DSOSuccessResponse(selfLinks, data.statusCode, this.processPageInfo(data.payload.page))
|
return new DSOSuccessResponse(selfLinks, data.statusCode, this.processPageInfo(data.payload.page))
|
||||||
}
|
}
|
||||||
|
|
||||||
protected process(data: any, requestHref: string): ProcessRequestDTO {
|
|
||||||
|
|
||||||
if (isNotEmpty(data)) {
|
|
||||||
if (isPaginatedResponse(data)) {
|
|
||||||
return this.process(data._embedded, requestHref);
|
|
||||||
} else if (isObjectLevel(data)) {
|
|
||||||
return { topLevel: this.deserializeAndCache(data, requestHref) };
|
|
||||||
} else {
|
|
||||||
const result = new ProcessRequestDTO();
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
result.topLevel = [];
|
|
||||||
data.forEach((datum) => {
|
|
||||||
if (isPaginatedResponse(datum)) {
|
|
||||||
const obj = this.process(datum, requestHref);
|
|
||||||
result.topLevel = [...result.topLevel, ...flattenSingleKeyObject(obj)];
|
|
||||||
} else {
|
|
||||||
result.topLevel = [...result.topLevel, ...this.deserializeAndCache(datum, requestHref)];
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Object.keys(data)
|
|
||||||
.filter((property) => data.hasOwnProperty(property))
|
|
||||||
.filter((property) => hasValue(data[property]))
|
|
||||||
.forEach((property) => {
|
|
||||||
if (isPaginatedResponse(data[property])) {
|
|
||||||
const obj = this.process(data[property], requestHref);
|
|
||||||
result[property] = flattenSingleKeyObject(obj);
|
|
||||||
} else {
|
|
||||||
result[property] = this.deserializeAndCache(data[property], requestHref);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected deserializeAndCache(obj, requestHref: string): NormalizedObject[] {
|
|
||||||
if (Array.isArray(obj)) {
|
|
||||||
let result = [];
|
|
||||||
obj.forEach((o) => result = [...result, ...this.deserializeAndCache(o, requestHref)])
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const type: ResourceType = obj.type;
|
|
||||||
if (hasValue(type)) {
|
|
||||||
const normObjConstructor = NormalizedObjectFactory.getConstructor(type);
|
|
||||||
|
|
||||||
if (hasValue(normObjConstructor)) {
|
|
||||||
const serializer = new DSpaceRESTv2Serializer(normObjConstructor);
|
|
||||||
|
|
||||||
let processed;
|
|
||||||
if (isNotEmpty(obj._embedded)) {
|
|
||||||
processed = this.process(obj._embedded, requestHref);
|
|
||||||
}
|
|
||||||
const normalizedObj = serializer.deserialize(obj);
|
|
||||||
|
|
||||||
if (isNotEmpty(processed)) {
|
|
||||||
const linksOnly = {};
|
|
||||||
Object.keys(processed).forEach((key) => {
|
|
||||||
linksOnly[key] = processed[key].map((no: NormalizedObject) => no.self);
|
|
||||||
});
|
|
||||||
Object.assign(normalizedObj, linksOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addToObjectCache(normalizedObj, requestHref);
|
|
||||||
return [normalizedObj];
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// TODO: move check to Validator?
|
|
||||||
// throw new Error(`The server returned an object with an unknown a known type: ${type}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// TODO: move check to Validator
|
|
||||||
// throw new Error(`The server returned an object without a type: ${JSON.stringify(obj)}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected addToObjectCache(co: CacheableObject, requestHref: string): void {
|
|
||||||
if (hasNoValue(co) || hasNoValue(co.self)) {
|
|
||||||
throw new Error('The server returned an invalid object');
|
|
||||||
}
|
|
||||||
this.objectCache.add(co, this.EnvConfig.cache.msToLive, requestHref);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected processPageInfo(pageObj: any): PageInfo {
|
|
||||||
if (isNotEmpty(pageObj)) {
|
|
||||||
return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
/* tslint:enable:max-classes-per-file */
|
|
||||||
|
42
src/app/core/data/paginated-list.ts
Normal file
42
src/app/core/data/paginated-list.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
7
src/app/core/data/remote-data-error.ts
Normal file
7
src/app/core/data/remote-data-error.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export class RemoteDataError {
|
||||||
|
constructor(
|
||||||
|
public statusCode: string,
|
||||||
|
public message: string
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
@@ -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
|
||||||
|
@@ -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 */
|
||||||
|
@@ -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(
|
||||||
|
@@ -6,24 +6,120 @@ import { DSOResponseParsingService } from './dso-response-parsing.service';
|
|||||||
import { ResponseParsingService } from './parsing.service';
|
import { ResponseParsingService } from './parsing.service';
|
||||||
import { RootResponseParsingService } from './root-response-parsing.service';
|
import { RootResponseParsingService } from './root-response-parsing.service';
|
||||||
import { BrowseResponseParsingService } from './browse-response-parsing.service';
|
import { BrowseResponseParsingService } from './browse-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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,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> {
|
||||||
@@ -54,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> {
|
||||||
@@ -64,6 +161,16 @@ export class BrowseEndpointRequest extends RestRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ConfigRequest extends GetRequest {
|
||||||
|
constructor(uuid: string, href: string) {
|
||||||
|
super(uuid, href);
|
||||||
|
}
|
||||||
|
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return ConfigResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class RequestError extends Error {
|
export class RequestError extends Error {
|
||||||
statusText: string;
|
statusText: string;
|
||||||
}
|
}
|
||||||
|
85
src/app/core/data/request.reducer.spec.ts
Normal file
85
src/app/core/data/request.reducer.spec.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import * as deepFreeze from 'deep-freeze';
|
||||||
|
|
||||||
|
import { requestReducer, RequestState } from './request.reducer';
|
||||||
|
import {
|
||||||
|
RequestCompleteAction, RequestConfigureAction, RequestExecuteAction
|
||||||
|
} from './request.actions';
|
||||||
|
import { GetRequest, RestRequest } from './request.models';
|
||||||
|
|
||||||
|
class NullAction extends RequestCompleteAction {
|
||||||
|
type = null;
|
||||||
|
payload = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 link2 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/1911e8a4-6939-490c-b58b-a5d70f8d91fb';
|
||||||
|
const testState: RequestState = {
|
||||||
|
[id1]: {
|
||||||
|
request: new GetRequest(id1, link1),
|
||||||
|
requestPending: false,
|
||||||
|
responsePending: false,
|
||||||
|
completed: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
deepFreeze(testState);
|
||||||
|
|
||||||
|
it('should return the current state when no valid actions have been made', () => {
|
||||||
|
const action = new NullAction();
|
||||||
|
const newState = requestReducer(testState, action);
|
||||||
|
|
||||||
|
expect(newState).toEqual(testState);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start with an empty state', () => {
|
||||||
|
const action = new NullAction();
|
||||||
|
const initialState = requestReducer(undefined, action);
|
||||||
|
|
||||||
|
expect(initialState).toEqual(Object.create(null));
|
||||||
|
});
|
||||||
|
|
||||||
|
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 request = new GetRequest(id2, link2);
|
||||||
|
|
||||||
|
const action = new RequestConfigureAction(request);
|
||||||
|
const newState = requestReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState[id2].request.uuid).toEqual(id2);
|
||||||
|
expect(newState[id2].request.href).toEqual(link2);
|
||||||
|
expect(newState[id2].requestPending).toEqual(true);
|
||||||
|
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', () => {
|
||||||
|
const state = testState;
|
||||||
|
|
||||||
|
const action = new RequestExecuteAction(id1);
|
||||||
|
const newState = requestReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState[id1].request.uuid).toEqual(id1);
|
||||||
|
expect(newState[id1].request.href).toEqual(link1);
|
||||||
|
expect(newState[id1].requestPending).toEqual(false);
|
||||||
|
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', () => {
|
||||||
|
const state = testState;
|
||||||
|
|
||||||
|
const action = new RequestCompleteAction(id1);
|
||||||
|
const newState = requestReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState[id1].request.uuid).toEqual(id1);
|
||||||
|
expect(newState[id1].request.href).toEqual(link1);
|
||||||
|
expect(newState[id1].requestPending).toEqual(state[id1].requestPending);
|
||||||
|
expect(newState[id1].responsePending).toEqual(false);
|
||||||
|
expect(newState[id1].completed).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
@@ -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,
|
||||||
|
446
src/app/core/data/request.service.spec.ts
Normal file
446
src/app/core/data/request.service.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -10,43 +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 store: Store<CoreState>
|
private uuidService: UUIDService,
|
||||||
) {
|
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)
|
||||||
@@ -55,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)
|
||||||
@@ -83,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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private dispatchRequest(request: RestRequest) {
|
||||||
this.store.dispatch(new RequestConfigureAction(request));
|
this.store.dispatch(new RequestConfigureAction(request));
|
||||||
this.store.dispatch(new RequestExecuteAction(request.href));
|
this.store.dispatch(new RequestExecuteAction(request.uuid));
|
||||||
this.trackRequestsOnTheirWayToTheStore(request.href);
|
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)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
export interface DSpaceRESTV2Response {
|
export interface DSpaceRESTV2Response {
|
||||||
payload: {
|
payload: {
|
||||||
|
[name: string]: any;
|
||||||
_embedded?: any;
|
_embedded?: any;
|
||||||
_links?: any;
|
_links?: any;
|
||||||
page?: any;
|
page?: any;
|
||||||
|
68
src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts
Normal file
68
src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { TestBed, inject } from '@angular/core/testing';
|
||||||
|
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||||
|
|
||||||
|
import { DSpaceRESTv2Service } from './dspace-rest-v2.service';
|
||||||
|
|
||||||
|
describe('DSpaceRESTv2Service', () => {
|
||||||
|
let dSpaceRESTv2Service: DSpaceRESTv2Service;
|
||||||
|
let httpMock: HttpTestingController;
|
||||||
|
const url = 'http://www.dspace.org/';
|
||||||
|
const mockError = new ErrorEvent('test error');
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
|
providers: [DSpaceRESTv2Service]
|
||||||
|
});
|
||||||
|
|
||||||
|
dSpaceRESTv2Service = TestBed.get(DSpaceRESTv2Service);
|
||||||
|
httpMock = TestBed.get(HttpTestingController);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => httpMock.verify());
|
||||||
|
|
||||||
|
it('should be created', inject([DSpaceRESTv2Service], (service: DSpaceRESTv2Service) => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('#get', () => {
|
||||||
|
it('should return an Observable<DSpaceRESTV2Response>', () => {
|
||||||
|
const mockPayload = {
|
||||||
|
page: 1
|
||||||
|
};
|
||||||
|
const mockStatusCode = 'GREAT';
|
||||||
|
|
||||||
|
dSpaceRESTv2Service.get(url).subscribe((response) => {
|
||||||
|
expect(response).toBeTruthy();
|
||||||
|
expect(response.statusCode).toEqual(mockStatusCode);
|
||||||
|
expect(response.payload.page).toEqual(mockPayload.page);
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = httpMock.expectOne(url);
|
||||||
|
expect(req.request.method).toBe('GET');
|
||||||
|
req.flush(mockPayload, { statusText: mockStatusCode});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error', () => {
|
||||||
|
dSpaceRESTv2Service.get(url).subscribe(() => undefined, (err) => {
|
||||||
|
expect(err.error).toBe(mockError);
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = httpMock.expectOne(url);
|
||||||
|
expect(req.request.method).toBe('GET');
|
||||||
|
req.error(mockError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log an error', () => {
|
||||||
|
spyOn(console, 'log');
|
||||||
|
|
||||||
|
dSpaceRESTv2Service.get(url).subscribe(() => undefined, (err) => {
|
||||||
|
expect(console.log).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = httpMock.expectOne(url);
|
||||||
|
expect(req.request.method).toBe('GET');
|
||||||
|
req.error(mockError);
|
||||||
|
});
|
||||||
|
});
|
@@ -1,19 +1,18 @@
|
|||||||
import { Inject, Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Http, RequestOptionsArgs } from '@angular/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 { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
|
||||||
import { DSpaceRESTV2Response } from './dspace-rest-v2-response.model';
|
import { DSpaceRESTV2Response } from './dspace-rest-v2-response.model';
|
||||||
|
|
||||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to access DSpace's REST API
|
* Service to access DSpace's REST API
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DSpaceRESTv2Service {
|
export class DSpaceRESTv2Service {
|
||||||
|
|
||||||
constructor(private http: Http, @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) {
|
constructor(private http: HttpClient) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,14 +21,33 @@ export class DSpaceRESTv2Service {
|
|||||||
*
|
*
|
||||||
* @param absoluteURL
|
* @param absoluteURL
|
||||||
* A URL
|
* A URL
|
||||||
* @param options
|
|
||||||
* A RequestOptionsArgs object, with options for the http call.
|
|
||||||
* @return {Observable<string>}
|
* @return {Observable<string>}
|
||||||
* An Observable<string> containing the response from the server
|
* An Observable<string> containing the response from the server
|
||||||
*/
|
*/
|
||||||
get(absoluteURL: string, options?: RequestOptionsArgs): Observable<DSpaceRESTV2Response> {
|
get(absoluteURL: string): Observable<DSpaceRESTV2Response> {
|
||||||
return this.http.get(absoluteURL, options)
|
return this.http.get(absoluteURL, { observe: 'response' })
|
||||||
.map((res) => ({ payload: res.json(), statusCode: res.statusText }))
|
.map((res: HttpResponse<any>) => ({ payload: res.body, statusCode: res.statusText }))
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('Error: ', err);
|
||||||
|
return Observable.throw(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) => {
|
.catch((err) => {
|
||||||
console.log('Error: ', err);
|
console.log('Error: ', err);
|
||||||
return Observable.throw(err);
|
return Observable.throw(err);
|
||||||
|
69
src/app/core/index/index.actions.ts
Normal file
69
src/app/core/index/index.actions.ts
Normal 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;
|
61
src/app/core/index/index.effects.ts
Normal file
61
src/app/core/index/index.effects.ts
Normal 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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
58
src/app/core/index/index.reducer.spec.ts
Normal file
58
src/app/core/index/index.reducer.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
62
src/app/core/index/index.reducer.ts
Normal file
62
src/app/core/index/index.reducer.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
@@ -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;
|
|
@@ -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) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -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;
|
|
||||||
}
|
|
@@ -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
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
30
src/app/core/shared/config/config-object-factory.ts
Normal file
30
src/app/core/shared/config/config-object-factory.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
import { GenericConstructor } from '../../shared/generic-constructor';
|
||||||
|
|
||||||
|
import { SubmissionSectionModel } from './config-submission-section.model';
|
||||||
|
import { SubmissionFormsModel } from './config-submission-forms.model';
|
||||||
|
import { SubmissionDefinitionsModel } from './config-submission-definitions.model';
|
||||||
|
import { ConfigType } from './config-type';
|
||||||
|
import { ConfigObject } from './config.model';
|
||||||
|
|
||||||
|
export class ConfigObjectFactory {
|
||||||
|
public static getConstructor(type): GenericConstructor<ConfigObject> {
|
||||||
|
switch (type) {
|
||||||
|
case ConfigType.SubmissionDefinition:
|
||||||
|
case ConfigType.SubmissionDefinitions: {
|
||||||
|
return SubmissionDefinitionsModel
|
||||||
|
}
|
||||||
|
case ConfigType.SubmissionForm:
|
||||||
|
case ConfigType.SubmissionForms: {
|
||||||
|
return SubmissionFormsModel
|
||||||
|
}
|
||||||
|
case ConfigType.SubmissionSection:
|
||||||
|
case ConfigType.SubmissionSections: {
|
||||||
|
return SubmissionSectionModel
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,14 @@
|
|||||||
|
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
|
||||||
|
import { ConfigObject } from './config.model';
|
||||||
|
import { SubmissionSectionModel } from './config-submission-section.model';
|
||||||
|
|
||||||
|
@inheritSerialization(ConfigObject)
|
||||||
|
export class SubmissionDefinitionsModel extends ConfigObject {
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
isDefault: boolean;
|
||||||
|
|
||||||
|
@autoserializeAs(SubmissionSectionModel)
|
||||||
|
sections: SubmissionSectionModel[];
|
||||||
|
|
||||||
|
}
|
10
src/app/core/shared/config/config-submission-forms.model.ts
Normal file
10
src/app/core/shared/config/config-submission-forms.model.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
|
||||||
|
import { ConfigObject } from './config.model';
|
||||||
|
|
||||||
|
@inheritSerialization(ConfigObject)
|
||||||
|
export class SubmissionFormsModel extends ConfigObject {
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
fields: any[];
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,22 @@
|
|||||||
|
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
|
||||||
|
import { ConfigObject } from './config.model';
|
||||||
|
|
||||||
|
@inheritSerialization(ConfigObject)
|
||||||
|
export class SubmissionSectionModel extends ConfigObject {
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
header: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
mandatory: boolean;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
sectionType: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
visibility: {
|
||||||
|
main: any,
|
||||||
|
other: any
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
14
src/app/core/shared/config/config-type.ts
Normal file
14
src/app/core/shared/config/config-type.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* TODO replace with actual string enum after upgrade to TypeScript 2.4:
|
||||||
|
* https://github.com/Microsoft/TypeScript/pull/15486
|
||||||
|
*/
|
||||||
|
import { ResourceType } from '../resource-type';
|
||||||
|
|
||||||
|
export enum ConfigType {
|
||||||
|
SubmissionDefinitions = 'submissiondefinitions',
|
||||||
|
SubmissionDefinition = 'submissiondefinition',
|
||||||
|
SubmissionForm = 'submissionform',
|
||||||
|
SubmissionForms = 'submissionforms',
|
||||||
|
SubmissionSections = 'submissionsections',
|
||||||
|
SubmissionSection = 'submissionsection'
|
||||||
|
}
|
21
src/app/core/shared/config/config.model.ts
Normal file
21
src/app/core/shared/config/config.model.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { autoserialize, autoserializeAs } from 'cerialize';
|
||||||
|
|
||||||
|
export abstract class ConfigObject {
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
public name: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
public type: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
public _links: {
|
||||||
|
[name: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The link to the rest endpoint where this config object can be found
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
self: string;
|
||||||
|
}
|
@@ -3,7 +3,7 @@ import { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
|||||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { ResourceType } from './resource-type';
|
import { ResourceType } from './resource-type';
|
||||||
import { ListableObject } from '../../object-list/listable-object/listable-object.model';
|
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@@ -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];
|
|
||||||
|
function getSubState(state: any, path: string[]) {
|
||||||
|
const current = path[0];
|
||||||
|
const remainingPath = path.slice(1);
|
||||||
|
const subState = state[current];
|
||||||
|
if (hasNoValue(subState) || isEmpty(remainingPath)) {
|
||||||
|
return subState;
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return getSubState(subState, remainingPath);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
9
src/app/core/shared/uuid.service.ts
Normal file
9
src/app/core/shared/uuid.service.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import * as uuidv4 from 'uuid/v4';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UUIDService {
|
||||||
|
generate(): string {
|
||||||
|
return uuidv4();
|
||||||
|
}
|
||||||
|
}
|
@@ -10,7 +10,6 @@ import { HeaderComponent } from './header.component';
|
|||||||
import { HeaderState } from './header.reducer';
|
import { HeaderState } from './header.reducer';
|
||||||
import { HeaderToggleAction } from './header.actions';
|
import { HeaderToggleAction } from './header.actions';
|
||||||
|
|
||||||
|
|
||||||
let comp: HeaderComponent;
|
let comp: HeaderComponent;
|
||||||
let fixture: ComponentFixture<HeaderComponent>;
|
let fixture: ComponentFixture<HeaderComponent>;
|
||||||
let store: Store<HeaderState>;
|
let store: Store<HeaderState>;
|
||||||
|
@@ -1 +0,0 @@
|
|||||||
@import '../../../styles/variables.scss';
|
|
@@ -1,14 +0,0 @@
|
|||||||
import { Component, Inject } from '@angular/core';
|
|
||||||
|
|
||||||
import { Collection } from '../../core/shared/collection.model';
|
|
||||||
import { ObjectListElementComponent } from '../object-list-element/object-list-element.component';
|
|
||||||
import { listElementFor } from '../list-element-decorator';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-collection-list-element',
|
|
||||||
styleUrls: ['./collection-list-element.component.scss'],
|
|
||||||
templateUrl: './collection-list-element.component.html'
|
|
||||||
})
|
|
||||||
|
|
||||||
@listElementFor(Collection)
|
|
||||||
export class CollectionListElementComponent extends ObjectListElementComponent<Collection> {}
|
|
@@ -1 +0,0 @@
|
|||||||
@import '../../../styles/variables.scss';
|
|
@@ -1,14 +0,0 @@
|
|||||||
import { Component, Input, Inject } from '@angular/core';
|
|
||||||
|
|
||||||
import { Community } from '../../core/shared/community.model';
|
|
||||||
import { ObjectListElementComponent } from '../object-list-element/object-list-element.component';
|
|
||||||
import { listElementFor } from '../list-element-decorator';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-community-list-element',
|
|
||||||
styleUrls: ['./community-list-element.component.scss'],
|
|
||||||
templateUrl: './community-list-element.component.html'
|
|
||||||
})
|
|
||||||
|
|
||||||
@listElementFor(Community)
|
|
||||||
export class CommunityListElementComponent extends ObjectListElementComponent<Community> {}
|
|
@@ -1 +0,0 @@
|
|||||||
@import '../../../styles/variables.scss';
|
|
@@ -1,16 +0,0 @@
|
|||||||
import { Component, Input, Inject } from '@angular/core';
|
|
||||||
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
|
||||||
import { ObjectListElementComponent } from '../object-list-element/object-list-element.component';
|
|
||||||
import { listElementFor } from '../list-element-decorator';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-item-list-element',
|
|
||||||
styleUrls: ['./item-list-element.component.scss'],
|
|
||||||
templateUrl: './item-list-element.component.html'
|
|
||||||
})
|
|
||||||
|
|
||||||
@listElementFor(Item)
|
|
||||||
export class ItemListElementComponent extends ObjectListElementComponent<Item> {
|
|
||||||
private lines = 3;
|
|
||||||
}
|
|
@@ -1,16 +0,0 @@
|
|||||||
import { ListableObject } from './listable-object/listable-object.model';
|
|
||||||
import { GenericConstructor } from '../core/shared/generic-constructor';
|
|
||||||
|
|
||||||
const listElementMap = new Map();
|
|
||||||
export function listElementFor(listable: GenericConstructor<ListableObject>) {
|
|
||||||
return function decorator(objectElement: any) {
|
|
||||||
if (!objectElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
listElementMap.set(listable, objectElement);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getListElementFor(listable: GenericConstructor<ListableObject>) {
|
|
||||||
return listElementMap.get(listable);
|
|
||||||
}
|
|
@@ -1,6 +0,0 @@
|
|||||||
@import '../../../styles/variables.scss';
|
|
||||||
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: $spacer;
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
import { Component, Inject } from '@angular/core';
|
|
||||||
import { ListableObject } from '../listable-object/listable-object.model';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-object-list-element',
|
|
||||||
styleUrls: ['./object-list-element.component.scss'],
|
|
||||||
templateUrl: './object-list-element.component.html'
|
|
||||||
})
|
|
||||||
export class ObjectListElementComponent <T extends ListableObject> {
|
|
||||||
object: T;
|
|
||||||
public constructor(@Inject('objectElementProvider') public listable: ListableObject) {
|
|
||||||
this.object = listable as T;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1 +0,0 @@
|
|||||||
@import '../../styles/variables.scss';
|
|
@@ -1 +0,0 @@
|
|||||||
@import '../../../../styles/variables.scss';
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user