mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'master' into w2p-60168_Alphabetic-browse-widget
Conflicts: src/app/core/browse/browse.service.spec.ts src/app/core/browse/browse.service.ts src/app/core/data/dso-response-parsing.service.ts src/app/shared/shared.module.ts
This commit is contained in:
@@ -18,9 +18,16 @@ module.exports = {
|
|||||||
// Caching settings
|
// Caching settings
|
||||||
cache: {
|
cache: {
|
||||||
// NOTE: how long should objects be cached for by default
|
// NOTE: how long should objects be cached for by default
|
||||||
msToLive: 15 * 60 * 1000, // 15 minutes
|
msToLive: {
|
||||||
|
default: 15 * 60 * 1000, // 15 minutes
|
||||||
|
},
|
||||||
// msToLive: 1000, // 15 minutes
|
// msToLive: 1000, // 15 minutes
|
||||||
control: 'max-age=60' // revalidate browser
|
control: 'max-age=60', // revalidate browser
|
||||||
|
autoSync: {
|
||||||
|
defaultTime: 0,
|
||||||
|
maxBufferSize: 100,
|
||||||
|
timePerMethod: {'PATCH': 3} //time in seconds
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// Form settings
|
// Form settings
|
||||||
form: {
|
form: {
|
||||||
|
@@ -96,6 +96,7 @@
|
|||||||
"core-js": "^2.5.7",
|
"core-js": "^2.5.7",
|
||||||
"express": "4.16.2",
|
"express": "4.16.2",
|
||||||
"express-session": "1.15.6",
|
"express-session": "1.15.6",
|
||||||
|
"fast-json-patch": "^2.0.7",
|
||||||
"font-awesome": "4.7.0",
|
"font-awesome": "4.7.0",
|
||||||
"fork-ts-checker-webpack-plugin": "^0.4.10",
|
"fork-ts-checker-webpack-plugin": "^0.4.10",
|
||||||
"http-server": "0.11.1",
|
"http-server": "0.11.1",
|
||||||
|
@@ -13,6 +13,38 @@
|
|||||||
"head": "Recent Submissions"
|
"head": "Recent Submissions"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"title": "Name",
|
||||||
|
"description": "Introductory text (HTML)",
|
||||||
|
"abstract": "Short Description",
|
||||||
|
"rights": "Copyright text (HTML)",
|
||||||
|
"tableofcontents": "News (HTML)",
|
||||||
|
"license": "License",
|
||||||
|
"provenance": "Provenance",
|
||||||
|
"errors": {
|
||||||
|
"title": {
|
||||||
|
"required": "Please enter a collection name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"head": "Edit Collection",
|
||||||
|
"delete": "Delete this collection"
|
||||||
|
},
|
||||||
|
"create": {
|
||||||
|
"head": "Create a Collection",
|
||||||
|
"sub-head": "Create a Collection for Community {{ parent }}"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"head": "Delete Collection",
|
||||||
|
"text": "Are you sure you want to delete collection \"{{ dso }}\"",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"notification": {
|
||||||
|
"success": "Successfully deleted collection",
|
||||||
|
"fail": "Collection could not be deleted"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"community": {
|
"community": {
|
||||||
@@ -25,6 +57,36 @@
|
|||||||
},
|
},
|
||||||
"sub-community-list": {
|
"sub-community-list": {
|
||||||
"head": "Communities of this Community"
|
"head": "Communities of this Community"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"title": "Name",
|
||||||
|
"description": "Introductory text (HTML)",
|
||||||
|
"abstract": "Short Description",
|
||||||
|
"rights": "Copyright text (HTML)",
|
||||||
|
"tableofcontents": "News (HTML)",
|
||||||
|
"errors": {
|
||||||
|
"title": {
|
||||||
|
"required": "Please enter a community name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"head": "Edit Community",
|
||||||
|
"delete": "Delete this community"
|
||||||
|
},
|
||||||
|
"create": {
|
||||||
|
"head": "Create a Community",
|
||||||
|
"sub-head": "Create a Sub-Community for Community {{ parent }}"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"head": "Delete Community",
|
||||||
|
"text": "Are you sure you want to delete community \"{{ dso }}\"",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"notification": {
|
||||||
|
"success": "Successfully deleted community",
|
||||||
|
"fail": "Community could not be deleted"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"item": {
|
"item": {
|
||||||
|
@@ -0,0 +1,71 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import {
|
||||||
|
DynamicInputModel,
|
||||||
|
DynamicTextAreaModel
|
||||||
|
} from '@ng-dynamic-forms/core';
|
||||||
|
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
|
||||||
|
import { ResourceType } from '../../core/shared/resource-type';
|
||||||
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
|
import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form used for creating and editing collections
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-collection-form',
|
||||||
|
styleUrls: ['../../shared/comcol-forms/comcol-form/comcol-form.component.scss'],
|
||||||
|
templateUrl: '../../shared/comcol-forms/comcol-form/comcol-form.component.html'
|
||||||
|
})
|
||||||
|
export class CollectionFormComponent extends ComColFormComponent<Collection> {
|
||||||
|
/**
|
||||||
|
* @type {Collection} A new collection when a collection is being created, an existing Input collection when a collection is being edited
|
||||||
|
*/
|
||||||
|
@Input() dso: Collection = new Collection();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {ResourceType.Collection} This is a collection-type form
|
||||||
|
*/
|
||||||
|
protected type = ResourceType.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dynamic form fields used for creating/editing a collection
|
||||||
|
* @type {(DynamicInputModel | DynamicTextAreaModel)[]}
|
||||||
|
*/
|
||||||
|
formModel: DynamicFormControlModel[] = [
|
||||||
|
new DynamicInputModel({
|
||||||
|
id: 'title',
|
||||||
|
name: 'dc.title',
|
||||||
|
required: true,
|
||||||
|
validators: {
|
||||||
|
required: null
|
||||||
|
},
|
||||||
|
errorMessages: {
|
||||||
|
required: 'Please enter a name for this title'
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'description',
|
||||||
|
name: 'dc.description',
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'abstract',
|
||||||
|
name: 'dc.description.abstract',
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'rights',
|
||||||
|
name: 'dc.rights',
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'tableofcontents',
|
||||||
|
name: 'dc.description.tableofcontents',
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'license',
|
||||||
|
name: 'dc.rights.license',
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'provenance',
|
||||||
|
name: 'dc.description.provenance',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
@@ -3,10 +3,38 @@ import { RouterModule } from '@angular/router';
|
|||||||
|
|
||||||
import { CollectionPageComponent } from './collection-page.component';
|
import { CollectionPageComponent } from './collection-page.component';
|
||||||
import { CollectionPageResolver } from './collection-page.resolver';
|
import { CollectionPageResolver } from './collection-page.resolver';
|
||||||
|
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
|
||||||
|
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||||
|
import { EditCollectionPageComponent } from './edit-collection-page/edit-collection-page.component';
|
||||||
|
import { CreateCollectionPageGuard } from './create-collection-page/create-collection-page.guard';
|
||||||
|
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
path: 'create',
|
||||||
|
component: CreateCollectionPageComponent,
|
||||||
|
canActivate: [AuthenticatedGuard, CreateCollectionPageGuard]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id/edit',
|
||||||
|
pathMatch: 'full',
|
||||||
|
component: EditCollectionPageComponent,
|
||||||
|
canActivate: [AuthenticatedGuard],
|
||||||
|
resolve: {
|
||||||
|
dso: CollectionPageResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id/delete',
|
||||||
|
pathMatch: 'full',
|
||||||
|
component: DeleteCollectionPageComponent,
|
||||||
|
canActivate: [AuthenticatedGuard],
|
||||||
|
resolve: {
|
||||||
|
dso: CollectionPageResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
component: CollectionPageComponent,
|
component: CollectionPageComponent,
|
||||||
@@ -19,6 +47,7 @@ import { CollectionPageResolver } from './collection-page.resolver';
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CollectionPageResolver,
|
CollectionPageResolver,
|
||||||
|
CreateCollectionPageGuard
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CollectionPageRoutingModule {
|
export class CollectionPageRoutingModule {
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
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, Subscription } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||||
import { CollectionDataService } from '../core/data/collection-data.service';
|
import { CollectionDataService } from '../core/data/collection-data.service';
|
||||||
import { PaginatedList } from '../core/data/paginated-list';
|
import { PaginatedList } from '../core/data/paginated-list';
|
||||||
@@ -15,7 +15,7 @@ import { Item } from '../core/shared/item.model';
|
|||||||
import { fadeIn, fadeInOut } from '../shared/animations/fade';
|
import { fadeIn, fadeInOut } from '../shared/animations/fade';
|
||||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||||
import { filter, flatMap, map } from 'rxjs/operators';
|
import { filter, flatMap, map, tap } from 'rxjs/operators';
|
||||||
import { SearchService } from '../+search-page/search-service/search.service';
|
import { SearchService } from '../+search-page/search-service/search.service';
|
||||||
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
||||||
import { toDSpaceObjectListRD } from '../core/shared/operators';
|
import { toDSpaceObjectListRD } from '../core/shared/operators';
|
||||||
@@ -55,7 +55,8 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.collectionRD$ = this.route.data.pipe(
|
this.collectionRD$ = this.route.data.pipe(
|
||||||
map((data) => data.collection)
|
map((data) => data.collection),
|
||||||
|
tap((data) => this.collectionId = data.payload.id)
|
||||||
);
|
);
|
||||||
this.logoRD$ = this.collectionRD$.pipe(
|
this.logoRD$ = this.collectionRD$.pipe(
|
||||||
map((rd: RemoteData<Collection>) => rd.payload),
|
map((rd: RemoteData<Collection>) => rd.payload),
|
||||||
@@ -75,8 +76,8 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
|||||||
pagination: pagination,
|
pagination: pagination,
|
||||||
sort: this.sortConfig
|
sort: this.sortConfig
|
||||||
});
|
});
|
||||||
})
|
}));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePage(searchOptions) {
|
updatePage(searchOptions) {
|
||||||
|
@@ -5,7 +5,11 @@ import { SharedModule } from '../shared/shared.module';
|
|||||||
|
|
||||||
import { CollectionPageComponent } from './collection-page.component';
|
import { CollectionPageComponent } from './collection-page.component';
|
||||||
import { CollectionPageRoutingModule } from './collection-page-routing.module';
|
import { CollectionPageRoutingModule } from './collection-page-routing.module';
|
||||||
|
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
|
||||||
|
import { CollectionFormComponent } from './collection-form/collection-form.component';
|
||||||
import { SearchPageModule } from '../+search-page/search-page.module';
|
import { SearchPageModule } from '../+search-page/search-page.module';
|
||||||
|
import { EditCollectionPageComponent } from './edit-collection-page/edit-collection-page.component';
|
||||||
|
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -16,6 +20,10 @@ import { SearchPageModule } from '../+search-page/search-page.module';
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
CollectionPageComponent,
|
CollectionPageComponent,
|
||||||
|
CreateCollectionPageComponent,
|
||||||
|
EditCollectionPageComponent,
|
||||||
|
DeleteCollectionPageComponent,
|
||||||
|
CollectionFormComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CollectionPageModule {
|
export class CollectionPageModule {
|
||||||
|
@@ -0,0 +1,8 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 pb-4">
|
||||||
|
<h2 id="sub-header" class="border-bottom pb-2">{{'collection.create.sub-head' | translate:{ parent: (parentRD$| async)?.payload.name } }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ds-collection-form (submitForm)="onSubmit($event)"></ds-collection-form>
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,46 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { RouteService } from '../../shared/services/route.service';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
import { CreateCollectionPageComponent } from './create-collection-page.component';
|
||||||
|
|
||||||
|
describe('CreateCollectionPageComponent', () => {
|
||||||
|
let comp: CreateCollectionPageComponent;
|
||||||
|
let fixture: ComponentFixture<CreateCollectionPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||||
|
declarations: [CreateCollectionPageComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: CollectionDataService, useValue: {} },
|
||||||
|
{
|
||||||
|
provide: CommunityDataService,
|
||||||
|
useValue: { findById: () => observableOf({ payload: { name: 'test' } }) }
|
||||||
|
},
|
||||||
|
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } },
|
||||||
|
{ provide: Router, useValue: {} },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CreateCollectionPageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('frontendURL', () => {
|
||||||
|
it('should have the right frontendURL set', () => {
|
||||||
|
expect((comp as any).frontendURL).toEqual('/collections/');
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,29 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
import { RouteService } from '../../shared/services/route.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component';
|
||||||
|
import { NormalizedCollection } from '../../core/cache/models/normalized-collection.model';
|
||||||
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that represents the page where a user can create a new Collection
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-create-collection',
|
||||||
|
styleUrls: ['./create-collection-page.component.scss'],
|
||||||
|
templateUrl: './create-collection-page.component.html'
|
||||||
|
})
|
||||||
|
export class CreateCollectionPageComponent extends CreateComColPageComponent<Collection, NormalizedCollection> {
|
||||||
|
protected frontendURL = '/collections/';
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
protected communityDataService: CommunityDataService,
|
||||||
|
protected collectionDataService: CollectionDataService,
|
||||||
|
protected routeService: RouteService,
|
||||||
|
protected router: Router
|
||||||
|
) {
|
||||||
|
super(collectionDataService, communityDataService, routeService, router);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,67 @@
|
|||||||
|
import { CreateCollectionPageGuard } from './create-collection-page.guard';
|
||||||
|
import { MockRouter } from '../../shared/mocks/mock-router';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
|
||||||
|
describe('CreateCollectionPageGuard', () => {
|
||||||
|
describe('canActivate', () => {
|
||||||
|
let guard: CreateCollectionPageGuard;
|
||||||
|
let router;
|
||||||
|
let communityDataServiceStub: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
communityDataServiceStub = {
|
||||||
|
findById: (id: string) => {
|
||||||
|
if (id === 'valid-id') {
|
||||||
|
return observableOf(new RemoteData(false, false, true, null, new Community()));
|
||||||
|
} else if (id === 'invalid-id') {
|
||||||
|
return observableOf(new RemoteData(false, false, true, null, undefined));
|
||||||
|
} else if (id === 'error-id') {
|
||||||
|
return observableOf(new RemoteData(false, false, false, null, new Community()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
router = new MockRouter();
|
||||||
|
|
||||||
|
guard = new CreateCollectionPageGuard(router, communityDataServiceStub);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when the parent ID resolves to a community', () => {
|
||||||
|
guard.canActivate({ queryParams: { parent: 'valid-id' } } as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(canActivate) =>
|
||||||
|
expect(canActivate).toEqual(true)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when no parent ID has been provided', () => {
|
||||||
|
guard.canActivate({ queryParams: { } } as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(canActivate) =>
|
||||||
|
expect(canActivate).toEqual(false)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the parent ID does not resolve to a community', () => {
|
||||||
|
guard.canActivate({ queryParams: { parent: 'invalid-id' } } as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(canActivate) =>
|
||||||
|
expect(canActivate).toEqual(false)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the parent ID resolves to an error response', () => {
|
||||||
|
guard.canActivate({ queryParams: { parent: 'error-id' } } as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(canActivate) =>
|
||||||
|
expect(canActivate).toEqual(false)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,46 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
|
||||||
|
import { hasNoValue, hasValue } from '../../shared/empty.util';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { getFinishedRemoteData } from '../../core/shared/operators';
|
||||||
|
import { map, tap } from 'rxjs/operators';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent creation of a collection without a parent community provided
|
||||||
|
* @class CreateCollectionPageGuard
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CreateCollectionPageGuard implements CanActivate {
|
||||||
|
public constructor(private router: Router, private communityService: CommunityDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when either a parent ID query parameter has been provided and the parent ID resolves to a valid parent community
|
||||||
|
* Reroutes to a 404 page when the page cannot be activated
|
||||||
|
* @method canActivate
|
||||||
|
*/
|
||||||
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||||
|
const parentID = route.queryParams.parent;
|
||||||
|
if (hasNoValue(parentID)) {
|
||||||
|
this.router.navigate(['/404']);
|
||||||
|
return observableOf(false);
|
||||||
|
}
|
||||||
|
const parent: Observable<RemoteData<Community>> = this.communityService.findById(parentID)
|
||||||
|
.pipe(
|
||||||
|
getFinishedRemoteData(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return parent.pipe(
|
||||||
|
map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
|
||||||
|
tap((isValid: boolean) => {
|
||||||
|
if (!isValid) {
|
||||||
|
this.router.navigate(['/404']);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,19 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<ng-container *ngVar="(dsoRD$ | async)?.payload as dso">
|
||||||
|
<div class="col-12 pb-4">
|
||||||
|
<h2 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate
|
||||||
|
}}</h2>
|
||||||
|
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p>
|
||||||
|
<button class="btn btn-primary mr-2" (click)="onConfirm(dso)">
|
||||||
|
{{'community.delete.confirm' |
|
||||||
|
translate}}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" (click)="onCancel(dso)">{{'community.delete.cancel' | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,41 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { DeleteCollectionPageComponent } from './delete-collection-page.component';
|
||||||
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
|
||||||
|
describe('DeleteCollectionPageComponent', () => {
|
||||||
|
let comp: DeleteCollectionPageComponent;
|
||||||
|
let fixture: ComponentFixture<DeleteCollectionPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||||
|
declarations: [DeleteCollectionPageComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: CollectionDataService, useValue: {} },
|
||||||
|
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
||||||
|
{ provide: NotificationsService, useValue: {} },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DeleteCollectionPageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('frontendURL', () => {
|
||||||
|
it('should have the right frontendURL set', () => {
|
||||||
|
expect((comp as any).frontendURL).toEqual('/collections/');
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,33 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model';
|
||||||
|
import { DeleteComColPageComponent } from '../../shared/comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
import { NormalizedCollection } from '../../core/cache/models/normalized-collection.model';
|
||||||
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that represents the page where a user can delete an existing Collection
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-delete-collection',
|
||||||
|
styleUrls: ['./delete-collection-page.component.scss'],
|
||||||
|
templateUrl: './delete-collection-page.component.html'
|
||||||
|
})
|
||||||
|
export class DeleteCollectionPageComponent extends DeleteComColPageComponent<Collection, NormalizedCollection> {
|
||||||
|
protected frontendURL = '/collections/';
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
protected dsoDataService: CollectionDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
protected notifications: NotificationsService,
|
||||||
|
protected translate: TranslateService
|
||||||
|
) {
|
||||||
|
super(dsoDataService, router, route, notifications, translate);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,11 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 pb-4">
|
||||||
|
<h2 id="header" class="border-bottom pb-2">{{ 'collection.edit.head' | translate }}</h2>
|
||||||
|
<ds-collection-form (submitForm)="onSubmit($event)" [dso]="(dsoRD$ | async)?.payload"></ds-collection-form>
|
||||||
|
<a class="btn btn-danger"
|
||||||
|
[routerLink]="'/collections/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'collection.edit.delete'
|
||||||
|
| translate}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,39 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { EditCollectionPageComponent } from './edit-collection-page.component';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
describe('EditCollectionPageComponent', () => {
|
||||||
|
let comp: EditCollectionPageComponent;
|
||||||
|
let fixture: ComponentFixture<EditCollectionPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||||
|
declarations: [EditCollectionPageComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: CollectionDataService, useValue: {} },
|
||||||
|
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(EditCollectionPageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('frontendURL', () => {
|
||||||
|
it('should have the right frontendURL set', () => {
|
||||||
|
expect((comp as any).frontendURL).toEqual('/collections/');
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,26 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
||||||
|
import { NormalizedCollection } from '../../core/cache/models/normalized-collection.model';
|
||||||
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that represents the page where a user can edit an existing Collection
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-edit-collection',
|
||||||
|
styleUrls: ['./edit-collection-page.component.scss'],
|
||||||
|
templateUrl: './edit-collection-page.component.html'
|
||||||
|
})
|
||||||
|
export class EditCollectionPageComponent extends EditComColPageComponent<Collection, NormalizedCollection> {
|
||||||
|
protected frontendURL = '/collections/';
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
protected collectionDataService: CollectionDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected route: ActivatedRoute
|
||||||
|
) {
|
||||||
|
super(collectionDataService, router, route);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,60 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core';
|
||||||
|
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { ResourceType } from '../../core/shared/resource-type';
|
||||||
|
import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form used for creating and editing communities
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-community-form',
|
||||||
|
styleUrls: ['../../shared/comcol-forms/comcol-form/comcol-form.component.scss'],
|
||||||
|
templateUrl: '../../shared/comcol-forms/comcol-form/comcol-form.component.html'
|
||||||
|
})
|
||||||
|
export class CommunityFormComponent extends ComColFormComponent<Community> {
|
||||||
|
/**
|
||||||
|
* @type {Community} A new community when a community is being created, an existing Input community when a community is being edited
|
||||||
|
*/
|
||||||
|
@Input() dso: Community = new Community();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {ResourceType.Community} This is a community-type form
|
||||||
|
*/
|
||||||
|
protected type = ResourceType.Community;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dynamic form fields used for creating/editing a community
|
||||||
|
* @type {(DynamicInputModel | DynamicTextAreaModel)[]}
|
||||||
|
*/
|
||||||
|
formModel: DynamicFormControlModel[] = [
|
||||||
|
new DynamicInputModel({
|
||||||
|
id: 'title',
|
||||||
|
name: 'dc.title',
|
||||||
|
required: true,
|
||||||
|
validators: {
|
||||||
|
required: null
|
||||||
|
},
|
||||||
|
errorMessages: {
|
||||||
|
required: 'Please enter a name for this title'
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'description',
|
||||||
|
name: 'dc.description',
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'abstract',
|
||||||
|
name: 'dc.description.abstract',
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'rights',
|
||||||
|
name: 'dc.rights',
|
||||||
|
}),
|
||||||
|
new DynamicTextAreaModel({
|
||||||
|
id: 'tableofcontents',
|
||||||
|
name: 'dc.description.tableofcontents',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
@@ -3,10 +3,38 @@ import { RouterModule } from '@angular/router';
|
|||||||
|
|
||||||
import { CommunityPageComponent } from './community-page.component';
|
import { CommunityPageComponent } from './community-page.component';
|
||||||
import { CommunityPageResolver } from './community-page.resolver';
|
import { CommunityPageResolver } from './community-page.resolver';
|
||||||
|
import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component';
|
||||||
|
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||||
|
import { EditCommunityPageComponent } from './edit-community-page/edit-community-page.component';
|
||||||
|
import { CreateCommunityPageGuard } from './create-community-page/create-community-page.guard';
|
||||||
|
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
path: 'create',
|
||||||
|
component: CreateCommunityPageComponent,
|
||||||
|
canActivate: [AuthenticatedGuard, CreateCommunityPageGuard]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id/edit',
|
||||||
|
pathMatch: 'full',
|
||||||
|
component: EditCommunityPageComponent,
|
||||||
|
canActivate: [AuthenticatedGuard],
|
||||||
|
resolve: {
|
||||||
|
dso: CommunityPageResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id/delete',
|
||||||
|
pathMatch: 'full',
|
||||||
|
component: DeleteCommunityPageComponent,
|
||||||
|
canActivate: [AuthenticatedGuard],
|
||||||
|
resolve: {
|
||||||
|
dso: CommunityPageResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
component: CommunityPageComponent,
|
component: CommunityPageComponent,
|
||||||
@@ -19,6 +47,7 @@ import { CommunityPageResolver } from './community-page.resolver';
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CommunityPageResolver,
|
CommunityPageResolver,
|
||||||
|
CreateCommunityPageGuard
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CommunityPageRoutingModule {
|
export class CommunityPageRoutingModule {
|
||||||
|
@@ -30,6 +30,7 @@
|
|||||||
<ds-community-page-sub-collection-list [community]="communityPayload"></ds-community-page-sub-collection-list>
|
<ds-community-page-sub-collection-list [community]="communityPayload"></ds-community-page-sub-collection-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ds-error *ngIf="communityRD?.hasFailed" message="{{'error.community' | translate}}"></ds-error>
|
<ds-error *ngIf="communityRD?.hasFailed" message="{{'error.community' | translate}}"></ds-error>
|
||||||
<ds-loading *ngIf="communityRD?.isLoading" message="{{'loading.community' | translate}}"></ds-loading>
|
<ds-loading *ngIf="communityRD?.isLoading" message="{{'loading.community' | translate}}"></ds-loading>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import {mergeMap, filter, map} from 'rxjs/operators';
|
import { mergeMap, filter, map } from 'rxjs/operators';
|
||||||
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';
|
||||||
|
|
||||||
@@ -45,5 +45,4 @@ export class CommunityPageComponent implements OnInit, OnDestroy {
|
|||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,10 @@ import { CommunityPageComponent } from './community-page.component';
|
|||||||
import { CommunityPageSubCollectionListComponent } from './sub-collection-list/community-page-sub-collection-list.component';
|
import { CommunityPageSubCollectionListComponent } from './sub-collection-list/community-page-sub-collection-list.component';
|
||||||
import { CommunityPageRoutingModule } from './community-page-routing.module';
|
import { CommunityPageRoutingModule } from './community-page-routing.module';
|
||||||
import {CommunityPageSubCommunityListComponent} from './sub-community-list/community-page-sub-community-list.component';
|
import {CommunityPageSubCommunityListComponent} from './sub-community-list/community-page-sub-community-list.component';
|
||||||
|
import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component';
|
||||||
|
import { CommunityFormComponent } from './community-form/community-form.component';
|
||||||
|
import { EditCommunityPageComponent } from './edit-community-page/edit-community-page.component';
|
||||||
|
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -18,8 +22,13 @@ import {CommunityPageSubCommunityListComponent} from './sub-community-list/commu
|
|||||||
CommunityPageComponent,
|
CommunityPageComponent,
|
||||||
CommunityPageSubCollectionListComponent,
|
CommunityPageSubCollectionListComponent,
|
||||||
CommunityPageSubCommunityListComponent,
|
CommunityPageSubCommunityListComponent,
|
||||||
|
CreateCommunityPageComponent,
|
||||||
|
EditCommunityPageComponent,
|
||||||
|
DeleteCommunityPageComponent,
|
||||||
|
CommunityFormComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
export class CommunityPageModule {
|
export class CommunityPageModule {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,11 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 pb-4">
|
||||||
|
<ng-container *ngVar="(parentRD$ | async)?.payload as parent">
|
||||||
|
<h2 *ngIf="!parent" id="header" class="border-bottom pb-2">{{ 'community.create.head' | translate }}</h2>
|
||||||
|
<h2 *ngIf="parent" id="sub-header" class="border-bottom pb-2">{{ 'community.create.sub-head' | translate:{ parent: parent.name } }}</h2>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ds-community-form (submitForm)="onSubmit($event)"></ds-community-form>
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,42 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { RouteService } from '../../shared/services/route.service';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
import { CreateCommunityPageComponent } from './create-community-page.component';
|
||||||
|
|
||||||
|
describe('CreateCommunityPageComponent', () => {
|
||||||
|
let comp: CreateCommunityPageComponent;
|
||||||
|
let fixture: ComponentFixture<CreateCommunityPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||||
|
declarations: [CreateCommunityPageComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: CommunityDataService, useValue: { findById: () => observableOf({}) } },
|
||||||
|
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } },
|
||||||
|
{ provide: Router, useValue: {} },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CreateCommunityPageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('frontendURL', () => {
|
||||||
|
it('should have the right frontendURL set', () => {
|
||||||
|
expect((comp as any).frontendURL).toEqual('/communities/');
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,27 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
import { RouteService } from '../../shared/services/route.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component';
|
||||||
|
import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that represents the page where a user can create a new Community
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-create-community',
|
||||||
|
styleUrls: ['./create-community-page.component.scss'],
|
||||||
|
templateUrl: './create-community-page.component.html'
|
||||||
|
})
|
||||||
|
export class CreateCommunityPageComponent extends CreateComColPageComponent<Community, NormalizedCommunity> {
|
||||||
|
protected frontendURL = '/communities/';
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
protected communityDataService: CommunityDataService,
|
||||||
|
protected routeService: RouteService,
|
||||||
|
protected router: Router
|
||||||
|
) {
|
||||||
|
super(communityDataService, communityDataService, routeService, router);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,67 @@
|
|||||||
|
import { CreateCommunityPageGuard } from './create-community-page.guard';
|
||||||
|
import { MockRouter } from '../../shared/mocks/mock-router';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { first } from 'rxjs/operators';
|
||||||
|
|
||||||
|
describe('CreateCommunityPageGuard', () => {
|
||||||
|
describe('canActivate', () => {
|
||||||
|
let guard: CreateCommunityPageGuard;
|
||||||
|
let router;
|
||||||
|
let communityDataServiceStub: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
communityDataServiceStub = {
|
||||||
|
findById: (id: string) => {
|
||||||
|
if (id === 'valid-id') {
|
||||||
|
return observableOf(new RemoteData(false, false, true, null, new Community()));
|
||||||
|
} else if (id === 'invalid-id') {
|
||||||
|
return observableOf(new RemoteData(false, false, true, null, undefined));
|
||||||
|
} else if (id === 'error-id') {
|
||||||
|
return observableOf(new RemoteData(false, false, false, null, new Community()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
router = new MockRouter();
|
||||||
|
|
||||||
|
guard = new CreateCommunityPageGuard(router, communityDataServiceStub);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when the parent ID resolves to a community', () => {
|
||||||
|
guard.canActivate({ queryParams: { parent: 'valid-id' } } as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(canActivate) =>
|
||||||
|
expect(canActivate).toEqual(true)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when no parent ID has been provided', () => {
|
||||||
|
guard.canActivate({ queryParams: { } } as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(canActivate) =>
|
||||||
|
expect(canActivate).toEqual(true)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the parent ID does not resolve to a community', () => {
|
||||||
|
guard.canActivate({ queryParams: { parent: 'invalid-id' } } as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(canActivate) =>
|
||||||
|
expect(canActivate).toEqual(false)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the parent ID resolves to an error response', () => {
|
||||||
|
guard.canActivate({ queryParams: { parent: 'error-id' } } as any, undefined)
|
||||||
|
.pipe(first())
|
||||||
|
.subscribe(
|
||||||
|
(canActivate) =>
|
||||||
|
expect(canActivate).toEqual(false)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,46 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
|
||||||
|
import { hasNoValue, hasValue } from '../../shared/empty.util';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { getFinishedRemoteData } from '../../core/shared/operators';
|
||||||
|
import { map, tap } from 'rxjs/operators';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent creation of a community with an invalid parent community provided
|
||||||
|
* @class CreateCommunityPageGuard
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CreateCommunityPageGuard implements CanActivate {
|
||||||
|
public constructor(private router: Router, private communityService: CommunityDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when either NO parent ID query parameter has been provided, or the parent ID resolves to a valid parent community
|
||||||
|
* Reroutes to a 404 page when the page cannot be activated
|
||||||
|
* @method canActivate
|
||||||
|
*/
|
||||||
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||||
|
const parentID = route.queryParams.parent;
|
||||||
|
if (hasNoValue(parentID)) {
|
||||||
|
return observableOf(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent: Observable<RemoteData<Community>> = this.communityService.findById(parentID)
|
||||||
|
.pipe(
|
||||||
|
getFinishedRemoteData(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return parent.pipe(
|
||||||
|
map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
|
||||||
|
tap((isValid: boolean) => {
|
||||||
|
if (!isValid) {
|
||||||
|
this.router.navigate(['/404']);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,19 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<ng-container *ngVar="(dsoRD$ | async)?.payload as dso">
|
||||||
|
<div class="col-12 pb-4">
|
||||||
|
<h2 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate
|
||||||
|
}}</h2>
|
||||||
|
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p>
|
||||||
|
<button class="btn btn-primary mr-2" (click)="onConfirm(dso)">
|
||||||
|
{{'community.delete.confirm' |
|
||||||
|
translate}}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" (click)="onCancel(dso)">{{'community.delete.cancel' | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,42 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { RouteService } from '../../shared/services/route.service';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { DeleteCommunityPageComponent } from './delete-community-page.component';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
|
||||||
|
describe('DeleteCommunityPageComponent', () => {
|
||||||
|
let comp: DeleteCommunityPageComponent;
|
||||||
|
let fixture: ComponentFixture<DeleteCommunityPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||||
|
declarations: [DeleteCommunityPageComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: CommunityDataService, useValue: {} },
|
||||||
|
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
||||||
|
{ provide: NotificationsService, useValue: {} },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DeleteCommunityPageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('frontendURL', () => {
|
||||||
|
it('should have the right frontendURL set', () => {
|
||||||
|
expect((comp as any).frontendURL).toEqual('/communities/');
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,30 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model';
|
||||||
|
import { DeleteComColPageComponent } from '../../shared/comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that represents the page where a user can delete an existing Community
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-delete-community',
|
||||||
|
styleUrls: ['./delete-community-page.component.scss'],
|
||||||
|
templateUrl: './delete-community-page.component.html'
|
||||||
|
})
|
||||||
|
export class DeleteCommunityPageComponent extends DeleteComColPageComponent<Community, NormalizedCommunity> {
|
||||||
|
protected frontendURL = '/communities/';
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
protected dsoDataService: CommunityDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
protected notifications: NotificationsService,
|
||||||
|
protected translate: TranslateService
|
||||||
|
) {
|
||||||
|
super(dsoDataService, router, route, notifications, translate);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,12 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 pb-4">
|
||||||
|
<h2 id="header" class="border-bottom pb-2">{{ 'community.edit.head' | translate }}</h2>
|
||||||
|
<ds-community-form (submitForm)="onSubmit($event)"
|
||||||
|
[dso]="(dsoRD$ | async)?.payload"></ds-community-form>
|
||||||
|
<a class="btn btn-danger"
|
||||||
|
[routerLink]="'/communities/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'community.edit.delete'
|
||||||
|
| translate}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,39 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { EditCommunityPageComponent } from './edit-community-page.component';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
|
||||||
|
describe('EditCommunityPageComponent', () => {
|
||||||
|
let comp: EditCommunityPageComponent;
|
||||||
|
let fixture: ComponentFixture<EditCommunityPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||||
|
declarations: [EditCommunityPageComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: CommunityDataService, useValue: {} },
|
||||||
|
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(EditCommunityPageComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('frontendURL', () => {
|
||||||
|
it('should have the right frontendURL set', () => {
|
||||||
|
expect((comp as any).frontendURL).toEqual('/communities/');
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,26 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Community } from '../../core/shared/community.model';
|
||||||
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model';
|
||||||
|
import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that represents the page where a user can edit an existing Community
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-edit-community',
|
||||||
|
styleUrls: ['./edit-community-page.component.scss'],
|
||||||
|
templateUrl: './edit-community-page.component.html'
|
||||||
|
})
|
||||||
|
export class EditCommunityPageComponent extends EditComColPageComponent<Community, NormalizedCommunity> {
|
||||||
|
protected frontendURL = '/communities/';
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
protected communityDataService: CommunityDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected route: ActivatedRoute
|
||||||
|
) {
|
||||||
|
super(communityDataService, router, route);
|
||||||
|
}
|
||||||
|
}
|
@@ -17,7 +17,6 @@ describe('SubCommunityList Component', () => {
|
|||||||
let fixture: ComponentFixture<CommunityPageSubCommunityListComponent>;
|
let fixture: ComponentFixture<CommunityPageSubCommunityListComponent>;
|
||||||
|
|
||||||
const subcommunities = [Object.assign(new Community(), {
|
const subcommunities = [Object.assign(new Community(), {
|
||||||
name: 'SubCommunity 1',
|
|
||||||
id: '123456789-1',
|
id: '123456789-1',
|
||||||
metadata: [
|
metadata: [
|
||||||
{
|
{
|
||||||
@@ -27,7 +26,6 @@ describe('SubCommunityList Component', () => {
|
|||||||
}]
|
}]
|
||||||
}),
|
}),
|
||||||
Object.assign(new Community(), {
|
Object.assign(new Community(), {
|
||||||
name: 'SubCommunity 2',
|
|
||||||
id: '123456789-2',
|
id: '123456789-2',
|
||||||
metadata: [
|
metadata: [
|
||||||
{
|
{
|
||||||
|
@@ -17,6 +17,7 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
animations: [fadeInOut]
|
animations: [fadeInOut]
|
||||||
})
|
})
|
||||||
|
|
||||||
export class TopLevelCommunityListComponent {
|
export class TopLevelCommunityListComponent {
|
||||||
communitiesRDObs: Observable<RemoteData<PaginatedList<Community>>>;
|
communitiesRDObs: Observable<RemoteData<PaginatedList<Community>>>;
|
||||||
config: PaginationComponentOptions;
|
config: PaginationComponentOptions;
|
||||||
|
@@ -1,22 +1,22 @@
|
|||||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import {RouterStub} from '../../../shared/testing/router-stub';
|
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||||
import {of as observableOf} from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import {RestResponse} from '../../../core/cache/response-cache.models';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import {RemoteData} from '../../../core/data/remote-data';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||||
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
import { CommonModule } from '@angular/common';
|
||||||
import {CommonModule} from '@angular/common';
|
import { FormsModule } from '@angular/forms';
|
||||||
import {FormsModule} from '@angular/forms';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import {RouterTestingModule} from '@angular/router/testing';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import {ItemDataService} from '../../../core/data/item-data.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
import { By } from '@angular/platform-browser';
|
||||||
import {By} from '@angular/platform-browser';
|
import { ItemDeleteComponent } from './item-delete.component';
|
||||||
import {ItemDeleteComponent} from './item-delete.component';
|
import { getItemEditPath } from '../../item-page-routing.module';
|
||||||
import {getItemEditPath} from '../../item-page-routing.module';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
let comp: ItemDeleteComponent;
|
let comp: ItemDeleteComponent;
|
||||||
let fixture: ComponentFixture<ItemDeleteComponent>;
|
let fixture: ComponentFixture<ItemDeleteComponent>;
|
||||||
@@ -27,8 +27,6 @@ let routerStub;
|
|||||||
let mockItemDataService: ItemDataService;
|
let mockItemDataService: ItemDataService;
|
||||||
let routeStub;
|
let routeStub;
|
||||||
let notificationsServiceStub;
|
let notificationsServiceStub;
|
||||||
let successfulRestResponse;
|
|
||||||
let failRestResponse;
|
|
||||||
|
|
||||||
describe('ItemDeleteComponent', () => {
|
describe('ItemDeleteComponent', () => {
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
@@ -46,14 +44,12 @@ describe('ItemDeleteComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
mockItemDataService = jasmine.createSpyObj('mockItemDataService', {
|
mockItemDataService = jasmine.createSpyObj('mockItemDataService', {
|
||||||
delete: observableOf(new RestResponse(true, '200'))
|
delete: observableOf(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
routeStub = {
|
routeStub = {
|
||||||
data: observableOf({
|
data: observableOf({
|
||||||
item: new RemoteData(false, false, true, null, {
|
item: new RemoteData(false, false, true, null, mockItem)
|
||||||
id: 'fake-id'
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,10 +59,10 @@ describe('ItemDeleteComponent', () => {
|
|||||||
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
declarations: [ItemDeleteComponent],
|
declarations: [ItemDeleteComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{provide: ActivatedRoute, useValue: routeStub},
|
{ provide: ActivatedRoute, useValue: routeStub },
|
||||||
{provide: Router, useValue: routerStub},
|
{ provide: Router, useValue: routerStub },
|
||||||
{provide: ItemDataService, useValue: mockItemDataService},
|
{ provide: ItemDataService, useValue: mockItemDataService },
|
||||||
{provide: NotificationsService, useValue: notificationsServiceStub},
|
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
||||||
], schemas: [
|
], schemas: [
|
||||||
CUSTOM_ELEMENTS_SCHEMA
|
CUSTOM_ELEMENTS_SCHEMA
|
||||||
]
|
]
|
||||||
@@ -74,9 +70,6 @@ describe('ItemDeleteComponent', () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
successfulRestResponse = new RestResponse(true, '200');
|
|
||||||
failRestResponse = new RestResponse(false, '500');
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ItemDeleteComponent);
|
fixture = TestBed.createComponent(ItemDeleteComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -95,22 +88,21 @@ describe('ItemDeleteComponent', () => {
|
|||||||
|
|
||||||
describe('performAction', () => {
|
describe('performAction', () => {
|
||||||
it('should call delete function from the ItemDataService', () => {
|
it('should call delete function from the ItemDataService', () => {
|
||||||
spyOn(comp, 'processRestResponse');
|
spyOn(comp, 'notify');
|
||||||
comp.performAction();
|
comp.performAction();
|
||||||
|
expect(mockItemDataService.delete).toHaveBeenCalledWith(mockItem);
|
||||||
expect(mockItemDataService.delete).toHaveBeenCalledWith(mockItem.id);
|
expect(comp.notify).toHaveBeenCalled();
|
||||||
expect(comp.processRestResponse).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('processRestResponse', () => {
|
describe('notify', () => {
|
||||||
it('should navigate to the homepage on successful deletion of the item', () => {
|
it('should navigate to the homepage on successful deletion of the item', () => {
|
||||||
comp.processRestResponse(successfulRestResponse);
|
comp.notify(true);
|
||||||
expect(routerStub.navigate).toHaveBeenCalledWith(['']);
|
expect(routerStub.navigate).toHaveBeenCalledWith(['']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('processRestResponse', () => {
|
describe('notify', () => {
|
||||||
it('should navigate to the item edit page on failed deletion of the item', () => {
|
it('should navigate to the item edit page on failed deletion of the item', () => {
|
||||||
comp.processRestResponse(failRestResponse);
|
comp.notify(false);
|
||||||
expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditPath('fake-id')]);
|
expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditPath('fake-id')]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import {Component} from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import {first} from 'rxjs/operators';
|
import { first } from 'rxjs/operators';
|
||||||
import {RestResponse} from '../../../core/cache/response-cache.models';
|
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
|
||||||
import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
|
import { getItemEditPath } from '../../item-page-routing.module';
|
||||||
import {getItemEditPath} from '../../item-page-routing.module';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-delete',
|
selector: 'ds-item-delete',
|
||||||
@@ -19,20 +19,19 @@ export class ItemDeleteComponent extends AbstractSimpleItemActionComponent {
|
|||||||
* Perform the delete action to the item
|
* Perform the delete action to the item
|
||||||
*/
|
*/
|
||||||
performAction() {
|
performAction() {
|
||||||
this.itemDataService.delete(this.item.id).pipe(first()).subscribe(
|
this.itemDataService.delete(this.item).pipe(first()).subscribe(
|
||||||
(response: RestResponse) => {
|
(succeeded: boolean) => {
|
||||||
this.processRestResponse(response);
|
this.notify(succeeded);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process the RestResponse retrieved from the server.
|
|
||||||
* When the item is successfully delete, navigate to the homepage, otherwise navigate back to the item edit page
|
* When the item is successfully delete, navigate to the homepage, otherwise navigate back to the item edit page
|
||||||
* @param response
|
* @param response
|
||||||
*/
|
*/
|
||||||
processRestResponse(response: RestResponse) {
|
notify(succeeded: boolean) {
|
||||||
if (response.isSuccessful) {
|
if (succeeded) {
|
||||||
this.notificationsService.success(this.translateService.get('item.edit.' + this.messageKey + '.success'));
|
this.notificationsService.success(this.translateService.get('item.edit.' + this.messageKey + '.success'));
|
||||||
this.router.navigate(['']);
|
this.router.navigate(['']);
|
||||||
} else {
|
} else {
|
||||||
|
@@ -2,7 +2,6 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
|||||||
import {Item} from '../../../core/shared/item.model';
|
import {Item} from '../../../core/shared/item.model';
|
||||||
import {RouterStub} from '../../../shared/testing/router-stub';
|
import {RouterStub} from '../../../shared/testing/router-stub';
|
||||||
import {of as observableOf} from 'rxjs';
|
import {of as observableOf} from 'rxjs';
|
||||||
import {RestResponse} from '../../../core/cache/response-cache.models';
|
|
||||||
import {RemoteData} from '../../../core/data/remote-data';
|
import {RemoteData} from '../../../core/data/remote-data';
|
||||||
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
||||||
import {CommonModule} from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
@@ -16,6 +15,7 @@ import {NotificationsService} from '../../../shared/notifications/notifications.
|
|||||||
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||||
import {By} from '@angular/platform-browser';
|
import {By} from '@angular/platform-browser';
|
||||||
import {ItemPrivateComponent} from './item-private.component';
|
import {ItemPrivateComponent} from './item-private.component';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
let comp: ItemPrivateComponent;
|
let comp: ItemPrivateComponent;
|
||||||
let fixture: ComponentFixture<ItemPrivateComponent>;
|
let fixture: ComponentFixture<ItemPrivateComponent>;
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import {Component} from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import {first} from 'rxjs/operators';
|
import { first } from 'rxjs/operators';
|
||||||
import {RestResponse} from '../../../core/cache/response-cache.models';
|
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
|
||||||
import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import {RemoteData} from '../../../core/data/remote-data';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-private',
|
selector: 'ds-item-private',
|
||||||
|
@@ -1,21 +1,21 @@
|
|||||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import {RouterStub} from '../../../shared/testing/router-stub';
|
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||||
import {of as observableOf} from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import {RestResponse} from '../../../core/cache/response-cache.models';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import {RemoteData} from '../../../core/data/remote-data';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||||
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
import { CommonModule } from '@angular/common';
|
||||||
import {CommonModule} from '@angular/common';
|
import { FormsModule } from '@angular/forms';
|
||||||
import {FormsModule} from '@angular/forms';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import {RouterTestingModule} from '@angular/router/testing';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import {ItemDataService} from '../../../core/data/item-data.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
import { By } from '@angular/platform-browser';
|
||||||
import {By} from '@angular/platform-browser';
|
import { ItemPublicComponent } from './item-public.component';
|
||||||
import {ItemPublicComponent} from './item-public.component';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
let comp: ItemPublicComponent;
|
let comp: ItemPublicComponent;
|
||||||
let fixture: ComponentFixture<ItemPublicComponent>;
|
let fixture: ComponentFixture<ItemPublicComponent>;
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import {Component} from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import {first} from 'rxjs/operators';
|
import { first } from 'rxjs/operators';
|
||||||
import {RestResponse} from '../../../core/cache/response-cache.models';
|
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
|
||||||
import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import {RemoteData} from '../../../core/data/remote-data';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-public',
|
selector: 'ds-item-public',
|
||||||
|
@@ -1,21 +1,21 @@
|
|||||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import {RouterStub} from '../../../shared/testing/router-stub';
|
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||||
import {of as observableOf} from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import {RestResponse} from '../../../core/cache/response-cache.models';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import {RemoteData} from '../../../core/data/remote-data';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||||
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
import { CommonModule } from '@angular/common';
|
||||||
import {CommonModule} from '@angular/common';
|
import { FormsModule } from '@angular/forms';
|
||||||
import {FormsModule} from '@angular/forms';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import {RouterTestingModule} from '@angular/router/testing';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import {ItemDataService} from '../../../core/data/item-data.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
import { By } from '@angular/platform-browser';
|
||||||
import {By} from '@angular/platform-browser';
|
import { ItemReinstateComponent } from './item-reinstate.component';
|
||||||
import {ItemReinstateComponent} from './item-reinstate.component';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
let comp: ItemReinstateComponent;
|
let comp: ItemReinstateComponent;
|
||||||
let fixture: ComponentFixture<ItemReinstateComponent>;
|
let fixture: ComponentFixture<ItemReinstateComponent>;
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import {Component} from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import {first} from 'rxjs/operators';
|
import { first } from 'rxjs/operators';
|
||||||
import {RestResponse} from '../../../core/cache/response-cache.models';
|
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
|
||||||
import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import {RemoteData} from '../../../core/data/remote-data';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-reinstate',
|
selector: 'ds-item-reinstate',
|
||||||
|
@@ -1,21 +1,21 @@
|
|||||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import {RouterStub} from '../../../shared/testing/router-stub';
|
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||||
import {of as observableOf} from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import {RestResponse} from '../../../core/cache/response-cache.models';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import {RemoteData} from '../../../core/data/remote-data';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||||
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
import { CommonModule } from '@angular/common';
|
||||||
import {CommonModule} from '@angular/common';
|
import { FormsModule } from '@angular/forms';
|
||||||
import {FormsModule} from '@angular/forms';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import {RouterTestingModule} from '@angular/router/testing';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import {ItemDataService} from '../../../core/data/item-data.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
import { ItemWithdrawComponent } from './item-withdraw.component';
|
||||||
import {ItemWithdrawComponent} from './item-withdraw.component';
|
import { By } from '@angular/platform-browser';
|
||||||
import {By} from '@angular/platform-browser';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
let comp: ItemWithdrawComponent;
|
let comp: ItemWithdrawComponent;
|
||||||
let fixture: ComponentFixture<ItemWithdrawComponent>;
|
let fixture: ComponentFixture<ItemWithdrawComponent>;
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import {Component} from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import {first} from 'rxjs/operators';
|
import { first } from 'rxjs/operators';
|
||||||
import {RestResponse} from '../../../core/cache/response-cache.models';
|
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
|
||||||
import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import {RemoteData} from '../../../core/data/remote-data';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-withdraw',
|
selector: 'ds-item-withdraw',
|
||||||
|
@@ -1,22 +1,22 @@
|
|||||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import {Item} from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import {RouterStub} from '../../../shared/testing/router-stub';
|
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||||
import {CommonModule} from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import {RouterTestingModule} from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import {FormsModule} from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import {ItemDataService} from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import {RemoteData} from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import {AbstractSimpleItemActionComponent} from './abstract-simple-item-action.component';
|
import { AbstractSimpleItemActionComponent } from './abstract-simple-item-action.component';
|
||||||
import {By} from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import {RestResponse} from '../../../core/cache/response-cache.models';
|
import { of as observableOf } from 'rxjs';
|
||||||
import {of as observableOf} from 'rxjs';
|
import { getItemEditPath } from '../../item-page-routing.module';
|
||||||
import {getItemEditPath} from '../../item-page-routing.module';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test component that implements the AbstractSimpleItemActionComponent used to test the
|
* Test component that implements the AbstractSimpleItemActionComponent used to test the
|
||||||
|
@@ -8,9 +8,9 @@ import {RemoteData} from '../../../core/data/remote-data';
|
|||||||
import {Observable} from 'rxjs';
|
import {Observable} from 'rxjs';
|
||||||
import {getSucceededRemoteData} from '../../../core/shared/operators';
|
import {getSucceededRemoteData} from '../../../core/shared/operators';
|
||||||
import {first, map} from 'rxjs/operators';
|
import {first, map} from 'rxjs/operators';
|
||||||
import {RestResponse} from '../../../core/cache/response-cache.models';
|
|
||||||
import {findSuccessfulAccordingTo} from '../edit-item-operators';
|
import {findSuccessfulAccordingTo} from '../edit-item-operators';
|
||||||
import {getItemEditPath} from '../../item-page-routing.module';
|
import {getItemEditPath} from '../../item-page-routing.module';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to render and handle simple item edit actions such as withdrawal and reinstatement.
|
* Component to render and handle simple item edit actions such as withdrawal and reinstatement.
|
||||||
|
@@ -6,7 +6,7 @@ import {
|
|||||||
Subject,
|
Subject,
|
||||||
Subscription
|
Subscription
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { switchMap, distinctUntilChanged, first, map } from 'rxjs/operators';
|
import { switchMap, distinctUntilChanged, map, take } from 'rxjs/operators';
|
||||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||||
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
|
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
@@ -126,7 +126,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
this.animationState = 'ready';
|
this.animationState = 'ready';
|
||||||
this.filterValues$.next(rd);
|
this.filterValues$.next(rd);
|
||||||
}));
|
}));
|
||||||
this.subs.push(newValues$.pipe(first()).subscribe((rd) => {
|
this.subs.push(newValues$.pipe(take(1)).subscribe((rd) => {
|
||||||
this.isLastPage$.next(hasNoValue(rd.payload.next))
|
this.isLastPage$.next(hasNoValue(rd.payload.next))
|
||||||
}));
|
}));
|
||||||
}));
|
}));
|
||||||
@@ -189,7 +189,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
* @param data The string from the input field
|
* @param data The string from the input field
|
||||||
*/
|
*/
|
||||||
onSubmit(data: any) {
|
onSubmit(data: any) {
|
||||||
this.selectedValues.pipe(first()).subscribe((selectedValues) => {
|
this.selectedValues.pipe(take(1)).subscribe((selectedValues) => {
|
||||||
if (isNotEmpty(data)) {
|
if (isNotEmpty(data)) {
|
||||||
this.router.navigate([this.getSearchLink()], {
|
this.router.navigate([this.getSearchLink()], {
|
||||||
queryParams:
|
queryParams:
|
||||||
@@ -258,7 +258,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
findSuggestions(data): void {
|
findSuggestions(data): void {
|
||||||
if (isNotEmpty(data)) {
|
if (isNotEmpty(data)) {
|
||||||
this.searchConfigService.searchOptions.pipe(first()).subscribe(
|
this.searchConfigService.searchOptions.pipe(take(1)).subscribe(
|
||||||
(options) => {
|
(options) => {
|
||||||
this.filterSearchResults = this.searchService.getFacetValuesFor(this.filterConfig, 1, options, data.toLowerCase())
|
this.filterSearchResults = this.searchService.getFacetValuesFor(this.filterConfig, 1, options, data.toLowerCase())
|
||||||
.pipe(
|
.pipe(
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import {first} from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||||
import { SearchFilterService } from './search-filter.service';
|
import { SearchFilterService } from './search-filter.service';
|
||||||
@@ -37,7 +37,7 @@ export class SearchFilterComponent implements OnInit {
|
|||||||
* Else, the filter should initially be collapsed
|
* Else, the filter should initially be collapsed
|
||||||
*/
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.getSelectedValues().pipe(first()).subscribe((isActive) => {
|
this.getSelectedValues().pipe(take(1)).subscribe((isActive) => {
|
||||||
if (this.filter.isOpenByDefault || isNotEmpty(isActive)) {
|
if (this.filter.isOpenByDefault || isNotEmpty(isActive)) {
|
||||||
this.initialExpand();
|
this.initialExpand();
|
||||||
} else {
|
} else {
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Component, HostBinding, OnInit } from '@angular/core';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { FilterType } from '../../../search-service/filter-type.model';
|
import { FilterType } from '../../../search-service/filter-type.model';
|
||||||
import {
|
import {
|
||||||
facetLoad,
|
facetLoad,
|
||||||
|
@@ -111,7 +111,6 @@ export const objects = [
|
|||||||
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
|
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
|
||||||
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
|
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
|
||||||
type: ResourceType.Community,
|
type: ResourceType.Community,
|
||||||
name: 'OR2017 - Demonstration',
|
|
||||||
metadata: [
|
metadata: [
|
||||||
{
|
{
|
||||||
key: 'dc.description',
|
key: 'dc.description',
|
||||||
@@ -161,7 +160,6 @@ export const objects = [
|
|||||||
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
||||||
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
||||||
type: ResourceType.Community,
|
type: ResourceType.Community,
|
||||||
name: 'Sample Community',
|
|
||||||
metadata: [
|
metadata: [
|
||||||
{
|
{
|
||||||
key: 'dc.description',
|
key: 'dc.description',
|
||||||
|
@@ -8,21 +8,18 @@ import { SearchService } from './search.service';
|
|||||||
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
||||||
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
|
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
|
||||||
import { RequestService } from '../../core/data/request.service';
|
import { RequestService } from '../../core/data/request.service';
|
||||||
import { ResponseCacheService } from '../../core/cache/response-cache.service';
|
|
||||||
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
||||||
import { RouterStub } from '../../shared/testing/router-stub';
|
import { RouterStub } from '../../shared/testing/router-stub';
|
||||||
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
|
import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
|
|
||||||
import { RequestEntry } from '../../core/data/request.reducer';
|
import { RequestEntry } from '../../core/data/request.reducer';
|
||||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
|
|
||||||
import {
|
import {
|
||||||
FacetConfigSuccessResponse,
|
FacetConfigSuccessResponse,
|
||||||
SearchSuccessResponse
|
SearchSuccessResponse
|
||||||
} from '../../core/cache/response-cache.models';
|
} from '../../core/cache/response.models';
|
||||||
import { SearchQueryResponse } from './search-query-response.model';
|
import { SearchQueryResponse } from './search-query-response.model';
|
||||||
import { SearchFilterConfig } from './search-filter-config.model';
|
import { SearchFilterConfig } from './search-filter-config.model';
|
||||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||||
@@ -54,7 +51,6 @@ describe('SearchService', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: Router, useValue: router },
|
{ provide: Router, useValue: router },
|
||||||
{ provide: ActivatedRoute, useValue: route },
|
{ provide: ActivatedRoute, useValue: route },
|
||||||
{ provide: ResponseCacheService, useValue: getMockResponseCacheService() },
|
|
||||||
{ provide: RequestService, useValue: getMockRequestService() },
|
{ provide: RequestService, useValue: getMockRequestService() },
|
||||||
{ provide: RemoteDataBuildService, useValue: {} },
|
{ provide: RemoteDataBuildService, useValue: {} },
|
||||||
{ provide: HALEndpointService, useValue: {} },
|
{ provide: HALEndpointService, useValue: {} },
|
||||||
@@ -86,11 +82,10 @@ describe('SearchService', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const remoteDataBuildService = {
|
const remoteDataBuildService = {
|
||||||
toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, responseCacheObs: Observable<ResponseCacheEntry>, payloadObs: Observable<any>) => {
|
toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, payloadObs: Observable<any>) => {
|
||||||
return observableCombineLatest(requestEntryObs,
|
return observableCombineLatest(requestEntryObs, payloadObs).pipe(
|
||||||
responseCacheObs, payloadObs).pipe(
|
map(([req, pay]) => {
|
||||||
map(([req, res, pay]) => {
|
return { req, pay };
|
||||||
return { req, res, pay };
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -113,7 +108,6 @@ describe('SearchService', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: Router, useValue: router },
|
{ provide: Router, useValue: router },
|
||||||
{ provide: ActivatedRoute, useValue: route },
|
{ provide: ActivatedRoute, useValue: route },
|
||||||
{ provide: ResponseCacheService, useValue: getMockResponseCacheService() },
|
|
||||||
{ provide: RequestService, useValue: getMockRequestService() },
|
{ provide: RequestService, useValue: getMockRequestService() },
|
||||||
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService },
|
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService },
|
||||||
{ provide: HALEndpointService, useValue: halService },
|
{ provide: HALEndpointService, useValue: halService },
|
||||||
@@ -162,10 +156,8 @@ describe('SearchService', () => {
|
|||||||
const searchOptions = new PaginatedSearchOptions({});
|
const searchOptions = new PaginatedSearchOptions({});
|
||||||
const queryResponse = Object.assign(new SearchQueryResponse(), { objects: [] });
|
const queryResponse = Object.assign(new SearchQueryResponse(), { objects: [] });
|
||||||
const response = new SearchSuccessResponse(queryResponse, '200');
|
const response = new SearchSuccessResponse(queryResponse, '200');
|
||||||
const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response });
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
|
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
|
||||||
(searchService as any).responseCache.get.and.returnValue(observableOf(responseEntry));
|
|
||||||
/* tslint:disable:no-empty */
|
/* tslint:disable:no-empty */
|
||||||
searchService.search(searchOptions).subscribe((t) => {
|
searchService.search(searchOptions).subscribe((t) => {
|
||||||
}); // subscribe to make sure all methods are called
|
}); // subscribe to make sure all methods are called
|
||||||
@@ -183,19 +175,14 @@ describe('SearchService', () => {
|
|||||||
it('should call getByHref on the request service with the correct request url', () => {
|
it('should call getByHref on the request service with the correct request url', () => {
|
||||||
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(endPoint);
|
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(endPoint);
|
||||||
});
|
});
|
||||||
it('should call get on the request service with the correct request url', () => {
|
|
||||||
expect((searchService as any).responseCache.get).toHaveBeenCalledWith(endPoint);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when getConfig is called without a scope', () => {
|
describe('when getConfig is called without a scope', () => {
|
||||||
const endPoint = 'http://endpoint.com/test/config';
|
const endPoint = 'http://endpoint.com/test/config';
|
||||||
const filterConfig = [new SearchFilterConfig()];
|
const filterConfig = [new SearchFilterConfig()];
|
||||||
const response = new FacetConfigSuccessResponse(filterConfig, '200');
|
const response = new FacetConfigSuccessResponse(filterConfig, '200');
|
||||||
const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response });
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
|
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
|
||||||
(searchService as any).responseCache.get.and.returnValue(observableOf(responseEntry));
|
|
||||||
/* tslint:disable:no-empty */
|
/* tslint:disable:no-empty */
|
||||||
searchService.getConfig(null).subscribe((t) => {
|
searchService.getConfig(null).subscribe((t) => {
|
||||||
}); // subscribe to make sure all methods are called
|
}); // subscribe to make sure all methods are called
|
||||||
@@ -213,9 +200,6 @@ describe('SearchService', () => {
|
|||||||
it('should call getByHref on the request service with the correct request url', () => {
|
it('should call getByHref on the request service with the correct request url', () => {
|
||||||
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(endPoint);
|
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(endPoint);
|
||||||
});
|
});
|
||||||
it('should call get on the request service with the correct request url', () => {
|
|
||||||
expect((searchService as any).responseCache.get).toHaveBeenCalledWith(endPoint);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when getConfig is called with a scope', () => {
|
describe('when getConfig is called with a scope', () => {
|
||||||
@@ -224,10 +208,8 @@ describe('SearchService', () => {
|
|||||||
const requestUrl = endPoint + '?scope=' + scope;
|
const requestUrl = endPoint + '?scope=' + scope;
|
||||||
const filterConfig = [new SearchFilterConfig()];
|
const filterConfig = [new SearchFilterConfig()];
|
||||||
const response = new FacetConfigSuccessResponse(filterConfig, '200');
|
const response = new FacetConfigSuccessResponse(filterConfig, '200');
|
||||||
const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response });
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
|
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
|
||||||
(searchService as any).responseCache.get.and.returnValue(observableOf(responseEntry));
|
|
||||||
/* tslint:disable:no-empty */
|
/* tslint:disable:no-empty */
|
||||||
searchService.getConfig(scope).subscribe((t) => {
|
searchService.getConfig(scope).subscribe((t) => {
|
||||||
}); // subscribe to make sure all methods are called
|
}); // subscribe to make sure all methods are called
|
||||||
@@ -245,9 +227,6 @@ describe('SearchService', () => {
|
|||||||
it('should call getByHref on the request service with the correct request url', () => {
|
it('should call getByHref on the request service with the correct request url', () => {
|
||||||
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(requestUrl);
|
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(requestUrl);
|
||||||
});
|
});
|
||||||
it('should call get on the request service with the correct request url', () => {
|
|
||||||
expect((searchService as any).responseCache.get).toHaveBeenCalledWith(requestUrl);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { of as observableOf, combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||||
import { Injectable, OnDestroy } from '@angular/core';
|
import { Injectable, OnDestroy } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ActivatedRoute,
|
ActivatedRoute,
|
||||||
@@ -7,15 +7,13 @@ import {
|
|||||||
Router,
|
Router,
|
||||||
UrlSegmentGroup
|
UrlSegmentGroup
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { flatMap, map, switchMap } from 'rxjs/operators';
|
import { map, switchMap, tap } from 'rxjs/operators';
|
||||||
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
||||||
import {
|
import {
|
||||||
FacetConfigSuccessResponse,
|
FacetConfigSuccessResponse,
|
||||||
FacetValueSuccessResponse,
|
FacetValueSuccessResponse,
|
||||||
SearchSuccessResponse
|
SearchSuccessResponse
|
||||||
} from '../../core/cache/response-cache.models';
|
} from '../../core/cache/response.models';
|
||||||
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
|
|
||||||
import { ResponseCacheService } from '../../core/cache/response-cache.service';
|
|
||||||
import { PaginatedList } from '../../core/data/paginated-list';
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
import { ResponseParsingService } from '../../core/data/parsing.service';
|
import { ResponseParsingService } from '../../core/data/parsing.service';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
@@ -24,7 +22,11 @@ import { RequestService } from '../../core/data/request.service';
|
|||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||||
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
import { configureRequest, getSucceededRemoteData } from '../../core/shared/operators';
|
import {
|
||||||
|
configureRequest,
|
||||||
|
getResponseFromEntry,
|
||||||
|
getSucceededRemoteData
|
||||||
|
} from '../../core/shared/operators';
|
||||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||||
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { NormalizedSearchResult } from '../normalized-search-result.model';
|
import { NormalizedSearchResult } from '../normalized-search-result.model';
|
||||||
@@ -68,7 +70,6 @@ export class SearchService implements OnDestroy {
|
|||||||
|
|
||||||
constructor(private router: Router,
|
constructor(private router: Router,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
protected responseCache: ResponseCacheService,
|
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
private rdb: RemoteDataBuildService,
|
private rdb: RemoteDataBuildService,
|
||||||
private halService: HALEndpointService,
|
private halService: HALEndpointService,
|
||||||
@@ -98,16 +99,12 @@ export class SearchService implements OnDestroy {
|
|||||||
configureRequest(this.requestService)
|
configureRequest(this.requestService)
|
||||||
);
|
);
|
||||||
const requestEntryObs = requestObs.pipe(
|
const requestEntryObs = requestObs.pipe(
|
||||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||||
);
|
|
||||||
|
|
||||||
const responseCacheObs = requestObs.pipe(
|
|
||||||
flatMap((request: RestRequest) => this.responseCache.get(request.href))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// get search results from response cache
|
// get search results from response cache
|
||||||
const sqrObs: Observable<SearchQueryResponse> = responseCacheObs.pipe(
|
const sqrObs: Observable<SearchQueryResponse> = requestEntryObs.pipe(
|
||||||
map((entry: ResponseCacheEntry) => entry.response),
|
getResponseFromEntry(),
|
||||||
map((response: SearchSuccessResponse) => response.results)
|
map((response: SearchSuccessResponse) => response.results)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -115,10 +112,11 @@ export class SearchService implements OnDestroy {
|
|||||||
// Turn list of observable remote data DSO's into observable remote data object with list of DSO
|
// Turn list of observable remote data DSO's into observable remote data object with list of DSO
|
||||||
const dsoObs: Observable<RemoteData<DSpaceObject[]>> = sqrObs.pipe(
|
const dsoObs: Observable<RemoteData<DSpaceObject[]>> = sqrObs.pipe(
|
||||||
map((sqr: SearchQueryResponse) => {
|
map((sqr: SearchQueryResponse) => {
|
||||||
return sqr.objects.map((nsr: NormalizedSearchResult) =>
|
return sqr.objects.map((nsr: NormalizedSearchResult) => {
|
||||||
this.rdb.buildSingle(nsr.dspaceObject));
|
return this.rdb.buildSingle(nsr.dspaceObject);
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
flatMap((input: Array<Observable<RemoteData<DSpaceObject>>>) => this.rdb.aggregate(input))
|
switchMap((input: Array<Observable<RemoteData<DSpaceObject>>>) => this.rdb.aggregate(input)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create search results again with the correct dso objects linked to each result
|
// Create search results again with the correct dso objects linked to each result
|
||||||
@@ -139,8 +137,8 @@ export class SearchService implements OnDestroy {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe(
|
const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
|
||||||
map((entry: ResponseCacheEntry) => entry.response),
|
getResponseFromEntry(),
|
||||||
map((response: FacetValueSuccessResponse) => response.pageInfo)
|
map((response: FacetValueSuccessResponse) => response.pageInfo)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -150,7 +148,7 @@ export class SearchService implements OnDestroy {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
|
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -182,21 +180,17 @@ export class SearchService implements OnDestroy {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const requestEntryObs = requestObs.pipe(
|
const requestEntryObs = requestObs.pipe(
|
||||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||||
);
|
|
||||||
|
|
||||||
const responseCacheObs = requestObs.pipe(
|
|
||||||
flatMap((request: RestRequest) => this.responseCache.get(request.href))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// get search results from response cache
|
// get search results from response cache
|
||||||
const facetConfigObs: Observable<SearchFilterConfig[]> = responseCacheObs.pipe(
|
const facetConfigObs: Observable<SearchFilterConfig[]> = requestEntryObs.pipe(
|
||||||
map((entry: ResponseCacheEntry) => entry.response),
|
getResponseFromEntry(),
|
||||||
map((response: FacetConfigSuccessResponse) =>
|
map((response: FacetConfigSuccessResponse) =>
|
||||||
response.results.map((result: any) => Object.assign(new SearchFilterConfig(), result)))
|
response.results.map((result: any) => Object.assign(new SearchFilterConfig(), result)))
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, facetConfigObs);
|
return this.rdb.toRemoteDataObservable(requestEntryObs, facetConfigObs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -229,21 +223,17 @@ export class SearchService implements OnDestroy {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const requestEntryObs = requestObs.pipe(
|
const requestEntryObs = requestObs.pipe(
|
||||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||||
);
|
|
||||||
|
|
||||||
const responseCacheObs = requestObs.pipe(
|
|
||||||
flatMap((request: RestRequest) => this.responseCache.get(request.href))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// get search results from response cache
|
// get search results from response cache
|
||||||
const facetValueObs: Observable<FacetValue[]> = responseCacheObs.pipe(
|
const facetValueObs: Observable<FacetValue[]> = requestEntryObs.pipe(
|
||||||
map((entry: ResponseCacheEntry) => entry.response),
|
getResponseFromEntry(),
|
||||||
map((response: FacetValueSuccessResponse) => response.results)
|
map((response: FacetValueSuccessResponse) => response.results)
|
||||||
);
|
);
|
||||||
|
|
||||||
const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe(
|
const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
|
||||||
map((entry: ResponseCacheEntry) => entry.response),
|
getResponseFromEntry(),
|
||||||
map((response: FacetValueSuccessResponse) => response.pageInfo)
|
map((response: FacetValueSuccessResponse) => response.pageInfo)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -253,7 +243,7 @@ export class SearchService implements OnDestroy {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
|
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { filter, first, map, take } from 'rxjs/operators';
|
import { filter, map, take } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
@@ -91,7 +91,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
// Whether is not authenticathed try to retrieve a possible stored auth token
|
// Whether is not authenticathed try to retrieve a possible stored auth token
|
||||||
this.store.pipe(select(isAuthenticated),
|
this.store.pipe(select(isAuthenticated),
|
||||||
first(),
|
take(1),
|
||||||
filter((authenticated) => !authenticated)
|
filter((authenticated) => !authenticated)
|
||||||
).subscribe((authenticated) => this.authService.checkAuthenticationToken());
|
).subscribe((authenticated) => this.authService.checkAuthenticationToken());
|
||||||
this.sidebarVisible = this.menuService.isMenuVisible(MenuID.ADMIN);
|
this.sidebarVisible = this.menuService.isMenuVisible(MenuID.ADMIN);
|
||||||
|
@@ -2,15 +2,15 @@ import { Observable, of as observableOf, throwError as observableThrowError } fr
|
|||||||
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
|
||||||
import { Inject, Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { GLOBAL_CONFIG } from '../../../config';
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
import { AuthGetRequest, AuthPostRequest, PostRequest, RestRequest } from '../data/request.models';
|
import { AuthGetRequest, AuthPostRequest, PostRequest, RestRequest } from '../data/request.models';
|
||||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
import { AuthStatusResponse, ErrorResponse } from '../cache/response.models';
|
||||||
import { AuthStatusResponse, ErrorResponse } from '../cache/response-cache.models';
|
|
||||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
|
import { getResponseFromEntry } from '../shared/operators';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthRequestService {
|
export class AuthRequestService {
|
||||||
@@ -19,18 +19,15 @@ export class AuthRequestService {
|
|||||||
|
|
||||||
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected responseCache: ResponseCacheService,
|
|
||||||
protected requestService: RequestService) {
|
protected requestService: RequestService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fetchRequest(request: RestRequest): Observable<any> {
|
protected fetchRequest(request: RestRequest): Observable<any> {
|
||||||
return this.responseCache.get(request.href).pipe(
|
return this.requestService.getByUUID(request.uuid).pipe(
|
||||||
map((entry: ResponseCacheEntry) => entry.response),
|
getResponseFromEntry(),
|
||||||
// TODO to review when https://github.com/DSpace/dspace-angular/issues/217 will be fixed
|
|
||||||
tap(() => this.responseCache.remove(request.href)),
|
|
||||||
mergeMap((response) => {
|
mergeMap((response) => {
|
||||||
if (response.isSuccessful && isNotEmpty(response)) {
|
if (response.isSuccessful && isNotEmpty(response)) {
|
||||||
return observableOf((response as AuthStatusResponse).response);
|
return observableOf((response as AuthStatusResponse).response);
|
||||||
} else if (!response.isSuccessful) {
|
} else if (!response.isSuccessful) {
|
||||||
return observableThrowError(new Error((response as ErrorResponse).errorMessage));
|
return observableThrowError(new Error((response as ErrorResponse).errorMessage));
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { AuthStatusResponse } from '../cache/response-cache.models';
|
import { AuthStatusResponse } from '../cache/response.models';
|
||||||
|
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
|
||||||
import { AuthStatus } from './models/auth-status.model';
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
import { AuthResponseParsingService } from './auth-response-parsing.service';
|
import { AuthResponseParsingService } from './auth-response-parsing.service';
|
||||||
import { AuthGetRequest, AuthPostRequest } from '../data/request.models';
|
import { AuthGetRequest, AuthPostRequest } from '../data/request.models';
|
||||||
@@ -11,7 +10,7 @@ import { ObjectCacheState } from '../cache/object-cache.reducer';
|
|||||||
describe('AuthResponseParsingService', () => {
|
describe('AuthResponseParsingService', () => {
|
||||||
let service: AuthResponseParsingService;
|
let service: AuthResponseParsingService;
|
||||||
|
|
||||||
const EnvConfig = { cache: { msToLive: 1000 } } as GlobalConfig;
|
const EnvConfig = { cache: { msToLive: 1000 } } as any;
|
||||||
const store = new MockStore<ObjectCacheState>({});
|
const store = new MockStore<ObjectCacheState>({});
|
||||||
const objectCacheService = new ObjectCacheService(store as any);
|
const objectCacheService = new ObjectCacheService(store as any);
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@angular/core';
|
|||||||
|
|
||||||
import { AuthObjectFactory } from './auth-object-factory';
|
import { AuthObjectFactory } from './auth-object-factory';
|
||||||
import { BaseResponseParsingService } from '../data/base-response-parsing.service';
|
import { BaseResponseParsingService } from '../data/base-response-parsing.service';
|
||||||
import { AuthStatusResponse, RestResponse } from '../cache/response-cache.models';
|
import { AuthStatusResponse, RestResponse } from '../cache/response.models';
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
import { GLOBAL_CONFIG } from '../../../config';
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
@@ -27,7 +27,7 @@ export class AuthResponseParsingService extends BaseResponseParsingService imple
|
|||||||
|
|
||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '200' || data.statusCode === 'OK')) {
|
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '200' || data.statusCode === 'OK')) {
|
||||||
const response = this.process<NormalizedAuthStatus, AuthType>(data.payload, request.href);
|
const response = this.process<NormalizedAuthStatus, AuthType>(data.payload, request.uuid);
|
||||||
return new AuthStatusResponse(response, data.statusCode);
|
return new AuthStatusResponse(response, data.statusCode);
|
||||||
} else {
|
} else {
|
||||||
return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode);
|
return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode);
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { of as observableOf, Observable } from 'rxjs';
|
import { of as observableOf, Observable } from 'rxjs';
|
||||||
|
|
||||||
import { filter, debounceTime, switchMap, take, tap, catchError, map, first } from 'rxjs/operators';
|
import { filter, debounceTime, switchMap, take, tap, catchError, map } from 'rxjs/operators';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
// import @ngrx
|
// import @ngrx
|
||||||
@@ -47,7 +47,7 @@ export class AuthEffects {
|
|||||||
ofType(AuthActionTypes.AUTHENTICATE),
|
ofType(AuthActionTypes.AUTHENTICATE),
|
||||||
switchMap((action: AuthenticateAction) => {
|
switchMap((action: AuthenticateAction) => {
|
||||||
return this.authService.authenticate(action.payload.email, action.payload.password).pipe(
|
return this.authService.authenticate(action.payload.email, action.payload.password).pipe(
|
||||||
first(),
|
take(1),
|
||||||
map((response: AuthStatus) => new AuthenticationSuccessAction(response.token)),
|
map((response: AuthStatus) => new AuthenticationSuccessAction(response.token)),
|
||||||
catchError((error) => observableOf(new AuthenticationErrorAction(error)))
|
catchError((error) => observableOf(new AuthenticationErrorAction(error)))
|
||||||
);
|
);
|
||||||
@@ -127,7 +127,7 @@ export class AuthEffects {
|
|||||||
switchMap(() => {
|
switchMap(() => {
|
||||||
return this.store.pipe(
|
return this.store.pipe(
|
||||||
select(isAuthenticated),
|
select(isAuthenticated),
|
||||||
first(),
|
take(1),
|
||||||
filter((authenticated) => !authenticated),
|
filter((authenticated) => !authenticated),
|
||||||
tap(() => this.authService.removeToken()),
|
tap(() => this.authService.removeToken()),
|
||||||
tap(() => this.authService.resetAuthenticationError())
|
tap(() => this.authService.resetAuthenticationError())
|
||||||
|
@@ -9,10 +9,10 @@ import { of as observableOf } from 'rxjs';
|
|||||||
import { AuthInterceptor } from './auth.interceptor';
|
import { AuthInterceptor } from './auth.interceptor';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { RestRequestMethod } from '../data/request.models';
|
|
||||||
import { RouterStub } from '../../shared/testing/router-stub';
|
import { RouterStub } from '../../shared/testing/router-stub';
|
||||||
import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer';
|
import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer';
|
||||||
import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
|
import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
|
||||||
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
|
||||||
describe(`AuthInterceptor`, () => {
|
describe(`AuthInterceptor`, () => {
|
||||||
let service: DSpaceRESTv2Service;
|
let service: DSpaceRESTv2Service;
|
||||||
@@ -49,7 +49,7 @@ describe(`AuthInterceptor`, () => {
|
|||||||
describe('when has a valid token', () => {
|
describe('when has a valid token', () => {
|
||||||
|
|
||||||
it('should not add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint', () => {
|
it('should not add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint', () => {
|
||||||
service.request(RestRequestMethod.Post, 'dspace-spring-rest/api/authn/login', 'password=password&user=user').subscribe((response) => {
|
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/authn/login', 'password=password&user=user').subscribe((response) => {
|
||||||
expect(response).toBeTruthy();
|
expect(response).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ describe(`AuthInterceptor`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint', () => {
|
it('should add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint', () => {
|
||||||
service.request(RestRequestMethod.Post, 'dspace-spring-rest/api/submission/workspaceitems', 'test').subscribe((response) => {
|
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'test').subscribe((response) => {
|
||||||
expect(response).toBeTruthy();
|
expect(response).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,11 +85,11 @@ describe(`AuthInterceptor`, () => {
|
|||||||
|
|
||||||
it('should redirect to login', () => {
|
it('should redirect to login', () => {
|
||||||
|
|
||||||
service.request(RestRequestMethod.Post, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user').subscribe((response) => {
|
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user').subscribe((response) => {
|
||||||
expect(response).toBeTruthy();
|
expect(response).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
service.request(RestRequestMethod.Post, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user');
|
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user');
|
||||||
|
|
||||||
httpMock.expectNone('dspace-spring-rest/api/submission/workspaceitems');
|
httpMock.expectNone('dspace-spring-rest/api/submission/workspaceitems');
|
||||||
});
|
});
|
||||||
|
@@ -277,7 +277,7 @@ export class AuthService {
|
|||||||
public isTokenExpiring(): Observable<boolean> {
|
public isTokenExpiring(): Observable<boolean> {
|
||||||
return this.store.pipe(
|
return this.store.pipe(
|
||||||
select(isTokenRefreshing),
|
select(isTokenRefreshing),
|
||||||
first(),
|
take(1),
|
||||||
map((isRefreshing: boolean) => {
|
map((isRefreshing: boolean) => {
|
||||||
if (this.isTokenExpired() || isRefreshing) {
|
if (this.isTokenExpired() || isRefreshing) {
|
||||||
return false;
|
return false;
|
||||||
@@ -360,7 +360,7 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
public redirectToPreviousUrl() {
|
public redirectToPreviousUrl() {
|
||||||
this.getRedirectUrl().pipe(
|
this.getRedirectUrl().pipe(
|
||||||
first())
|
take(1))
|
||||||
.subscribe((redirectUrl) => {
|
.subscribe((redirectUrl) => {
|
||||||
|
|
||||||
if (isNotEmpty(redirectUrl)) {
|
if (isNotEmpty(redirectUrl)) {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { first, map, switchMap } from 'rxjs/operators';
|
import { map, switchMap, take } from 'rxjs/operators';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
@@ -45,7 +45,7 @@ export class ServerAuthService extends AuthService {
|
|||||||
} else {
|
} else {
|
||||||
throw(new Error('Not authenticated'));
|
throw(new Error('Not authenticated'));
|
||||||
}
|
}
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,7 +60,7 @@ export class ServerAuthService extends AuthService {
|
|||||||
*/
|
*/
|
||||||
public redirectToPreviousUrl() {
|
public redirectToPreviousUrl() {
|
||||||
this.getRedirectUrl().pipe(
|
this.getRedirectUrl().pipe(
|
||||||
first())
|
take(1))
|
||||||
.subscribe((redirectUrl) => {
|
.subscribe((redirectUrl) => {
|
||||||
if (isNotEmpty(redirectUrl)) {
|
if (isNotEmpty(redirectUrl)) {
|
||||||
// override the route reuse strategy
|
// override the route reuse strategy
|
||||||
|
@@ -2,20 +2,19 @@ import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
|||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
|
import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
|
||||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
|
|
||||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
|
||||||
import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from '../data/request.models';
|
import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from '../data/request.models';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
import { BrowseService } from './browse.service';
|
import { BrowseService } from './browse.service';
|
||||||
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
|
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
|
||||||
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
describe('BrowseService', () => {
|
describe('BrowseService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
let service: BrowseService;
|
let service: BrowseService;
|
||||||
let responseCache: ResponseCacheService;
|
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
let rdbService: RemoteDataBuildService;
|
let rdbService: RemoteDataBuildService;
|
||||||
|
|
||||||
@@ -80,22 +79,14 @@ describe('BrowseService', () => {
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
function initMockResponseCacheService(isSuccessful: boolean) {
|
const getRequestEntry$ = (successful: boolean) => {
|
||||||
const rcs = getMockResponseCacheService();
|
return observableOf({
|
||||||
(rcs.get as any).and.returnValue(cold('b-', {
|
response: { isSuccessful: successful, payload: browseDefinitions } as any
|
||||||
b: {
|
} as RequestEntry)
|
||||||
response: {
|
};
|
||||||
isSuccessful,
|
|
||||||
payload: browseDefinitions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
return rcs;
|
|
||||||
}
|
|
||||||
|
|
||||||
function initTestService() {
|
function initTestService() {
|
||||||
return new BrowseService(
|
return new BrowseService(
|
||||||
responseCache,
|
|
||||||
requestService,
|
requestService,
|
||||||
halService,
|
halService,
|
||||||
rdbService
|
rdbService
|
||||||
@@ -109,8 +100,7 @@ describe('BrowseService', () => {
|
|||||||
describe('getBrowseDefinitions', () => {
|
describe('getBrowseDefinitions', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
responseCache = initMockResponseCacheService(true);
|
requestService = getMockRequestService(getRequestEntry$(true));
|
||||||
requestService = getMockRequestService();
|
|
||||||
rdbService = getMockRemoteDataBuildService();
|
rdbService = getMockRemoteDataBuildService();
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
spyOn(halService, 'getEndpoint').and
|
spyOn(halService, 'getEndpoint').and
|
||||||
@@ -148,8 +138,7 @@ describe('BrowseService', () => {
|
|||||||
const mockAuthorName = 'Donald Smith';
|
const mockAuthorName = 'Donald Smith';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
responseCache = initMockResponseCacheService(true);
|
requestService = getMockRequestService(getRequestEntry$(true));
|
||||||
requestService = getMockRequestService();
|
|
||||||
rdbService = getMockRemoteDataBuildService();
|
rdbService = getMockRemoteDataBuildService();
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
spyOn(service, 'getBrowseDefinitions').and
|
spyOn(service, 'getBrowseDefinitions').and
|
||||||
@@ -222,8 +211,7 @@ describe('BrowseService', () => {
|
|||||||
|
|
||||||
describe('if getBrowseDefinitions fires', () => {
|
describe('if getBrowseDefinitions fires', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
responseCache = initMockResponseCacheService(true);
|
requestService = getMockRequestService(getRequestEntry$(true));
|
||||||
requestService = getMockRequestService();
|
|
||||||
rdbService = getMockRemoteDataBuildService();
|
rdbService = getMockRemoteDataBuildService();
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
spyOn(service, 'getBrowseDefinitions').and
|
spyOn(service, 'getBrowseDefinitions').and
|
||||||
@@ -278,8 +266,7 @@ describe('BrowseService', () => {
|
|||||||
|
|
||||||
describe('if getBrowseDefinitions doesn\'t fire', () => {
|
describe('if getBrowseDefinitions doesn\'t fire', () => {
|
||||||
it('should return undefined', () => {
|
it('should return undefined', () => {
|
||||||
responseCache = initMockResponseCacheService(true);
|
requestService = getMockRequestService(getRequestEntry$(true));
|
||||||
requestService = getMockRequestService();
|
|
||||||
rdbService = getMockRemoteDataBuildService();
|
rdbService = getMockRemoteDataBuildService();
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
spyOn(service, 'getBrowseDefinitions').and
|
spyOn(service, 'getBrowseDefinitions').and
|
||||||
@@ -297,7 +284,6 @@ describe('BrowseService', () => {
|
|||||||
|
|
||||||
describe('getFirstItemFor', () => {
|
describe('getFirstItemFor', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
responseCache = initMockResponseCacheService(true);
|
|
||||||
requestService = getMockRequestService();
|
requestService = getMockRequestService();
|
||||||
rdbService = getMockRemoteDataBuildService();
|
rdbService = getMockRemoteDataBuildService();
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
|
@@ -9,9 +9,6 @@ import {
|
|||||||
isNotEmptyOperator
|
isNotEmptyOperator
|
||||||
} from '../../shared/empty.util';
|
} from '../../shared/empty.util';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { GenericSuccessResponse } from '../cache/response-cache.models';
|
|
||||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
|
||||||
import { PaginatedList } from '../data/paginated-list';
|
import { PaginatedList } from '../data/paginated-list';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import {
|
import {
|
||||||
@@ -28,13 +25,14 @@ import {
|
|||||||
configureRequest,
|
configureRequest,
|
||||||
filterSuccessfulResponses, getBrowseDefinitionLinks, getFirstOccurrence,
|
filterSuccessfulResponses, getBrowseDefinitionLinks, getFirstOccurrence,
|
||||||
getRemoteDataPayload,
|
getRemoteDataPayload,
|
||||||
getRequestFromSelflink,
|
getRequestFromRequestHref
|
||||||
getResponseFromSelflink
|
|
||||||
} from '../shared/operators';
|
} from '../shared/operators';
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
|
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
|
||||||
|
import { GenericSuccessResponse } from '../cache/response.models';
|
||||||
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The service handling all browse requests
|
* The service handling all browse requests
|
||||||
@@ -57,7 +55,6 @@ export class BrowseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
private rdb: RemoteDataBuildService,
|
private rdb: RemoteDataBuildService,
|
||||||
@@ -76,11 +73,9 @@ export class BrowseService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const href$ = request$.pipe(map((request: RestRequest) => request.href));
|
const href$ = request$.pipe(map((request: RestRequest) => request.href));
|
||||||
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
|
const requestEntry$ = href$.pipe(getRequestFromRequestHref(this.requestService));
|
||||||
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
|
const payload$ = requestEntry$.pipe(
|
||||||
const payload$ = responseCache$.pipe(
|
|
||||||
filterSuccessfulResponses(),
|
filterSuccessfulResponses(),
|
||||||
map((entry: ResponseCacheEntry) => entry.response),
|
|
||||||
map((response: GenericSuccessResponse<BrowseDefinition[]>) => response.payload),
|
map((response: GenericSuccessResponse<BrowseDefinition[]>) => response.payload),
|
||||||
ensureArrayHasValue(),
|
ensureArrayHasValue(),
|
||||||
map((definitions: BrowseDefinition[]) => definitions
|
map((definitions: BrowseDefinition[]) => definitions
|
||||||
@@ -88,7 +83,7 @@ export class BrowseService {
|
|||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,7 +117,7 @@ export class BrowseService {
|
|||||||
}
|
}
|
||||||
return href;
|
return href;
|
||||||
}),
|
}),
|
||||||
getBrowseEntriesFor(this.requestService, this.responseCache, this.rdb)
|
getBrowseEntriesFor(this.requestService, this.rdb)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +156,7 @@ export class BrowseService {
|
|||||||
}
|
}
|
||||||
return href;
|
return href;
|
||||||
}),
|
}),
|
||||||
getBrowseItemsFor(this.requestService, this.responseCache, this.rdb)
|
getBrowseItemsFor(this.requestService, this.rdb)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +183,7 @@ export class BrowseService {
|
|||||||
}
|
}
|
||||||
return href;
|
return href;
|
||||||
}),
|
}),
|
||||||
getBrowseItemsFor(this.requestService, this.responseCache, this.rdb),
|
getBrowseItemsFor(this.requestService, this.rdb),
|
||||||
getFirstOccurrence()
|
getFirstOccurrence()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -199,7 +194,7 @@ export class BrowseService {
|
|||||||
*/
|
*/
|
||||||
getPrevBrowseItems(items: RemoteData<PaginatedList<Item>>): Observable<RemoteData<PaginatedList<Item>>> {
|
getPrevBrowseItems(items: RemoteData<PaginatedList<Item>>): Observable<RemoteData<PaginatedList<Item>>> {
|
||||||
return observableOf(items.payload.prev).pipe(
|
return observableOf(items.payload.prev).pipe(
|
||||||
getBrowseItemsFor(this.requestService, this.responseCache, this.rdb)
|
getBrowseItemsFor(this.requestService, this.rdb)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +204,7 @@ export class BrowseService {
|
|||||||
*/
|
*/
|
||||||
getNextBrowseItems(items: RemoteData<PaginatedList<Item>>): Observable<RemoteData<PaginatedList<Item>>> {
|
getNextBrowseItems(items: RemoteData<PaginatedList<Item>>): Observable<RemoteData<PaginatedList<Item>>> {
|
||||||
return observableOf(items.payload.next).pipe(
|
return observableOf(items.payload.next).pipe(
|
||||||
getBrowseItemsFor(this.requestService, this.responseCache, this.rdb)
|
getBrowseItemsFor(this.requestService, this.rdb)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +214,7 @@ export class BrowseService {
|
|||||||
*/
|
*/
|
||||||
getPrevBrowseEntries(entries: RemoteData<PaginatedList<BrowseEntry>>): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
|
getPrevBrowseEntries(entries: RemoteData<PaginatedList<BrowseEntry>>): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
|
||||||
return observableOf(entries.payload.prev).pipe(
|
return observableOf(entries.payload.prev).pipe(
|
||||||
getBrowseEntriesFor(this.requestService, this.responseCache, this.rdb)
|
getBrowseEntriesFor(this.requestService, this.rdb)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +224,7 @@ export class BrowseService {
|
|||||||
*/
|
*/
|
||||||
getNextBrowseEntries(entries: RemoteData<PaginatedList<BrowseEntry>>): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
|
getNextBrowseEntries(entries: RemoteData<PaginatedList<BrowseEntry>>): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
|
||||||
return observableOf(entries.payload.next).pipe(
|
return observableOf(entries.payload.next).pipe(
|
||||||
getBrowseEntriesFor(this.requestService, this.responseCache, this.rdb)
|
getBrowseEntriesFor(this.requestService, this.rdb)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,12 +263,12 @@ export class BrowseService {
|
|||||||
* @param responseCache
|
* @param responseCache
|
||||||
* @param rdb
|
* @param rdb
|
||||||
*/
|
*/
|
||||||
export const getBrowseEntriesFor = (requestService: RequestService, responseCache: ResponseCacheService, rdb: RemoteDataBuildService) =>
|
export const getBrowseEntriesFor = (requestService: RequestService, rdb: RemoteDataBuildService) =>
|
||||||
(source: Observable<string>): Observable<RemoteData<PaginatedList<BrowseEntry>>> =>
|
(source: Observable<string>): Observable<RemoteData<PaginatedList<BrowseEntry>>> =>
|
||||||
source.pipe(
|
source.pipe(
|
||||||
map((href: string) => new BrowseEntriesRequest(requestService.generateRequestId(), href)),
|
map((href: string) => new BrowseEntriesRequest(requestService.generateRequestId(), href)),
|
||||||
configureRequest(requestService),
|
configureRequest(requestService),
|
||||||
toRDPaginatedBrowseEntries(requestService, responseCache, rdb)
|
toRDPaginatedBrowseEntries(requestService, rdb)
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -282,12 +277,12 @@ export const getBrowseEntriesFor = (requestService: RequestService, responseCach
|
|||||||
* @param responseCache
|
* @param responseCache
|
||||||
* @param rdb
|
* @param rdb
|
||||||
*/
|
*/
|
||||||
export const getBrowseItemsFor = (requestService: RequestService, responseCache: ResponseCacheService, rdb: RemoteDataBuildService) =>
|
export const getBrowseItemsFor = (requestService: RequestService, rdb: RemoteDataBuildService) =>
|
||||||
(source: Observable<string>): Observable<RemoteData<PaginatedList<Item>>> =>
|
(source: Observable<string>): Observable<RemoteData<PaginatedList<Item>>> =>
|
||||||
source.pipe(
|
source.pipe(
|
||||||
map((href: string) => new BrowseItemsRequest(requestService.generateRequestId(), href)),
|
map((href: string) => new BrowseItemsRequest(requestService.generateRequestId(), href)),
|
||||||
configureRequest(requestService),
|
configureRequest(requestService),
|
||||||
toRDPaginatedBrowseItems(requestService, responseCache, rdb)
|
toRDPaginatedBrowseItems(requestService, rdb)
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -296,16 +291,14 @@ export const getBrowseItemsFor = (requestService: RequestService, responseCache:
|
|||||||
* @param responseCache
|
* @param responseCache
|
||||||
* @param rdb
|
* @param rdb
|
||||||
*/
|
*/
|
||||||
export const toRDPaginatedBrowseItems = (requestService: RequestService, responseCache: ResponseCacheService, rdb: RemoteDataBuildService) =>
|
export const toRDPaginatedBrowseItems = (requestService: RequestService, rdb: RemoteDataBuildService) =>
|
||||||
(source: Observable<RestRequest>): Observable<RemoteData<PaginatedList<Item>>> => {
|
(source: Observable<RestRequest>): Observable<RemoteData<PaginatedList<Item>>> => {
|
||||||
const href$ = source.pipe(map((request: RestRequest) => request.href));
|
const href$ = source.pipe(map((request: RestRequest) => request.href));
|
||||||
|
|
||||||
const requestEntry$ = href$.pipe(getRequestFromSelflink(requestService));
|
const requestEntry$ = href$.pipe(getRequestFromRequestHref(requestService));
|
||||||
const responseCache$ = href$.pipe(getResponseFromSelflink(responseCache));
|
|
||||||
|
|
||||||
const payload$ = responseCache$.pipe(
|
const payload$ = requestEntry$.pipe(
|
||||||
filterSuccessfulResponses(),
|
filterSuccessfulResponses(),
|
||||||
map((entry: ResponseCacheEntry) => entry.response),
|
|
||||||
map((response: GenericSuccessResponse<Item[]>) => new PaginatedList(response.pageInfo, response.payload)),
|
map((response: GenericSuccessResponse<Item[]>) => new PaginatedList(response.pageInfo, response.payload)),
|
||||||
map((list: PaginatedList<Item>) => Object.assign(list, {
|
map((list: PaginatedList<Item>) => Object.assign(list, {
|
||||||
page: list.page ? list.page.map((item: DSpaceObject) => Object.assign(new Item(), item)) : list.page
|
page: list.page ? list.page.map((item: DSpaceObject) => Object.assign(new Item(), item)) : list.page
|
||||||
@@ -313,7 +306,7 @@ export const toRDPaginatedBrowseItems = (requestService: RequestService, respons
|
|||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
);
|
);
|
||||||
|
|
||||||
return rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
return rdb.toRemoteDataObservable(requestEntry$, payload$);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -322,16 +315,14 @@ export const toRDPaginatedBrowseItems = (requestService: RequestService, respons
|
|||||||
* @param responseCache
|
* @param responseCache
|
||||||
* @param rdb
|
* @param rdb
|
||||||
*/
|
*/
|
||||||
export const toRDPaginatedBrowseEntries = (requestService: RequestService, responseCache: ResponseCacheService, rdb: RemoteDataBuildService) =>
|
export const toRDPaginatedBrowseEntries = (requestService: RequestService, rdb: RemoteDataBuildService) =>
|
||||||
(source: Observable<RestRequest>): Observable<RemoteData<PaginatedList<BrowseEntry>>> => {
|
(source: Observable<RestRequest>): Observable<RemoteData<PaginatedList<BrowseEntry>>> => {
|
||||||
const href$ = source.pipe(map((request: RestRequest) => request.href));
|
const href$ = source.pipe(map((request: RestRequest) => request.href));
|
||||||
|
|
||||||
const requestEntry$ = href$.pipe(getRequestFromSelflink(requestService));
|
const requestEntry$ = href$.pipe(getRequestFromRequestHref(requestService));
|
||||||
const responseCache$ = href$.pipe(getResponseFromSelflink(responseCache));
|
|
||||||
|
|
||||||
const payload$ = responseCache$.pipe(
|
const payload$ = requestEntry$.pipe(
|
||||||
filterSuccessfulResponses(),
|
filterSuccessfulResponses(),
|
||||||
map((entry: ResponseCacheEntry) => entry.response),
|
|
||||||
map((response: GenericSuccessResponse<BrowseEntry[]>) => new PaginatedList(response.pageInfo, response.payload)),
|
map((response: GenericSuccessResponse<BrowseEntry[]>) => new PaginatedList(response.pageInfo, response.payload)),
|
||||||
map((list: PaginatedList<BrowseEntry>) => Object.assign(list, {
|
map((list: PaginatedList<BrowseEntry>) => Object.assign(list, {
|
||||||
page: list.page ? list.page.map((entry: BrowseEntry) => Object.assign(new BrowseEntry(), entry)) : list.page
|
page: list.page ? list.page.map((entry: BrowseEntry) => Object.assign(new BrowseEntry(), entry)) : list.page
|
||||||
@@ -339,5 +330,5 @@ export const toRDPaginatedBrowseEntries = (requestService: RequestService, respo
|
|||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
);
|
);
|
||||||
|
|
||||||
return rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
return rdb.toRemoteDataObservable(requestEntry$, payload$);
|
||||||
};
|
};
|
||||||
|
50
src/app/core/cache/builders/normalized-object-build.service.ts
vendored
Normal file
50
src/app/core/cache/builders/normalized-object-build.service.ts
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { NormalizedObject } from '../models/normalized-object.model';
|
||||||
|
import { CacheableObject } from '../object-cache.reducer';
|
||||||
|
import { getRelationships } from './build-decorators';
|
||||||
|
import { NormalizedObjectFactory } from '../models/normalized-object-factory';
|
||||||
|
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if halObj has a value for `_links.self`
|
||||||
|
*
|
||||||
|
* @param {any} halObj The object to test
|
||||||
|
*/
|
||||||
|
export function isRestDataObject(halObj: any): boolean {
|
||||||
|
return isNotEmpty(halObj._links) && hasValue(halObj._links.self);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if halObj has a value for `page` and `_embedded`
|
||||||
|
*
|
||||||
|
* @param {any} halObj The object to test
|
||||||
|
*/
|
||||||
|
export function isRestPaginatedList(halObj: any): boolean {
|
||||||
|
return hasValue(halObj.page) && hasValue(halObj._embedded);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service to turn domain models in to their normalized
|
||||||
|
* counterparts.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class NormalizedObjectBuildService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the normalized model that corresponds to the given domain model
|
||||||
|
*
|
||||||
|
* @param {TDomain} domainModel a domain model
|
||||||
|
*/
|
||||||
|
normalize<TDomain extends CacheableObject, TNormalized extends NormalizedObject>(domainModel: TDomain): TNormalized {
|
||||||
|
const normalizedConstructor = NormalizedObjectFactory.getConstructor(domainModel.type);
|
||||||
|
const relationships = getRelationships(normalizedConstructor) || [];
|
||||||
|
|
||||||
|
const normalizedModel = Object.assign({}, domainModel) as any;
|
||||||
|
relationships.forEach((key: string) => {
|
||||||
|
if (hasValue(domainModel[key])) {
|
||||||
|
domainModel[key] = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return normalizedModel;
|
||||||
|
}
|
||||||
|
}
|
@@ -32,7 +32,7 @@ describe('RemoteDataBuildService', () => {
|
|||||||
let service: RemoteDataBuildService;
|
let service: RemoteDataBuildService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service = new RemoteDataBuildService(undefined, undefined, undefined);
|
service = new RemoteDataBuildService(undefined, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when toPaginatedList is called', () => {
|
describe('when toPaginatedList is called', () => {
|
||||||
|
@@ -5,7 +5,15 @@ import {
|
|||||||
race as observableRace
|
race as observableRace
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { distinctUntilChanged, flatMap, map, startWith } from 'rxjs/operators';
|
import {
|
||||||
|
distinctUntilChanged,
|
||||||
|
first,
|
||||||
|
flatMap,
|
||||||
|
map,
|
||||||
|
startWith,
|
||||||
|
switchMap,
|
||||||
|
take
|
||||||
|
} from 'rxjs/operators';
|
||||||
import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { PaginatedList } from '../../data/paginated-list';
|
import { PaginatedList } from '../../data/paginated-list';
|
||||||
import { RemoteData } from '../../data/remote-data';
|
import { RemoteData } from '../../data/remote-data';
|
||||||
@@ -16,22 +24,18 @@ import { RequestService } from '../../data/request.service';
|
|||||||
|
|
||||||
import { NormalizedObject } from '../models/normalized-object.model';
|
import { NormalizedObject } from '../models/normalized-object.model';
|
||||||
import { ObjectCacheService } from '../object-cache.service';
|
import { ObjectCacheService } from '../object-cache.service';
|
||||||
import { DSOSuccessResponse, ErrorResponse } from '../response-cache.models';
|
import { DSOSuccessResponse, ErrorResponse } from '../response.models';
|
||||||
import { ResponseCacheEntry } from '../response-cache.reducer';
|
|
||||||
import { ResponseCacheService } from '../response-cache.service';
|
|
||||||
import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators';
|
import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators';
|
||||||
import { PageInfo } from '../../shared/page-info.model';
|
import { PageInfo } from '../../shared/page-info.model';
|
||||||
import {
|
import {
|
||||||
filterSuccessfulResponses,
|
filterSuccessfulResponses,
|
||||||
getRequestFromSelflink,
|
getRequestFromRequestHref, getRequestFromRequestUUID,
|
||||||
getResourceLinksFromResponse,
|
getResourceLinksFromResponse
|
||||||
getResponseFromSelflink
|
|
||||||
} from '../../shared/operators';
|
} from '../../shared/operators';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RemoteDataBuildService {
|
export class RemoteDataBuildService {
|
||||||
constructor(protected objectCache: ObjectCacheService,
|
constructor(protected objectCache: ObjectCacheService,
|
||||||
protected responseCache: ResponseCacheService,
|
|
||||||
protected requestService: RequestService) {
|
protected requestService: RequestService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,29 +43,24 @@ export class RemoteDataBuildService {
|
|||||||
if (typeof href$ === 'string') {
|
if (typeof href$ === 'string') {
|
||||||
href$ = observableOf(href$);
|
href$ = observableOf(href$);
|
||||||
}
|
}
|
||||||
const requestHref$ = href$.pipe(flatMap((href: string) =>
|
const requestUUID$ = href$.pipe(
|
||||||
this.objectCache.getRequestHrefBySelfLink(href)));
|
switchMap((href: string) =>
|
||||||
|
this.objectCache.getRequestUUIDBySelfLink(href)),
|
||||||
|
);
|
||||||
|
|
||||||
const requestEntry$ = observableRace(
|
const requestEntry$ = observableRace(
|
||||||
href$.pipe(getRequestFromSelflink(this.requestService)),
|
href$.pipe(getRequestFromRequestHref(this.requestService)),
|
||||||
requestHref$.pipe(getRequestFromSelflink(this.requestService))
|
requestUUID$.pipe(getRequestFromRequestUUID(this.requestService)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const responseCache$ = observableRace(
|
|
||||||
href$.pipe(getResponseFromSelflink(this.responseCache)),
|
|
||||||
requestHref$.pipe(getResponseFromSelflink(this.responseCache))
|
|
||||||
);
|
|
||||||
|
|
||||||
// always use self link if that is cached, only if it isn't, get it via the response.
|
// always use self link if that is cached, only if it isn't, get it via the response.
|
||||||
const payload$ =
|
const payload$ =
|
||||||
observableCombineLatest(
|
observableCombineLatest(
|
||||||
href$.pipe(
|
href$.pipe(
|
||||||
flatMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href)),
|
switchMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href)),
|
||||||
startWith(undefined)
|
startWith(undefined)),
|
||||||
),
|
requestEntry$.pipe(
|
||||||
responseCache$.pipe(
|
|
||||||
getResourceLinksFromResponse(),
|
getResourceLinksFromResponse(),
|
||||||
flatMap((resourceSelfLinks: string[]) => {
|
switchMap((resourceSelfLinks: string[]) => {
|
||||||
if (isNotEmpty(resourceSelfLinks)) {
|
if (isNotEmpty(resourceSelfLinks)) {
|
||||||
return this.objectCache.getBySelfLink(resourceSelfLinks[0]);
|
return this.objectCache.getBySelfLink(resourceSelfLinks[0]);
|
||||||
} else {
|
} else {
|
||||||
@@ -86,21 +85,21 @@ export class RemoteDataBuildService {
|
|||||||
startWith(undefined),
|
startWith(undefined),
|
||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
);
|
);
|
||||||
return this.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
return this.toRemoteDataObservable(requestEntry$, payload$);
|
||||||
}
|
}
|
||||||
|
|
||||||
toRemoteDataObservable<T>(requestEntry$: Observable<RequestEntry>, responseCache$: Observable<ResponseCacheEntry>, payload$: Observable<T>) {
|
toRemoteDataObservable<T>(requestEntry$: Observable<RequestEntry>, payload$: Observable<T>) {
|
||||||
return observableCombineLatest(requestEntry$, responseCache$.pipe(startWith(undefined)), payload$).pipe(
|
return observableCombineLatest(requestEntry$, payload$).pipe(
|
||||||
map(([reqEntry, resEntry, payload]) => {
|
map(([reqEntry, payload]) => {
|
||||||
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 error: RemoteDataError;
|
let error: RemoteDataError;
|
||||||
if (hasValue(resEntry) && hasValue(resEntry.response)) {
|
if (hasValue(reqEntry) && hasValue(reqEntry.response)) {
|
||||||
isSuccessful = resEntry.response.isSuccessful;
|
isSuccessful = reqEntry.response.isSuccessful;
|
||||||
const errorMessage = isSuccessful === false ? (resEntry.response as ErrorResponse).errorMessage : undefined;
|
const errorMessage = isSuccessful === false ? (reqEntry.response as ErrorResponse).errorMessage : undefined;
|
||||||
if (hasValue(errorMessage)) {
|
if (hasValue(errorMessage)) {
|
||||||
error = new RemoteDataError(resEntry.response.statusCode, errorMessage);
|
error = new RemoteDataError(reqEntry.response.statusCode, errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new RemoteData(
|
return new RemoteData(
|
||||||
@@ -119,10 +118,8 @@ export class RemoteDataBuildService {
|
|||||||
href$ = observableOf(href$);
|
href$ = observableOf(href$);
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
|
const requestEntry$ = href$.pipe(getRequestFromRequestHref(this.requestService));
|
||||||
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
|
const tDomainList$ = requestEntry$.pipe(
|
||||||
|
|
||||||
const tDomainList$ = responseCache$.pipe(
|
|
||||||
getResourceLinksFromResponse(),
|
getResourceLinksFromResponse(),
|
||||||
flatMap((resourceUUIDs: string[]) => {
|
flatMap((resourceUUIDs: string[]) => {
|
||||||
return this.objectCache.getList(resourceUUIDs).pipe(
|
return this.objectCache.getList(resourceUUIDs).pipe(
|
||||||
@@ -133,14 +130,13 @@ export class RemoteDataBuildService {
|
|||||||
}));
|
}));
|
||||||
}),
|
}),
|
||||||
startWith([]),
|
startWith([]),
|
||||||
distinctUntilChanged()
|
distinctUntilChanged(),
|
||||||
);
|
);
|
||||||
|
const pageInfo$ = requestEntry$.pipe(
|
||||||
const pageInfo$ = responseCache$.pipe(
|
|
||||||
filterSuccessfulResponses(),
|
filterSuccessfulResponses(),
|
||||||
map((entry: ResponseCacheEntry) => {
|
map((response: DSOSuccessResponse) => {
|
||||||
if (hasValue((entry.response as DSOSuccessResponse).pageInfo)) {
|
if (hasValue((response as DSOSuccessResponse).pageInfo)) {
|
||||||
const resPageInfo = (entry.response as DSOSuccessResponse).pageInfo;
|
const resPageInfo = (response as DSOSuccessResponse).pageInfo;
|
||||||
if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) {
|
if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) {
|
||||||
return Object.assign({}, resPageInfo, { currentPage: resPageInfo.currentPage + 1 });
|
return Object.assign({}, resPageInfo, { currentPage: resPageInfo.currentPage + 1 });
|
||||||
} else {
|
} else {
|
||||||
@@ -156,12 +152,11 @@ export class RemoteDataBuildService {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
return this.toRemoteDataObservable(requestEntry$, payload$);
|
||||||
}
|
}
|
||||||
|
|
||||||
build<TNormalized, TDomain>(normalized: TNormalized): TDomain {
|
build<TNormalized, TDomain>(normalized: TNormalized): TDomain {
|
||||||
const links: any = {};
|
const links: any = {};
|
||||||
|
|
||||||
const relationships = getRelationships(normalized.constructor) || [];
|
const relationships = getRelationships(normalized.constructor) || [];
|
||||||
|
|
||||||
relationships.forEach((relationship: string) => {
|
relationships.forEach((relationship: string) => {
|
||||||
@@ -204,7 +199,6 @@ export class RemoteDataBuildService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const domainModel = getMapsTo(normalized.constructor);
|
const domainModel = getMapsTo(normalized.constructor);
|
||||||
return Object.assign(new domainModel(), normalized, links);
|
return Object.assign(new domainModel(), normalized, links);
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { autoserialize, inheritSerialization } from 'cerialize';
|
import { autoserialize, deserialize, inheritSerialization, serialize } from 'cerialize';
|
||||||
|
|
||||||
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
|
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
|
||||||
import { Community } from '../../shared/community.model';
|
import { Community } from '../../shared/community.model';
|
||||||
@@ -21,32 +21,32 @@ export class NormalizedCommunity extends NormalizedDSpaceObject {
|
|||||||
/**
|
/**
|
||||||
* The Bitstream that represents the logo of this Community
|
* The Bitstream that represents the logo of this Community
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@deserialize
|
||||||
@relationship(ResourceType.Bitstream, false)
|
@relationship(ResourceType.Bitstream, false)
|
||||||
logo: string;
|
logo: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of Communities that are direct parents of this Community
|
* An array of Communities that are direct parents of this Community
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@deserialize
|
||||||
@relationship(ResourceType.Community, true)
|
@relationship(ResourceType.Community, true)
|
||||||
parents: string[];
|
parents: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Community that owns this Community
|
* The Community that owns this Community
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@deserialize
|
||||||
@relationship(ResourceType.Community, false)
|
@relationship(ResourceType.Community, false)
|
||||||
owner: string;
|
owner: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of Collections that are owned by this Community
|
* List of Collections that are owned by this Community
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@deserialize
|
||||||
@relationship(ResourceType.Collection, true)
|
@relationship(ResourceType.Collection, true)
|
||||||
collections: string[];
|
collections: string[];
|
||||||
|
|
||||||
@autoserialize
|
@deserialize
|
||||||
@relationship(ResourceType.Community, true)
|
@relationship(ResourceType.Community, true)
|
||||||
subcommunities: string[];
|
subcommunities: string[];
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { autoserialize, autoserializeAs } from 'cerialize';
|
import { autoserialize, autoserializeAs, deserialize, serialize } from 'cerialize';
|
||||||
import { DSpaceObject } from '../../shared/dspace-object.model';
|
import { DSpaceObject } from '../../shared/dspace-object.model';
|
||||||
|
|
||||||
import { Metadatum } from '../../shared/metadatum.model';
|
import { Metadatum } from '../../shared/metadatum.model';
|
||||||
@@ -18,7 +18,7 @@ export class NormalizedDSpaceObject extends NormalizedObject {
|
|||||||
* Repeated here to make the serialization work,
|
* Repeated here to make the serialization work,
|
||||||
* inheritSerialization doesn't seem to work for more than one level
|
* inheritSerialization doesn't seem to work for more than one level
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@deserialize
|
||||||
self: string;
|
self: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,12 +45,6 @@ export class NormalizedDSpaceObject extends NormalizedObject {
|
|||||||
@autoserialize
|
@autoserialize
|
||||||
type: ResourceType;
|
type: ResourceType;
|
||||||
|
|
||||||
/**
|
|
||||||
* The name for this DSpaceObject
|
|
||||||
*/
|
|
||||||
@autoserialize
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array containing all metadata of this DSpaceObject
|
* An array containing all metadata of this DSpaceObject
|
||||||
*/
|
*/
|
||||||
@@ -60,13 +54,13 @@ export class NormalizedDSpaceObject extends NormalizedObject {
|
|||||||
/**
|
/**
|
||||||
* An array of DSpaceObjects that are direct parents of this DSpaceObject
|
* An array of DSpaceObjects that are direct parents of this DSpaceObject
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@deserialize
|
||||||
parents: string[];
|
parents: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The DSpaceObject that owns this DSpaceObject
|
* The DSpaceObject that owns this DSpaceObject
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@deserialize
|
||||||
owner: string;
|
owner: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,7 +69,7 @@ export class NormalizedDSpaceObject extends NormalizedObject {
|
|||||||
* Repeated here to make the serialization work,
|
* Repeated here to make the serialization work,
|
||||||
* inheritSerialization doesn't seem to work for more than one level
|
* inheritSerialization doesn't seem to work for more than one level
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@deserialize
|
||||||
_links: {
|
_links: {
|
||||||
[name: string]: string
|
[name: string]: string
|
||||||
}
|
}
|
||||||
|
65
src/app/core/cache/object-cache.actions.ts
vendored
65
src/app/core/cache/object-cache.actions.ts
vendored
@@ -2,6 +2,7 @@ import { Action } from '@ngrx/store';
|
|||||||
|
|
||||||
import { type } from '../../shared/ngrx/type';
|
import { type } from '../../shared/ngrx/type';
|
||||||
import { CacheableObject } from './object-cache.reducer';
|
import { CacheableObject } from './object-cache.reducer';
|
||||||
|
import { Operation } from 'fast-json-patch';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of ObjectCacheAction type definitions
|
* The list of ObjectCacheAction type definitions
|
||||||
@@ -9,7 +10,9 @@ import { CacheableObject } from './object-cache.reducer';
|
|||||||
export const ObjectCacheActionTypes = {
|
export const ObjectCacheActionTypes = {
|
||||||
ADD: type('dspace/core/cache/object/ADD'),
|
ADD: type('dspace/core/cache/object/ADD'),
|
||||||
REMOVE: type('dspace/core/cache/object/REMOVE'),
|
REMOVE: type('dspace/core/cache/object/REMOVE'),
|
||||||
RESET_TIMESTAMPS: type('dspace/core/cache/object/RESET_TIMESTAMPS')
|
RESET_TIMESTAMPS: type('dspace/core/cache/object/RESET_TIMESTAMPS'),
|
||||||
|
ADD_PATCH: type('dspace/core/cache/object/ADD_PATCH'),
|
||||||
|
APPLY_PATCH: type('dspace/core/cache/object/APPLY_PATCH')
|
||||||
};
|
};
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
@@ -22,7 +25,7 @@ export class AddToObjectCacheAction implements Action {
|
|||||||
objectToCache: CacheableObject;
|
objectToCache: CacheableObject;
|
||||||
timeAdded: number;
|
timeAdded: number;
|
||||||
msToLive: number;
|
msToLive: number;
|
||||||
requestHref: string;
|
requestUUID: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,8 +42,8 @@ export class AddToObjectCacheAction implements Action {
|
|||||||
* This isn't necessarily the same as the object's self
|
* This isn't necessarily the same as the object's self
|
||||||
* link, it could have been part of a list for example
|
* link, it could have been part of a list for example
|
||||||
*/
|
*/
|
||||||
constructor(objectToCache: CacheableObject, timeAdded: number, msToLive: number, requestHref: string) {
|
constructor(objectToCache: CacheableObject, timeAdded: number, msToLive: number, requestUUID: string) {
|
||||||
this.payload = { objectToCache, timeAdded, msToLive, requestHref };
|
this.payload = { objectToCache, timeAdded, msToLive, requestUUID };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,11 +57,11 @@ export class RemoveFromObjectCacheAction implements Action {
|
|||||||
/**
|
/**
|
||||||
* Create a new RemoveFromObjectCacheAction
|
* Create a new RemoveFromObjectCacheAction
|
||||||
*
|
*
|
||||||
* @param uuid
|
* @param href
|
||||||
* the UUID of the object to remove
|
* the unique href of the object to remove
|
||||||
*/
|
*/
|
||||||
constructor(uuid: string) {
|
constructor(href: string) {
|
||||||
this.payload = uuid;
|
this.payload = href;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +82,48 @@ export class ResetObjectCacheTimestampsAction implements Action {
|
|||||||
this.payload = newTimestamp;
|
this.payload = newTimestamp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to add new operations to a specified cached object
|
||||||
|
*/
|
||||||
|
export class AddPatchObjectCacheAction implements Action {
|
||||||
|
type = ObjectCacheActionTypes.ADD_PATCH;
|
||||||
|
payload: {
|
||||||
|
href: string,
|
||||||
|
operations: Operation[]
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new AddPatchObjectCacheAction
|
||||||
|
*
|
||||||
|
* @param href
|
||||||
|
* the unique href of the object that should be updated
|
||||||
|
* @param operations
|
||||||
|
* the list of operations to add
|
||||||
|
*/
|
||||||
|
constructor(href: string, operations: Operation[]) {
|
||||||
|
this.payload = { href, operations };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to apply all existing operations to a specified cached object
|
||||||
|
*/
|
||||||
|
export class ApplyPatchObjectCacheAction implements Action {
|
||||||
|
type = ObjectCacheActionTypes.APPLY_PATCH;
|
||||||
|
payload: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new ApplyPatchObjectCacheAction
|
||||||
|
*
|
||||||
|
* @param href
|
||||||
|
* the unique href of the object that should be updated
|
||||||
|
*/
|
||||||
|
constructor(href: string) {
|
||||||
|
this.payload = href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* tslint:enable:max-classes-per-file */
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,4 +132,6 @@ export class ResetObjectCacheTimestampsAction implements Action {
|
|||||||
export type ObjectCacheAction
|
export type ObjectCacheAction
|
||||||
= AddToObjectCacheAction
|
= AddToObjectCacheAction
|
||||||
| RemoveFromObjectCacheAction
|
| RemoveFromObjectCacheAction
|
||||||
| ResetObjectCacheTimestampsAction;
|
| ResetObjectCacheTimestampsAction
|
||||||
|
| AddPatchObjectCacheAction
|
||||||
|
| ApplyPatchObjectCacheAction;
|
||||||
|
59
src/app/core/cache/object-cache.reducer.spec.ts
vendored
59
src/app/core/cache/object-cache.reducer.spec.ts
vendored
@@ -2,9 +2,13 @@ import * as deepFreeze from 'deep-freeze';
|
|||||||
|
|
||||||
import { objectCacheReducer } from './object-cache.reducer';
|
import { objectCacheReducer } from './object-cache.reducer';
|
||||||
import {
|
import {
|
||||||
|
AddPatchObjectCacheAction,
|
||||||
AddToObjectCacheAction,
|
AddToObjectCacheAction,
|
||||||
RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction
|
ApplyPatchObjectCacheAction,
|
||||||
|
RemoveFromObjectCacheAction,
|
||||||
|
ResetObjectCacheTimestampsAction
|
||||||
} from './object-cache.actions';
|
} from './object-cache.actions';
|
||||||
|
import { Operation } from 'fast-json-patch';
|
||||||
|
|
||||||
class NullAction extends RemoveFromObjectCacheAction {
|
class NullAction extends RemoveFromObjectCacheAction {
|
||||||
type = null;
|
type = null;
|
||||||
@@ -16,8 +20,11 @@ class NullAction extends RemoveFromObjectCacheAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('objectCacheReducer', () => {
|
describe('objectCacheReducer', () => {
|
||||||
|
const requestUUID1 = '8646169a-a8fc-4b31-a368-384c07867eb1';
|
||||||
|
const requestUUID2 = 'bd36820b-4bf7-4d58-bd80-b832058b7279';
|
||||||
const selfLink1 = 'https://localhost:8080/api/core/items/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
const selfLink1 = 'https://localhost:8080/api/core/items/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||||
const selfLink2 = 'https://localhost:8080/api/core/items/28b04544-1766-4e82-9728-c4e93544ecd3';
|
const selfLink2 = 'https://localhost:8080/api/core/items/28b04544-1766-4e82-9728-c4e93544ecd3';
|
||||||
|
const newName = 'new different name';
|
||||||
const testState = {
|
const testState = {
|
||||||
[selfLink1]: {
|
[selfLink1]: {
|
||||||
data: {
|
data: {
|
||||||
@@ -26,16 +33,20 @@ describe('objectCacheReducer', () => {
|
|||||||
},
|
},
|
||||||
timeAdded: new Date().getTime(),
|
timeAdded: new Date().getTime(),
|
||||||
msToLive: 900000,
|
msToLive: 900000,
|
||||||
requestHref: selfLink1
|
requestUUID: requestUUID1,
|
||||||
|
patches: [],
|
||||||
|
isDirty: false
|
||||||
},
|
},
|
||||||
[selfLink2]: {
|
[selfLink2]: {
|
||||||
data: {
|
data: {
|
||||||
self: selfLink2,
|
self: requestUUID2,
|
||||||
foo: 'baz'
|
foo: 'baz'
|
||||||
},
|
},
|
||||||
timeAdded: new Date().getTime(),
|
timeAdded: new Date().getTime(),
|
||||||
msToLive: 900000,
|
msToLive: 900000,
|
||||||
requestHref: selfLink2
|
requestUUID: selfLink2,
|
||||||
|
patches: [],
|
||||||
|
isDirty: false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
deepFreeze(testState);
|
deepFreeze(testState);
|
||||||
@@ -59,8 +70,8 @@ describe('objectCacheReducer', () => {
|
|||||||
const objectToCache = { self: selfLink1 };
|
const objectToCache = { self: selfLink1 };
|
||||||
const timeAdded = new Date().getTime();
|
const timeAdded = new Date().getTime();
|
||||||
const msToLive = 900000;
|
const msToLive = 900000;
|
||||||
const requestHref = 'https://rest.api/endpoint/selfLink1';
|
const requestUUID = requestUUID1;
|
||||||
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref);
|
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestUUID);
|
||||||
const newState = objectCacheReducer(state, action);
|
const newState = objectCacheReducer(state, action);
|
||||||
|
|
||||||
expect(newState[selfLink1].data).toEqual(objectToCache);
|
expect(newState[selfLink1].data).toEqual(objectToCache);
|
||||||
@@ -72,8 +83,8 @@ describe('objectCacheReducer', () => {
|
|||||||
const objectToCache = { self: selfLink1, foo: 'baz', somethingElse: true };
|
const objectToCache = { self: selfLink1, foo: 'baz', somethingElse: true };
|
||||||
const timeAdded = new Date().getTime();
|
const timeAdded = new Date().getTime();
|
||||||
const msToLive = 900000;
|
const msToLive = 900000;
|
||||||
const requestHref = 'https://rest.api/endpoint/selfLink1';
|
const requestUUID = requestUUID1;
|
||||||
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref);
|
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestUUID);
|
||||||
const newState = objectCacheReducer(testState, action);
|
const newState = objectCacheReducer(testState, action);
|
||||||
|
|
||||||
/* tslint:disable:no-string-literal */
|
/* tslint:disable:no-string-literal */
|
||||||
@@ -87,8 +98,8 @@ describe('objectCacheReducer', () => {
|
|||||||
const objectToCache = { self: selfLink1 };
|
const objectToCache = { self: selfLink1 };
|
||||||
const timeAdded = new Date().getTime();
|
const timeAdded = new Date().getTime();
|
||||||
const msToLive = 900000;
|
const msToLive = 900000;
|
||||||
const requestHref = 'https://rest.api/endpoint/selfLink1';
|
const requestUUID = requestUUID1;
|
||||||
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref);
|
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestUUID);
|
||||||
deepFreeze(state);
|
deepFreeze(state);
|
||||||
|
|
||||||
objectCacheReducer(state, action);
|
objectCacheReducer(state, action);
|
||||||
@@ -132,4 +143,32 @@ describe('objectCacheReducer', () => {
|
|||||||
objectCacheReducer(testState, action);
|
objectCacheReducer(testState, action);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should perform the ADD_PATCH action without affecting the previous state', () => {
|
||||||
|
const action = new AddPatchObjectCacheAction(selfLink1, [{
|
||||||
|
op: 'replace',
|
||||||
|
path: '/name',
|
||||||
|
value: 'random string'
|
||||||
|
}]);
|
||||||
|
// testState has already been frozen above
|
||||||
|
objectCacheReducer(testState, action);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should when the ADD_PATCH action dispatched', () => {
|
||||||
|
const patch = [{ op: 'add', path: '/name', value: newName } as Operation];
|
||||||
|
const action = new AddPatchObjectCacheAction(selfLink1, patch);
|
||||||
|
const newState = objectCacheReducer(testState, action);
|
||||||
|
expect(newState[selfLink1].patches.map((p) => p.operations)).toContain(patch);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should when the APPLY_PATCH action dispatched', () => {
|
||||||
|
const patch = [{ op: 'add', path: '/name', value: newName } as Operation];
|
||||||
|
const addPatchAction = new AddPatchObjectCacheAction(selfLink1, patch);
|
||||||
|
const stateWithPatch = objectCacheReducer(testState, addPatchAction);
|
||||||
|
|
||||||
|
const action = new ApplyPatchObjectCacheAction(selfLink1);
|
||||||
|
const newState = objectCacheReducer(stateWithPatch, action);
|
||||||
|
expect(newState[selfLink1].patches).toEqual([]);
|
||||||
|
expect((newState[selfLink1].data as any).name).toEqual(newName);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
89
src/app/core/cache/object-cache.reducer.ts
vendored
89
src/app/core/cache/object-cache.reducer.ts
vendored
@@ -1,10 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
ObjectCacheAction, ObjectCacheActionTypes, AddToObjectCacheAction,
|
ObjectCacheAction,
|
||||||
RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction
|
ObjectCacheActionTypes,
|
||||||
|
AddToObjectCacheAction,
|
||||||
|
RemoveFromObjectCacheAction,
|
||||||
|
ResetObjectCacheTimestampsAction,
|
||||||
|
AddPatchObjectCacheAction, ApplyPatchObjectCacheAction
|
||||||
} from './object-cache.actions';
|
} from './object-cache.actions';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { CacheEntry } from './cache-entry';
|
import { CacheEntry } from './cache-entry';
|
||||||
import { ResourceType } from '../shared/resource-type';
|
import { ResourceType } from '../shared/resource-type';
|
||||||
|
import { applyPatch, Operation } from 'fast-json-patch';
|
||||||
|
|
||||||
export enum DirtyType {
|
export enum DirtyType {
|
||||||
Created = 'Created',
|
Created = 'Created',
|
||||||
@@ -12,6 +17,21 @@ export enum DirtyType {
|
|||||||
Deleted = 'Deleted'
|
Deleted = 'Deleted'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface to represent a JsonPatch
|
||||||
|
*/
|
||||||
|
export interface Patch {
|
||||||
|
/**
|
||||||
|
* The identifier for this Patch
|
||||||
|
*/
|
||||||
|
uuid?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the list of operations this Patch is composed of
|
||||||
|
*/
|
||||||
|
operations: Operation[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An interface to represent objects that can be cached
|
* An interface to represent objects that can be cached
|
||||||
*
|
*
|
||||||
@@ -35,7 +55,9 @@ export class ObjectCacheEntry implements CacheEntry {
|
|||||||
data: CacheableObject;
|
data: CacheableObject;
|
||||||
timeAdded: number;
|
timeAdded: number;
|
||||||
msToLive: number;
|
msToLive: number;
|
||||||
requestHref: string;
|
requestUUID: string;
|
||||||
|
patches: Patch[] = [];
|
||||||
|
isDirty: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,6 +98,14 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi
|
|||||||
return resetObjectCacheTimestamps(state, action as ResetObjectCacheTimestampsAction)
|
return resetObjectCacheTimestamps(state, action as ResetObjectCacheTimestampsAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case ObjectCacheActionTypes.ADD_PATCH: {
|
||||||
|
return addPatchObjectCache(state, action as AddPatchObjectCacheAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
case ObjectCacheActionTypes.APPLY_PATCH: {
|
||||||
|
return applyPatchObjectCache(state, action as ApplyPatchObjectCacheAction);
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -93,12 +123,15 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi
|
|||||||
* the new state, with the object added, or overwritten.
|
* the new state, with the object added, or overwritten.
|
||||||
*/
|
*/
|
||||||
function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheAction): ObjectCacheState {
|
function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheAction): ObjectCacheState {
|
||||||
|
const existing = state[action.payload.objectToCache.self];
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
[action.payload.objectToCache.self]: {
|
[action.payload.objectToCache.self]: {
|
||||||
data: action.payload.objectToCache,
|
data: action.payload.objectToCache,
|
||||||
timeAdded: action.payload.timeAdded,
|
timeAdded: action.payload.timeAdded,
|
||||||
msToLive: action.payload.msToLive,
|
msToLive: action.payload.msToLive,
|
||||||
requestHref: action.payload.requestHref
|
requestUUID: action.payload.requestUUID,
|
||||||
|
isDirty: (hasValue(existing) ? isNotEmpty(existing.patches) : false),
|
||||||
|
patches: (hasValue(existing) ? existing.patches : [])
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -143,3 +176,49 @@ function resetObjectCacheTimestamps(state: ObjectCacheState, action: ResetObject
|
|||||||
});
|
});
|
||||||
return newState;
|
return newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the list of patch operations to a cached object
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* the current state
|
||||||
|
* @param action
|
||||||
|
* an AddPatchObjectCacheAction
|
||||||
|
* @return ObjectCacheState
|
||||||
|
* the new state, with the new operations added to the state of the specified ObjectCacheEntry
|
||||||
|
*/
|
||||||
|
function addPatchObjectCache(state: ObjectCacheState, action: AddPatchObjectCacheAction): ObjectCacheState {
|
||||||
|
const uuid = action.payload.href;
|
||||||
|
const operations = action.payload.operations;
|
||||||
|
const newState = Object.assign({}, state);
|
||||||
|
if (hasValue(newState[uuid])) {
|
||||||
|
const patches = newState[uuid].patches;
|
||||||
|
newState[uuid] = Object.assign({}, newState[uuid], {
|
||||||
|
patches: [...patches, { operations } as Patch],
|
||||||
|
isDirty: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the list of patch operations to a cached object
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* the current state
|
||||||
|
* @param action
|
||||||
|
* an ApplyPatchObjectCacheAction
|
||||||
|
* @return ObjectCacheState
|
||||||
|
* the new state, with the new operations applied to the state of the specified ObjectCacheEntry
|
||||||
|
*/
|
||||||
|
function applyPatchObjectCache(state: ObjectCacheState, action: ApplyPatchObjectCacheAction): ObjectCacheState {
|
||||||
|
const uuid = action.payload;
|
||||||
|
const newState = Object.assign({}, state);
|
||||||
|
if (hasValue(newState[uuid])) {
|
||||||
|
// flatten two dimensional array
|
||||||
|
const flatPatch: Operation[] = [].concat(...newState[uuid].patches.map((patch) => patch.operations));
|
||||||
|
const newData = applyPatch(newState[uuid].data, flatPatch, undefined, false);
|
||||||
|
newState[uuid] = Object.assign({}, newState[uuid], { data: newData.newDocument, patches: [] });
|
||||||
|
}
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
66
src/app/core/cache/object-cache.service.spec.ts
vendored
66
src/app/core/cache/object-cache.service.spec.ts
vendored
@@ -2,32 +2,52 @@ import { Store } from '@ngrx/store';
|
|||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
import { ObjectCacheService } from './object-cache.service';
|
import { ObjectCacheService } from './object-cache.service';
|
||||||
import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions';
|
import {
|
||||||
|
AddPatchObjectCacheAction,
|
||||||
|
AddToObjectCacheAction, ApplyPatchObjectCacheAction,
|
||||||
|
RemoveFromObjectCacheAction
|
||||||
|
} from './object-cache.actions';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
import { ResourceType } from '../shared/resource-type';
|
import { ResourceType } from '../shared/resource-type';
|
||||||
import { NormalizedItem } from './models/normalized-item.model';
|
import { NormalizedItem } from './models/normalized-item.model';
|
||||||
import { first } from 'rxjs/operators';
|
import { first } from 'rxjs/operators';
|
||||||
import * as ngrx from '@ngrx/store';
|
import * as ngrx from '@ngrx/store';
|
||||||
|
import { Operation } from '../../../../node_modules/fast-json-patch';
|
||||||
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
import { AddToSSBAction } from './server-sync-buffer.actions';
|
||||||
|
import { Patch } from './object-cache.reducer';
|
||||||
|
|
||||||
describe('ObjectCacheService', () => {
|
describe('ObjectCacheService', () => {
|
||||||
let service: ObjectCacheService;
|
let service: ObjectCacheService;
|
||||||
let store: Store<CoreState>;
|
let store: Store<CoreState>;
|
||||||
|
|
||||||
const selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
const selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||||
|
const requestUUID = '4d3a4ce8-a375-4b98-859b-39f0a014d736';
|
||||||
const timestamp = new Date().getTime();
|
const timestamp = new Date().getTime();
|
||||||
const msToLive = 900000;
|
const msToLive = 900000;
|
||||||
const objectToCache = {
|
let objectToCache = {
|
||||||
self: selfLink,
|
self: selfLink,
|
||||||
type: ResourceType.Item
|
type: ResourceType.Item
|
||||||
};
|
};
|
||||||
const cacheEntry = {
|
let cacheEntry;
|
||||||
data: objectToCache,
|
let invalidCacheEntry;
|
||||||
timeAdded: timestamp,
|
const operations = [{ op: 'replace', path: '/name', value: 'random string' } as Operation];
|
||||||
msToLive: msToLive
|
|
||||||
};
|
function init() {
|
||||||
const invalidCacheEntry = Object.assign({}, cacheEntry, { msToLive: -1 });
|
objectToCache = {
|
||||||
|
self: selfLink,
|
||||||
|
type: ResourceType.Item
|
||||||
|
};
|
||||||
|
cacheEntry = {
|
||||||
|
data: objectToCache,
|
||||||
|
timeAdded: timestamp,
|
||||||
|
msToLive: msToLive
|
||||||
|
};
|
||||||
|
invalidCacheEntry = Object.assign({}, cacheEntry, { msToLive: -1 })
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
init();
|
||||||
store = new Store<CoreState>(undefined, undefined, undefined);
|
store = new Store<CoreState>(undefined, undefined, undefined);
|
||||||
spyOn(store, 'dispatch');
|
spyOn(store, 'dispatch');
|
||||||
service = new ObjectCacheService(store);
|
service = new ObjectCacheService(store);
|
||||||
@@ -39,8 +59,8 @@ describe('ObjectCacheService', () => {
|
|||||||
|
|
||||||
describe('add', () => {
|
describe('add', () => {
|
||||||
it('should dispatch an ADD action with the object to add, the time to live, and the current timestamp', () => {
|
it('should dispatch an ADD action with the object to add, the time to live, and the current timestamp', () => {
|
||||||
service.add(objectToCache, msToLive, selfLink);
|
service.add(objectToCache, msToLive, requestUUID);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive, selfLink));
|
expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive, requestUUID));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -127,4 +147,30 @@ describe('ObjectCacheService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('patch methods', () => {
|
||||||
|
it('should dispatch the correct actions when addPatch is called', () => {
|
||||||
|
service.addPatch(selfLink, operations);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new AddPatchObjectCacheAction(selfLink, operations));
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new AddToSSBAction(selfLink, RestRequestMethod.PATCH));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isDirty should return true when the patches list in the cache entry is not empty', () => {
|
||||||
|
cacheEntry.patches = [
|
||||||
|
{
|
||||||
|
operations: operations
|
||||||
|
} as Patch];
|
||||||
|
const result = (service as any).isDirty(cacheEntry);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isDirty should return false when the patches list in the cache entry is empty', () => {
|
||||||
|
cacheEntry.patches = [];
|
||||||
|
const result = (service as any).isDirty(cacheEntry);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
it('should dispatch the correct actions when applyPatchesToCachedObject is called', () => {
|
||||||
|
(service as any).applyPatchesToCachedObject(selfLink);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new ApplyPatchObjectCacheAction(selfLink));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
94
src/app/core/cache/object-cache.service.ts
vendored
94
src/app/core/cache/object-cache.service.ts
vendored
@@ -1,25 +1,33 @@
|
|||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
|
|
||||||
import { distinctUntilChanged, filter, first, map, mergeMap, take } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
||||||
import { IndexName } from '../index/index.reducer';
|
import { IndexName } from '../index/index.reducer';
|
||||||
|
|
||||||
import { CacheableObject, ObjectCacheEntry } from './object-cache.reducer';
|
import { CacheableObject, ObjectCacheEntry } from './object-cache.reducer';
|
||||||
import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions';
|
import {
|
||||||
import { hasNoValue } from '../../shared/empty.util';
|
AddPatchObjectCacheAction,
|
||||||
|
AddToObjectCacheAction,
|
||||||
|
ApplyPatchObjectCacheAction,
|
||||||
|
RemoveFromObjectCacheAction
|
||||||
|
} from './object-cache.actions';
|
||||||
|
import { hasNoValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { GenericConstructor } from '../shared/generic-constructor';
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
import { coreSelector, CoreState } from '../core.reducers';
|
import { coreSelector, CoreState } from '../core.reducers';
|
||||||
import { pathSelector } from '../shared/selectors';
|
import { pathSelector } from '../shared/selectors';
|
||||||
import { NormalizedObjectFactory } from './models/normalized-object-factory';
|
import { NormalizedObjectFactory } from './models/normalized-object-factory';
|
||||||
import { NormalizedObject } from './models/normalized-object.model';
|
import { NormalizedObject } from './models/normalized-object.model';
|
||||||
|
import { applyPatch, Operation } from 'fast-json-patch';
|
||||||
|
import { AddToSSBAction } from './server-sync-buffer.actions';
|
||||||
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
|
||||||
function selfLinkFromUuidSelector(uuid: string): MemoizedSelector<CoreState, string> {
|
function selfLinkFromUuidSelector(uuid: string): MemoizedSelector<CoreState, string> {
|
||||||
return pathSelector<CoreState, string>(coreSelector, 'index', IndexName.OBJECT, 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 pathSelector<CoreState, ObjectCacheEntry>(coreSelector, 'data/object', selfLink);
|
return pathSelector<CoreState, ObjectCacheEntry>(coreSelector, 'cache/object', selfLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,20 +45,18 @@ export class ObjectCacheService {
|
|||||||
* The object to add
|
* The object to add
|
||||||
* @param msToLive
|
* @param msToLive
|
||||||
* The number of milliseconds it should be cached for
|
* The number of milliseconds it should be cached for
|
||||||
* @param requestHref
|
* @param requestUUID
|
||||||
* The selfLink of the request that resulted in this object
|
* The UUID of the request that resulted in this object
|
||||||
* This isn't necessarily the same as the object's self
|
|
||||||
* link, it could have been part of a list for example
|
|
||||||
*/
|
*/
|
||||||
add(objectToCache: CacheableObject, msToLive: number, requestHref: string): void {
|
add(objectToCache: CacheableObject, msToLive: number, requestUUID: string): void {
|
||||||
this.store.dispatch(new AddToObjectCacheAction(objectToCache, new Date().getTime(), msToLive, requestHref));
|
this.store.dispatch(new AddToObjectCacheAction(objectToCache, new Date().getTime(), msToLive, requestUUID));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the object with the supplied UUID from the cache
|
* Remove the object with the supplied href from the cache
|
||||||
*
|
*
|
||||||
* @param uuid
|
* @param href
|
||||||
* The UUID of the object to be removed
|
* The unique href of the object to be removed
|
||||||
*/
|
*/
|
||||||
remove(uuid: string): void {
|
remove(uuid: string): void {
|
||||||
this.store.dispatch(new RemoveFromObjectCacheAction(uuid));
|
this.store.dispatch(new RemoveFromObjectCacheAction(uuid));
|
||||||
@@ -82,10 +88,21 @@ export class ObjectCacheService {
|
|||||||
|
|
||||||
getBySelfLink<T extends NormalizedObject>(selfLink: string): Observable<T> {
|
getBySelfLink<T extends NormalizedObject>(selfLink: string): Observable<T> {
|
||||||
return this.getEntry(selfLink).pipe(
|
return this.getEntry(selfLink).pipe(
|
||||||
|
map((entry: ObjectCacheEntry) => {
|
||||||
|
if (isNotEmpty(entry.patches)) {
|
||||||
|
const flatPatch: Operation[] = [].concat(...entry.patches.map((patch) => patch.operations));
|
||||||
|
const patchedData = applyPatch(entry.data, flatPatch, undefined, false).newDocument;
|
||||||
|
return Object.assign({}, entry, { data: patchedData });
|
||||||
|
} else {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
map((entry: ObjectCacheEntry) => {
|
map((entry: ObjectCacheEntry) => {
|
||||||
const type: GenericConstructor<NormalizedObject> = NormalizedObjectFactory.getConstructor(entry.data.type);
|
const type: GenericConstructor<NormalizedObject> = NormalizedObjectFactory.getConstructor(entry.data.type);
|
||||||
return Object.assign(new type(), entry.data) as T
|
return Object.assign(new type(), entry.data) as T
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEntry(selfLink: string): Observable<ObjectCacheEntry> {
|
private getEntry(selfLink: string): Observable<ObjectCacheEntry> {
|
||||||
@@ -96,16 +113,16 @@ export class ObjectCacheService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRequestHrefBySelfLink(selfLink: string): Observable<string> {
|
getRequestUUIDBySelfLink(selfLink: string): Observable<string> {
|
||||||
return this.getEntry(selfLink).pipe(
|
return this.getEntry(selfLink).pipe(
|
||||||
map((entry: ObjectCacheEntry) => entry.requestHref),
|
map((entry: ObjectCacheEntry) => entry.requestUUID),
|
||||||
distinctUntilChanged(),);
|
distinctUntilChanged());
|
||||||
}
|
}
|
||||||
|
|
||||||
getRequestHrefByUUID(uuid: string): Observable<string> {
|
getRequestUUIDByObjectUUID(uuid: string): Observable<string> {
|
||||||
return this.store.pipe(
|
return this.store.pipe(
|
||||||
select(selfLinkFromUuidSelector(uuid)),
|
select(selfLinkFromUuidSelector(uuid)),
|
||||||
mergeMap((selfLink: string) => this.getRequestHrefBySelfLink(selfLink))
|
mergeMap((selfLink: string) => this.getRequestUUIDBySelfLink(selfLink))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +165,7 @@ export class ObjectCacheService {
|
|||||||
|
|
||||||
this.store.pipe(
|
this.store.pipe(
|
||||||
select(selfLinkFromUuidSelector(uuid)),
|
select(selfLinkFromUuidSelector(uuid)),
|
||||||
first()
|
take(1)
|
||||||
).subscribe((selfLink: string) => result = this.hasBySelfLink(selfLink));
|
).subscribe((selfLink: string) => result = this.hasBySelfLink(selfLink));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -167,7 +184,7 @@ export class ObjectCacheService {
|
|||||||
let result = false;
|
let result = false;
|
||||||
|
|
||||||
this.store.pipe(select(entryFromSelfLinkSelector(selfLink)),
|
this.store.pipe(select(entryFromSelfLinkSelector(selfLink)),
|
||||||
first()
|
take(1)
|
||||||
).subscribe((entry: ObjectCacheEntry) => result = this.isValid(entry));
|
).subscribe((entry: ObjectCacheEntry) => result = this.isValid(entry));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -195,4 +212,39 @@ export class ObjectCacheService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add operations to the existing list of operations for an ObjectCacheEntry
|
||||||
|
* Makes sure the ServerSyncBuffer for this ObjectCacheEntry is updated
|
||||||
|
* @param {string} uuid
|
||||||
|
* the uuid of the ObjectCacheEntry
|
||||||
|
* @param {Operation[]} patch
|
||||||
|
* list of operations to perform
|
||||||
|
*/
|
||||||
|
public addPatch(selfLink: string, patch: Operation[]) {
|
||||||
|
this.store.dispatch(new AddPatchObjectCacheAction(selfLink, patch));
|
||||||
|
this.store.dispatch(new AddToSSBAction(selfLink, RestRequestMethod.PATCH));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether there are any unperformed operations for an ObjectCacheEntry
|
||||||
|
*
|
||||||
|
* @param entry
|
||||||
|
* the entry to check
|
||||||
|
* @return boolean
|
||||||
|
* false if the entry is there are no operations left in the ObjectCacheEntry, true otherwise
|
||||||
|
*/
|
||||||
|
private isDirty(entry: ObjectCacheEntry): boolean {
|
||||||
|
return isNotEmpty(entry.patches);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the existing operations on an ObjectCacheEntry in the store
|
||||||
|
* NB: this does not make any server side changes
|
||||||
|
* @param {string} uuid
|
||||||
|
* the uuid of the ObjectCacheEntry
|
||||||
|
*/
|
||||||
|
private applyPatchesToCachedObject(selfLink: string) {
|
||||||
|
this.store.dispatch(new ApplyPatchObjectCacheAction(selfLink));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
72
src/app/core/cache/response-cache.actions.ts
vendored
72
src/app/core/cache/response-cache.actions.ts
vendored
@@ -1,72 +0,0 @@
|
|||||||
import { Action } from '@ngrx/store';
|
|
||||||
|
|
||||||
import { type } from '../../shared/ngrx/type';
|
|
||||||
import { RestResponse } from './response-cache.models';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of ResponseCacheAction type definitions
|
|
||||||
*/
|
|
||||||
export const ResponseCacheActionTypes = {
|
|
||||||
ADD: type('dspace/core/cache/response/ADD'),
|
|
||||||
REMOVE: type('dspace/core/cache/response/REMOVE'),
|
|
||||||
RESET_TIMESTAMPS: type('dspace/core/cache/response/RESET_TIMESTAMPS')
|
|
||||||
};
|
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
|
||||||
export class ResponseCacheAddAction implements Action {
|
|
||||||
type = ResponseCacheActionTypes.ADD;
|
|
||||||
payload: {
|
|
||||||
key: string,
|
|
||||||
response: RestResponse
|
|
||||||
timeAdded: number;
|
|
||||||
msToLive: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(key: string, response: RestResponse, timeAdded: number, msToLive: number) {
|
|
||||||
this.payload = { key, response, timeAdded, msToLive };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An ngrx action to remove a request from the cache
|
|
||||||
*/
|
|
||||||
export class ResponseCacheRemoveAction implements Action {
|
|
||||||
type = ResponseCacheActionTypes.REMOVE;
|
|
||||||
payload: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new ResponseCacheRemoveAction
|
|
||||||
* @param key
|
|
||||||
* The key of the request to remove
|
|
||||||
*/
|
|
||||||
constructor(key: string) {
|
|
||||||
this.payload = key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An ngrx action to reset the timeAdded property of all cached objects
|
|
||||||
*/
|
|
||||||
export class ResetResponseCacheTimestampsAction implements Action {
|
|
||||||
type = ResponseCacheActionTypes.RESET_TIMESTAMPS;
|
|
||||||
payload: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new ResetObjectCacheTimestampsAction
|
|
||||||
*
|
|
||||||
* @param newTimestamp
|
|
||||||
* the new timeAdded all objects should get
|
|
||||||
*/
|
|
||||||
constructor(newTimestamp: number) {
|
|
||||||
this.payload = newTimestamp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* tslint:enable:max-classes-per-file */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A type to encompass all ResponseCacheActions
|
|
||||||
*/
|
|
||||||
export type ResponseCacheAction
|
|
||||||
= ResponseCacheAddAction
|
|
||||||
| ResponseCacheRemoveAction
|
|
||||||
| ResetResponseCacheTimestampsAction;
|
|
@@ -1,38 +0,0 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
27
src/app/core/cache/response-cache.effects.ts
vendored
27
src/app/core/cache/response-cache.effects.ts
vendored
@@ -1,27 +0,0 @@
|
|||||||
import { map } from 'rxjs/operators';
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
|
||||||
|
|
||||||
import { ResetResponseCacheTimestampsAction } from './response-cache.actions';
|
|
||||||
import { StoreActionTypes } from '../../store.actions';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ResponseCacheEffects {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When the store is rehydrated in the browser, set all cache
|
|
||||||
* timestamps to 'now', because the time zone of the server can
|
|
||||||
* differ from the client.
|
|
||||||
*
|
|
||||||
* This assumes that the server cached everything a negligible
|
|
||||||
* time ago, and will likely need to be revisited later
|
|
||||||
*/
|
|
||||||
@Effect() fixTimestampsOnRehydrate = this.actions$
|
|
||||||
.pipe(ofType(StoreActionTypes.REHYDRATE),
|
|
||||||
map(() => new ResetResponseCacheTimestampsAction(new Date().getTime()))
|
|
||||||
);
|
|
||||||
|
|
||||||
constructor(private actions$: Actions,) {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
124
src/app/core/cache/response-cache.reducer.spec.ts
vendored
124
src/app/core/cache/response-cache.reducer.spec.ts
vendored
@@ -1,124 +0,0 @@
|
|||||||
import * as deepFreeze from 'deep-freeze';
|
|
||||||
|
|
||||||
import { responseCacheReducer, ResponseCacheState } from './response-cache.reducer';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ResponseCacheRemoveAction,
|
|
||||||
ResetResponseCacheTimestampsAction, ResponseCacheAddAction
|
|
||||||
} from './response-cache.actions';
|
|
||||||
import { RestResponse } from './response-cache.models';
|
|
||||||
|
|
||||||
class NullAction extends ResponseCacheRemoveAction {
|
|
||||||
type = null;
|
|
||||||
payload = null;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('responseCacheReducer', () => {
|
|
||||||
const keys = ['125c17f89046283c5f0640722aac9feb', 'a06c3006a41caec5d635af099b0c780c'];
|
|
||||||
const msToLive = 900000;
|
|
||||||
const uuids = [
|
|
||||||
'9e32a2e2-6b91-4236-a361-995ccdc14c60',
|
|
||||||
'598ce822-c357-46f3-ab70-63724d02d6ad',
|
|
||||||
'be8325f7-243b-49f4-8a4b-df2b793ff3b5'
|
|
||||||
];
|
|
||||||
const testState: ResponseCacheState = {
|
|
||||||
[keys[0]]: {
|
|
||||||
key: keys[0],
|
|
||||||
response: new RestResponse(true, '200'),
|
|
||||||
timeAdded: new Date().getTime(),
|
|
||||||
msToLive: msToLive
|
|
||||||
},
|
|
||||||
[keys[1]]: {
|
|
||||||
key: keys[1],
|
|
||||||
response: new RestResponse(true, '200'),
|
|
||||||
timeAdded: new Date().getTime(),
|
|
||||||
msToLive: msToLive
|
|
||||||
}
|
|
||||||
};
|
|
||||||
deepFreeze(testState);
|
|
||||||
const errorState: {} = {
|
|
||||||
[keys[0]]: {
|
|
||||||
errorMessage: 'error',
|
|
||||||
resourceUUIDs: uuids
|
|
||||||
}
|
|
||||||
};
|
|
||||||
deepFreeze(errorState);
|
|
||||||
|
|
||||||
it('should return the current state when no valid actions have been made', () => {
|
|
||||||
const action = new NullAction();
|
|
||||||
const newState = responseCacheReducer(testState, action);
|
|
||||||
|
|
||||||
expect(newState).toEqual(testState);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should start with an empty cache', () => {
|
|
||||||
const action = new NullAction();
|
|
||||||
const initialState = responseCacheReducer(undefined, action);
|
|
||||||
|
|
||||||
expect(initialState).toEqual(Object.create(null));
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ADD', () => {
|
|
||||||
const addTimeAdded = new Date().getTime();
|
|
||||||
const addMsToLive = 5;
|
|
||||||
const addResponse = new RestResponse(true, '200');
|
|
||||||
const action = new ResponseCacheAddAction(keys[0], addResponse, addTimeAdded, addMsToLive);
|
|
||||||
|
|
||||||
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]].timeAdded).toBe(addTimeAdded);
|
|
||||||
expect(newState[keys[0]].msToLive).toBe(addMsToLive);
|
|
||||||
expect(newState[keys[0]].response).toBe(addResponse);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
111
src/app/core/cache/response-cache.reducer.ts
vendored
111
src/app/core/cache/response-cache.reducer.ts
vendored
@@ -1,111 +0,0 @@
|
|||||||
import {
|
|
||||||
ResponseCacheAction, ResponseCacheActionTypes,
|
|
||||||
ResponseCacheRemoveAction, ResetResponseCacheTimestampsAction,
|
|
||||||
ResponseCacheAddAction
|
|
||||||
} from './response-cache.actions';
|
|
||||||
import { CacheEntry } from './cache-entry';
|
|
||||||
import { hasValue } from '../../shared/empty.util';
|
|
||||||
import { RestResponse } from './response-cache.models';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An entry in the ResponseCache
|
|
||||||
*/
|
|
||||||
export class ResponseCacheEntry implements CacheEntry {
|
|
||||||
key: string;
|
|
||||||
response: RestResponse;
|
|
||||||
timeAdded: number;
|
|
||||||
msToLive: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ResponseCache State
|
|
||||||
*/
|
|
||||||
export interface ResponseCacheState {
|
|
||||||
[key: string]: ResponseCacheEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
|
||||||
const initialState = Object.create(null);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ResponseCache Reducer
|
|
||||||
*
|
|
||||||
* @param state
|
|
||||||
* the current state
|
|
||||||
* @param action
|
|
||||||
* the action to perform on the state
|
|
||||||
* @return ResponseCacheState
|
|
||||||
* the new state
|
|
||||||
*/
|
|
||||||
export function responseCacheReducer(state = initialState, action: ResponseCacheAction): ResponseCacheState {
|
|
||||||
switch (action.type) {
|
|
||||||
|
|
||||||
case ResponseCacheActionTypes.ADD: {
|
|
||||||
return addToCache(state, action as ResponseCacheAddAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
case ResponseCacheActionTypes.REMOVE: {
|
|
||||||
return removeFromCache(state, action as ResponseCacheRemoveAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
case ResponseCacheActionTypes.RESET_TIMESTAMPS: {
|
|
||||||
return resetResponseCacheTimestamps(state, action as ResetResponseCacheTimestampsAction)
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addToCache(state: ResponseCacheState, action: ResponseCacheAddAction): ResponseCacheState {
|
|
||||||
return Object.assign({}, state, {
|
|
||||||
[action.payload.key]: {
|
|
||||||
key: action.payload.key,
|
|
||||||
response: action.payload.response,
|
|
||||||
timeAdded: action.payload.timeAdded,
|
|
||||||
msToLive: action.payload.msToLive
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a request from the cache
|
|
||||||
*
|
|
||||||
* @param state
|
|
||||||
* the current state
|
|
||||||
* @param action
|
|
||||||
* an ResponseCacheRemoveAction
|
|
||||||
* @return ResponseCacheState
|
|
||||||
* the new state, with the request removed if it existed.
|
|
||||||
*/
|
|
||||||
function removeFromCache(state: ResponseCacheState, action: ResponseCacheRemoveAction): ResponseCacheState {
|
|
||||||
if (hasValue(state[action.payload])) {
|
|
||||||
const newCache = Object.assign({}, state);
|
|
||||||
delete newCache[action.payload];
|
|
||||||
|
|
||||||
return newCache;
|
|
||||||
} else {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the timeAdded timestamp of every cached request to the specified value
|
|
||||||
*
|
|
||||||
* @param state
|
|
||||||
* the current state
|
|
||||||
* @param action
|
|
||||||
* a ResetResponseCacheTimestampsAction
|
|
||||||
* @return ResponseCacheState
|
|
||||||
* the new state, with all timeAdded timestamps set to the specified value
|
|
||||||
*/
|
|
||||||
function resetResponseCacheTimestamps(state: ResponseCacheState, action: ResetResponseCacheTimestampsAction): ResponseCacheState {
|
|
||||||
const newState = Object.create(null);
|
|
||||||
Object.keys(state).forEach((key) => {
|
|
||||||
newState[key] = Object.assign({}, state[key], {
|
|
||||||
timeAdded: action.payload
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return newState;
|
|
||||||
}
|
|
100
src/app/core/cache/response-cache.service.spec.ts
vendored
100
src/app/core/cache/response-cache.service.spec.ts
vendored
@@ -1,100 +0,0 @@
|
|||||||
import { Store } from '@ngrx/store';
|
|
||||||
|
|
||||||
import { ResponseCacheService } from './response-cache.service';
|
|
||||||
import { of as observableOf } from 'rxjs';
|
|
||||||
import { CoreState } from '../core.reducers';
|
|
||||||
import { RestResponse } from './response-cache.models';
|
|
||||||
import { ResponseCacheEntry } from './response-cache.reducer';
|
|
||||||
import { first } from 'rxjs/operators';
|
|
||||||
import * as ngrx from '@ngrx/store'
|
|
||||||
import { cold } from 'jasmine-marbles';
|
|
||||||
|
|
||||||
describe('ResponseCacheService', () => {
|
|
||||||
let service: ResponseCacheService;
|
|
||||||
let store: Store<CoreState>;
|
|
||||||
|
|
||||||
const keys = ['125c17f89046283c5f0640722aac9feb', 'a06c3006a41caec5d635af099b0c780c'];
|
|
||||||
const timestamp = new Date().getTime();
|
|
||||||
const validCacheEntry = (key) => {
|
|
||||||
return {
|
|
||||||
key: key,
|
|
||||||
response: new RestResponse(true, '200'),
|
|
||||||
timeAdded: timestamp,
|
|
||||||
msToLive: 24 * 60 * 60 * 1000 // a day
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const invalidCacheEntry = (key) => {
|
|
||||||
return {
|
|
||||||
key: key,
|
|
||||||
response: new RestResponse(true, '200'),
|
|
||||||
timeAdded: 0,
|
|
||||||
msToLive: 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
store = new Store<CoreState>(undefined, undefined, undefined);
|
|
||||||
spyOn(store, 'dispatch');
|
|
||||||
service = new ResponseCacheService(store);
|
|
||||||
spyOn(Date.prototype, 'getTime').and.callFake(() => {
|
|
||||||
return timestamp;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('get', () => {
|
|
||||||
it('should return an observable of the cached request with the specified key', () => {
|
|
||||||
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
|
||||||
return () => {
|
|
||||||
return () => observableOf(validCacheEntry(keys[1]));
|
|
||||||
};
|
|
||||||
});
|
|
||||||
let testObj: ResponseCacheEntry;
|
|
||||||
service.get(keys[1]).pipe(first()).subscribe((entry) => {
|
|
||||||
testObj = entry;
|
|
||||||
});
|
|
||||||
expect(testObj.key).toEqual(keys[1]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not return a cached request that has exceeded its time to live', () => {
|
|
||||||
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
|
||||||
return () => {
|
|
||||||
return () => observableOf(invalidCacheEntry(keys[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', () => {
|
|
||||||
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
|
||||||
return () => {
|
|
||||||
return () => observableOf(validCacheEntry(keys[1]));
|
|
||||||
};
|
|
||||||
});
|
|
||||||
expect(service.has(keys[1])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false if the request with the supplied key isn\'t cached', () => {
|
|
||||||
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
|
||||||
return () => {
|
|
||||||
return () => observableOf(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', () => {
|
|
||||||
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
|
||||||
return () => {
|
|
||||||
return () => observableOf(invalidCacheEntry(keys[1]));
|
|
||||||
};
|
|
||||||
});
|
|
||||||
expect(service.has(keys[1])).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
100
src/app/core/cache/response-cache.service.ts
vendored
100
src/app/core/cache/response-cache.service.ts
vendored
@@ -1,100 +0,0 @@
|
|||||||
import { filter, take, distinctUntilChanged, first } from 'rxjs/operators';
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
import { ResponseCacheEntry } from './response-cache.reducer';
|
|
||||||
import { hasNoValue } from '../../shared/empty.util';
|
|
||||||
import { ResponseCacheRemoveAction, ResponseCacheAddAction } from './response-cache.actions';
|
|
||||||
import { RestResponse } from './response-cache.models';
|
|
||||||
import { coreSelector, CoreState } from '../core.reducers';
|
|
||||||
import { pathSelector } from '../shared/selectors';
|
|
||||||
|
|
||||||
function entryFromKeySelector(key: string): MemoizedSelector<CoreState, ResponseCacheEntry> {
|
|
||||||
return pathSelector<CoreState, ResponseCacheEntry>(coreSelector, 'data/response', key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A service to interact with the response cache
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class ResponseCacheService {
|
|
||||||
constructor(
|
|
||||||
private store: Store<CoreState>
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
add(key: string, response: RestResponse, msToLive: number): Observable<ResponseCacheEntry> {
|
|
||||||
if (!this.has(key)) {
|
|
||||||
this.store.dispatch(new ResponseCacheAddAction(key, response, new Date().getTime(), msToLive));
|
|
||||||
}
|
|
||||||
return this.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an observable of the response with the specified key
|
|
||||||
*
|
|
||||||
* @param key
|
|
||||||
* the key of the response to get
|
|
||||||
* @return Observable<ResponseCacheEntry>
|
|
||||||
* an observable of the ResponseCacheEntry with the specified key
|
|
||||||
*/
|
|
||||||
get(key: string): Observable<ResponseCacheEntry> {
|
|
||||||
return this.store.pipe(
|
|
||||||
select(entryFromKeySelector(key)),
|
|
||||||
filter((entry: ResponseCacheEntry) => this.isValid(entry)),
|
|
||||||
distinctUntilChanged()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether the response with the specified key is cached
|
|
||||||
*
|
|
||||||
* @param key
|
|
||||||
* the key of the response to check
|
|
||||||
* @return boolean
|
|
||||||
* true if the response with the specified key is cached,
|
|
||||||
* false otherwise
|
|
||||||
*/
|
|
||||||
has(key: string): boolean {
|
|
||||||
let result: boolean;
|
|
||||||
|
|
||||||
this.store.pipe(select(entryFromKeySelector(key)),
|
|
||||||
first()
|
|
||||||
).subscribe((entry: ResponseCacheEntry) => {
|
|
||||||
result = this.isValid(entry);
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
remove(key: string): void {
|
|
||||||
if (this.has(key)) {
|
|
||||||
this.store.dispatch(new ResponseCacheRemoveAction(key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether a ResponseCacheEntry should still be cached
|
|
||||||
*
|
|
||||||
* @param entry
|
|
||||||
* the entry to check
|
|
||||||
* @return boolean
|
|
||||||
* false if the entry is null, undefined, or its time to
|
|
||||||
* live has been exceeded, true otherwise
|
|
||||||
*/
|
|
||||||
private isValid(entry: ResponseCacheEntry): boolean {
|
|
||||||
if (hasNoValue(entry)) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
const timeOutdated = entry.timeAdded + entry.msToLive;
|
|
||||||
const isOutDated = new Date().getTime() > timeOutdated;
|
|
||||||
if (isOutDated) {
|
|
||||||
this.store.dispatch(new ResponseCacheRemoveAction(entry.key));
|
|
||||||
}
|
|
||||||
return !isOutDated;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -10,10 +10,11 @@ import { MetadataSchema } from '../metadata/metadataschema.model';
|
|||||||
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
|
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
|
||||||
import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model';
|
import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model';
|
||||||
import { AuthStatus } from '../auth/models/auth-status.model';
|
import { AuthStatus } from '../auth/models/auth-status.model';
|
||||||
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
export class RestResponse {
|
export class RestResponse {
|
||||||
public toCache = true;
|
public timeAdded: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public isSuccessful: boolean,
|
public isSuccessful: boolean,
|
82
src/app/core/cache/server-sync-buffer.actions.ts
vendored
Normal file
82
src/app/core/cache/server-sync-buffer.actions.ts
vendored
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { Action } from '@ngrx/store';
|
||||||
|
|
||||||
|
import { type } from '../../shared/ngrx/type';
|
||||||
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of ServerSyncBufferAction type definitions
|
||||||
|
*/
|
||||||
|
export const ServerSyncBufferActionTypes = {
|
||||||
|
ADD: type('dspace/core/cache/syncbuffer/ADD'),
|
||||||
|
COMMIT: type('dspace/core/cache/syncbuffer/COMMIT'),
|
||||||
|
EMPTY: type('dspace/core/cache/syncbuffer/EMPTY'),
|
||||||
|
};
|
||||||
|
|
||||||
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to add a new cached object to the server sync buffer
|
||||||
|
*/
|
||||||
|
export class AddToSSBAction implements Action {
|
||||||
|
type = ServerSyncBufferActionTypes.ADD;
|
||||||
|
payload: {
|
||||||
|
href: string,
|
||||||
|
method: RestRequestMethod
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new AddToSSBAction
|
||||||
|
*
|
||||||
|
* @param href
|
||||||
|
* the unique href of the cached object entry that should be updated
|
||||||
|
*/
|
||||||
|
constructor(href: string, method: RestRequestMethod) {
|
||||||
|
this.payload = { href, method: method };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to commit everything (for a certain method, when specified) in the ServerSyncBuffer to the server
|
||||||
|
*/
|
||||||
|
export class CommitSSBAction implements Action {
|
||||||
|
type = ServerSyncBufferActionTypes.COMMIT;
|
||||||
|
payload?: RestRequestMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new CommitSSBAction
|
||||||
|
*
|
||||||
|
* @param method
|
||||||
|
* an optional method for which the ServerSyncBuffer should send its entries to the server
|
||||||
|
*/
|
||||||
|
constructor(method?: RestRequestMethod) {
|
||||||
|
this.payload = method;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* An ngrx action to remove everything (for a certain method, when specified) from the ServerSyncBuffer to the server
|
||||||
|
*/
|
||||||
|
export class EmptySSBAction implements Action {
|
||||||
|
type = ServerSyncBufferActionTypes.EMPTY;
|
||||||
|
payload?: RestRequestMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new EmptySSBAction
|
||||||
|
*
|
||||||
|
* @param method
|
||||||
|
* an optional method for which the ServerSyncBuffer should remove its entries
|
||||||
|
* if this parameter is omitted, the buffer will be emptied as a whole
|
||||||
|
*/
|
||||||
|
constructor(method?: RestRequestMethod) {
|
||||||
|
this.payload = method;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type to encompass all ServerSyncBufferActions
|
||||||
|
*/
|
||||||
|
export type ServerSyncBufferAction
|
||||||
|
= AddToSSBAction
|
||||||
|
| CommitSSBAction
|
||||||
|
| EmptySSBAction
|
139
src/app/core/cache/server-sync-buffer.effects.spec.ts
vendored
Normal file
139
src/app/core/cache/server-sync-buffer.effects.spec.ts
vendored
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { provideMockActions } from '@ngrx/effects/testing';
|
||||||
|
import { cold, hot } from 'jasmine-marbles';
|
||||||
|
import { ServerSyncBufferEffects } from './server-sync-buffer.effects';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
import {
|
||||||
|
CommitSSBAction,
|
||||||
|
EmptySSBAction,
|
||||||
|
ServerSyncBufferActionTypes
|
||||||
|
} from './server-sync-buffer.actions';
|
||||||
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { ObjectCacheService } from './object-cache.service';
|
||||||
|
import { MockStore } from '../../shared/testing/mock-store';
|
||||||
|
import { ObjectCacheState } from './object-cache.reducer';
|
||||||
|
import * as operators from 'rxjs/operators';
|
||||||
|
import { spyOnOperator } from '../../shared/testing/utils';
|
||||||
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
|
import { ApplyPatchObjectCacheAction } from './object-cache.actions';
|
||||||
|
|
||||||
|
describe('ServerSyncBufferEffects', () => {
|
||||||
|
let ssbEffects: ServerSyncBufferEffects;
|
||||||
|
let actions: Observable<any>;
|
||||||
|
const testConfig = {
|
||||||
|
cache:
|
||||||
|
{
|
||||||
|
autoSync:
|
||||||
|
{
|
||||||
|
timePerMethod: {},
|
||||||
|
defaultTime: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||||
|
let store;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store = new MockStore<ObjectCacheState>({});
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
ServerSyncBufferEffects,
|
||||||
|
provideMockActions(() => actions),
|
||||||
|
{ provide: GLOBAL_CONFIG, useValue: testConfig },
|
||||||
|
{ provide: RequestService, useValue: getMockRequestService() },
|
||||||
|
{
|
||||||
|
provide: ObjectCacheService, useValue: {
|
||||||
|
getBySelfLink: (link) => {
|
||||||
|
const object = new DSpaceObject();
|
||||||
|
object.self = link;
|
||||||
|
return observableOf(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ provide: Store, useValue: store }
|
||||||
|
// other providers
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
ssbEffects = TestBed.get(ServerSyncBufferEffects);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setTimeoutForServerSync', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOnOperator(operators, 'delay').and.returnValue((v) => v);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a COMMIT action in response to an ADD action', () => {
|
||||||
|
actions = hot('a', {
|
||||||
|
a: {
|
||||||
|
type: ServerSyncBufferActionTypes.ADD,
|
||||||
|
payload: { href: selfLink, method: RestRequestMethod.PUT }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const expected = cold('b', { b: new CommitSSBAction(RestRequestMethod.PUT) });
|
||||||
|
|
||||||
|
expect(ssbEffects.setTimeoutForServerSync).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('commitServerSyncBuffer', () => {
|
||||||
|
describe('when the buffer is not empty', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store
|
||||||
|
.subscribe((state) => {
|
||||||
|
(state as any).core = Object({});
|
||||||
|
(state as any).core['cache/syncbuffer'] = {
|
||||||
|
buffer: [{
|
||||||
|
href: selfLink,
|
||||||
|
method: RestRequestMethod.PATCH
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should return a list of actions in response to a COMMIT action', () => {
|
||||||
|
actions = hot('a', {
|
||||||
|
a: {
|
||||||
|
type: ServerSyncBufferActionTypes.COMMIT,
|
||||||
|
payload: RestRequestMethod.PATCH
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const expected = cold('(bc)', {
|
||||||
|
b: new ApplyPatchObjectCacheAction(selfLink),
|
||||||
|
c: new EmptySSBAction(RestRequestMethod.PATCH)
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ssbEffects.commitServerSyncBuffer).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the buffer is empty', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store
|
||||||
|
.subscribe((state) => {
|
||||||
|
(state as any).core = Object({});
|
||||||
|
(state as any).core['cache/syncbuffer'] = {
|
||||||
|
buffer: []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should return a placeholder action in response to a COMMIT action', () => {
|
||||||
|
store.subscribe();
|
||||||
|
actions = hot('a', {
|
||||||
|
a: {
|
||||||
|
type: ServerSyncBufferActionTypes.COMMIT,
|
||||||
|
payload: { method: RestRequestMethod.PATCH }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const expected = cold('b', { b: { type: 'NO_ACTION' } });
|
||||||
|
|
||||||
|
expect(ssbEffects.commitServerSyncBuffer).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
122
src/app/core/cache/server-sync-buffer.effects.ts
vendored
Normal file
122
src/app/core/cache/server-sync-buffer.effects.ts
vendored
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { delay, exhaustMap, map, switchMap, take } from 'rxjs/operators';
|
||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||||
|
import {
|
||||||
|
AddToSSBAction,
|
||||||
|
CommitSSBAction,
|
||||||
|
EmptySSBAction,
|
||||||
|
ServerSyncBufferActionTypes
|
||||||
|
} from './server-sync-buffer.actions';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
import { coreSelector, CoreState } from '../core.reducers';
|
||||||
|
import { Action, createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||||
|
import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer';
|
||||||
|
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { PutRequest } from '../data/request.models';
|
||||||
|
import { ObjectCacheService } from './object-cache.service';
|
||||||
|
import { ApplyPatchObjectCacheAction } from './object-cache.actions';
|
||||||
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
|
import { hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ServerSyncBufferEffects {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When an ADDToSSBAction is dispatched
|
||||||
|
* Set a time out (configurable per method type)
|
||||||
|
* Then dispatch a CommitSSBAction
|
||||||
|
* When the delay is running, no new AddToSSBActions are processed in this effect
|
||||||
|
*/
|
||||||
|
@Effect() setTimeoutForServerSync = this.actions$
|
||||||
|
.pipe(
|
||||||
|
ofType(ServerSyncBufferActionTypes.ADD),
|
||||||
|
exhaustMap((action: AddToSSBAction) => {
|
||||||
|
const autoSyncConfig = this.EnvConfig.cache.autoSync;
|
||||||
|
const timeoutInSeconds = autoSyncConfig.timePerMethod[action.payload.method] || autoSyncConfig.defaultTime;
|
||||||
|
return observableOf(new CommitSSBAction(action.payload.method)).pipe(
|
||||||
|
delay(timeoutInSeconds * 1000),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a CommitSSBAction is dispatched
|
||||||
|
* Create a list of actions for each entry in the current buffer state to be dispatched
|
||||||
|
* When the list of actions is not empty, also dispatch an EmptySSBAction
|
||||||
|
* When the list is empty dispatch a NO_ACTION placeholder action
|
||||||
|
*/
|
||||||
|
@Effect() commitServerSyncBuffer = this.actions$
|
||||||
|
.pipe(
|
||||||
|
ofType(ServerSyncBufferActionTypes.COMMIT),
|
||||||
|
switchMap((action: CommitSSBAction) => {
|
||||||
|
return this.store.pipe(
|
||||||
|
select(serverSyncBufferSelector()),
|
||||||
|
take(1), /* necessary, otherwise delay will not have any effect after the first run */
|
||||||
|
switchMap((bufferState: ServerSyncBufferState) => {
|
||||||
|
const actions: Array<Observable<Action>> = bufferState.buffer
|
||||||
|
.filter((entry: ServerSyncBufferEntry) => {
|
||||||
|
/* If there's a request method, filter
|
||||||
|
If there's no filter, commit everything */
|
||||||
|
if (hasValue(action.payload)) {
|
||||||
|
return entry.method === action.payload;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((entry: ServerSyncBufferEntry) => {
|
||||||
|
if (entry.method === RestRequestMethod.PATCH) {
|
||||||
|
return this.applyPatch(entry.href);
|
||||||
|
} else {
|
||||||
|
/* TODO implement for other request method types */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Add extra action to array, to make sure the ServerSyncBuffer is emptied afterwards */
|
||||||
|
if (isNotEmpty(actions) && isNotUndefined(actions[0])) {
|
||||||
|
return observableCombineLatest(...actions).pipe(
|
||||||
|
switchMap((array) => [...array, new EmptySSBAction(action.payload)])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return observableOf({ type: 'NO_ACTION' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* private method to create an ApplyPatchObjectCacheAction based on a cache entry
|
||||||
|
* and to do the actual patch request to the server
|
||||||
|
* @param {string} href The self link of the cache entry
|
||||||
|
* @returns {Observable<Action>} ApplyPatchObjectCacheAction to be dispatched
|
||||||
|
*/
|
||||||
|
private applyPatch(href: string): Observable<Action> {
|
||||||
|
const patchObject = this.objectCache.getBySelfLink(href).pipe(take(1));
|
||||||
|
|
||||||
|
return patchObject.pipe(
|
||||||
|
map((object) => {
|
||||||
|
const serializedObject = new DSpaceRESTv2Serializer(object.constructor as GenericConstructor<{}>).serialize(object);
|
||||||
|
|
||||||
|
this.requestService.configure(new PutRequest(this.requestService.generateRequestId(), href, serializedObject));
|
||||||
|
|
||||||
|
return new ApplyPatchObjectCacheAction(href)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private actions$: Actions,
|
||||||
|
private store: Store<CoreState>,
|
||||||
|
private requestService: RequestService,
|
||||||
|
private objectCache: ObjectCacheService,
|
||||||
|
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serverSyncBufferSelector(): MemoizedSelector<CoreState, ServerSyncBufferState> {
|
||||||
|
return createSelector(coreSelector, (state: CoreState) => state['cache/syncbuffer']);
|
||||||
|
}
|
85
src/app/core/cache/server-sync-buffer.reducer.spec.ts
vendored
Normal file
85
src/app/core/cache/server-sync-buffer.reducer.spec.ts
vendored
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import * as deepFreeze from 'deep-freeze';
|
||||||
|
import { RemoveFromObjectCacheAction } from './object-cache.actions';
|
||||||
|
import { serverSyncBufferReducer } from './server-sync-buffer.reducer';
|
||||||
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
import { AddToSSBAction, EmptySSBAction } from './server-sync-buffer.actions';
|
||||||
|
|
||||||
|
class NullAction extends RemoveFromObjectCacheAction {
|
||||||
|
type = null;
|
||||||
|
payload = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('serverSyncBufferReducer', () => {
|
||||||
|
const selfLink1 = 'https://localhost:8080/api/core/items/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||||
|
const selfLink2 = 'https://localhost:8080/api/core/items/28b04544-1766-4e82-9728-c4e93544ecd3';
|
||||||
|
const testState = {
|
||||||
|
buffer:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
href: selfLink1,
|
||||||
|
method: RestRequestMethod.PATCH,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: selfLink2,
|
||||||
|
method: RestRequestMethod.GET,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const newSelfLink = 'https://localhost:8080/api/core/items/1ce6b5ae-97e1-4e5a-b4b0-f9029bad10c0';
|
||||||
|
|
||||||
|
deepFreeze(testState);
|
||||||
|
|
||||||
|
it('should return the current state when no valid actions have been made', () => {
|
||||||
|
const action = new NullAction();
|
||||||
|
const newState = serverSyncBufferReducer(testState, action);
|
||||||
|
|
||||||
|
expect(newState).toEqual(testState);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start with an empty buffer array', () => {
|
||||||
|
const action = new NullAction();
|
||||||
|
const initialState = serverSyncBufferReducer(undefined, action);
|
||||||
|
|
||||||
|
expect(initialState).toEqual({ buffer: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should perform the ADD action without affecting the previous state', () => {
|
||||||
|
const action = new AddToSSBAction(selfLink1, RestRequestMethod.POST);
|
||||||
|
// testState has already been frozen above
|
||||||
|
serverSyncBufferReducer(testState, action);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should perform the EMPTY action without affecting the previous state', () => {
|
||||||
|
const action = new EmptySSBAction();
|
||||||
|
// testState has already been frozen above
|
||||||
|
serverSyncBufferReducer(testState, action);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should empty the buffer if the EmptySSBAction is dispatched without a payload', () => {
|
||||||
|
const action = new EmptySSBAction();
|
||||||
|
// testState has already been frozen above
|
||||||
|
const emptyState = serverSyncBufferReducer(testState, action);
|
||||||
|
expect(emptyState).toEqual({ buffer: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should empty the buffer partially if the EmptySSBAction is dispatched with a payload', () => {
|
||||||
|
const action = new EmptySSBAction(RestRequestMethod.PATCH);
|
||||||
|
// testState has already been frozen above
|
||||||
|
const emptyState = serverSyncBufferReducer(testState, action);
|
||||||
|
expect(emptyState).toEqual({ buffer: testState.buffer.filter((entry) => entry.method !== RestRequestMethod.PATCH) });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add an entry to the buffer if the AddSSBAction is dispatched', () => {
|
||||||
|
const action = new AddToSSBAction(newSelfLink, RestRequestMethod.PUT);
|
||||||
|
// testState has already been frozen above
|
||||||
|
const newState = serverSyncBufferReducer(testState, action);
|
||||||
|
expect(newState.buffer).toContain({
|
||||||
|
href: newSelfLink, method: RestRequestMethod.PUT
|
||||||
|
})
|
||||||
|
;
|
||||||
|
})
|
||||||
|
});
|
92
src/app/core/cache/server-sync-buffer.reducer.ts
vendored
Normal file
92
src/app/core/cache/server-sync-buffer.reducer.ts
vendored
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { hasNoValue, hasValue } from '../../shared/empty.util';
|
||||||
|
import {
|
||||||
|
AddToSSBAction,
|
||||||
|
EmptySSBAction,
|
||||||
|
ServerSyncBufferAction,
|
||||||
|
ServerSyncBufferActionTypes
|
||||||
|
} from './server-sync-buffer.actions';
|
||||||
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An entry in the ServerSyncBufferState
|
||||||
|
* href: unique href of an ObjectCacheEntry
|
||||||
|
* method: RestRequestMethod type
|
||||||
|
*/
|
||||||
|
export class ServerSyncBufferEntry {
|
||||||
|
href: string;
|
||||||
|
method: RestRequestMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ServerSyncBuffer State
|
||||||
|
*
|
||||||
|
* Consists list of ServerSyncBufferState
|
||||||
|
*/
|
||||||
|
export interface ServerSyncBufferState {
|
||||||
|
buffer: ServerSyncBufferEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
||||||
|
const initialState: ServerSyncBufferState = { buffer: [] };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ServerSyncBuffer Reducer
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* the current state
|
||||||
|
* @param action
|
||||||
|
* the action to perform on the state
|
||||||
|
* @return ServerSyncBufferState
|
||||||
|
* the new state
|
||||||
|
*/
|
||||||
|
export function serverSyncBufferReducer(state = initialState, action: ServerSyncBufferAction): ServerSyncBufferState {
|
||||||
|
switch (action.type) {
|
||||||
|
|
||||||
|
case ServerSyncBufferActionTypes.ADD: {
|
||||||
|
return addToServerSyncQueue(state, action as AddToSSBAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
case ServerSyncBufferActionTypes.EMPTY: {
|
||||||
|
return emptyServerSyncQueue(state, action as EmptySSBAction);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new entry to the buffer with a specified method
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* the current state
|
||||||
|
* @param action
|
||||||
|
* an AddToSSBAction
|
||||||
|
* @return ServerSyncBufferState
|
||||||
|
* the new state, with a new entry added to the buffer
|
||||||
|
*/
|
||||||
|
function addToServerSyncQueue(state: ServerSyncBufferState, action: AddToSSBAction): ServerSyncBufferState {
|
||||||
|
const actionEntry = action.payload as ServerSyncBufferEntry;
|
||||||
|
if (hasNoValue(state.buffer.find((entry) => entry.href === actionEntry.href && entry.method === actionEntry.method))) {
|
||||||
|
return Object.assign({}, state, { buffer: state.buffer.concat(actionEntry) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all ServerSyncBuffers entry from the buffer with a specified method
|
||||||
|
* If no method is specified, empty the whole buffer
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* the current state
|
||||||
|
* @param action
|
||||||
|
* an AddToSSBAction
|
||||||
|
* @return ServerSyncBufferState
|
||||||
|
* the new state, with a new entry added to the buffer
|
||||||
|
*/
|
||||||
|
function emptyServerSyncQueue(state: ServerSyncBufferState, action: EmptySSBAction): ServerSyncBufferState {
|
||||||
|
let newBuffer = [];
|
||||||
|
if (hasValue(action.payload)) {
|
||||||
|
newBuffer = state.buffer.filter((entry) => entry.method !== action.payload);
|
||||||
|
}
|
||||||
|
return Object.assign({}, state, { buffer: newBuffer });
|
||||||
|
}
|
@@ -1,7 +1,6 @@
|
|||||||
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
|
||||||
import { ConfigService } from './config.service';
|
import { ConfigService } from './config.service';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { ConfigRequest, FindAllOptions } from '../data/request.models';
|
import { ConfigRequest, FindAllOptions } from '../data/request.models';
|
||||||
@@ -16,7 +15,6 @@ class TestService extends ConfigService {
|
|||||||
protected browseEndpoint = BROWSE;
|
protected browseEndpoint = BROWSE;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected halService: HALEndpointService) {
|
protected halService: HALEndpointService) {
|
||||||
super();
|
super();
|
||||||
@@ -26,7 +24,6 @@ class TestService extends ConfigService {
|
|||||||
describe('ConfigService', () => {
|
describe('ConfigService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
let service: TestService;
|
let service: TestService;
|
||||||
let responseCache: ResponseCacheService;
|
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
let halService: any;
|
let halService: any;
|
||||||
|
|
||||||
@@ -39,17 +36,8 @@ describe('ConfigService', () => {
|
|||||||
const scopedEndpoint = `${serviceEndpoint}/${scopeName}`;
|
const scopedEndpoint = `${serviceEndpoint}/${scopeName}`;
|
||||||
const searchEndpoint = `${serviceEndpoint}/${BROWSE}?uuid=${scopeID}`;
|
const searchEndpoint = `${serviceEndpoint}/${BROWSE}?uuid=${scopeID}`;
|
||||||
|
|
||||||
function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService {
|
|
||||||
return jasmine.createSpyObj('responseCache', {
|
|
||||||
get: cold('c-', {
|
|
||||||
c: { response: { isSuccessful } }
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function initTestService(): TestService {
|
function initTestService(): TestService {
|
||||||
return new TestService(
|
return new TestService(
|
||||||
responseCache,
|
|
||||||
requestService,
|
requestService,
|
||||||
halService
|
halService
|
||||||
);
|
);
|
||||||
@@ -57,7 +45,6 @@ describe('ConfigService', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
scheduler = getTestScheduler();
|
scheduler = getTestScheduler();
|
||||||
responseCache = initMockResponseCacheService(true);
|
|
||||||
requestService = getMockRequestService();
|
requestService = getMockRequestService();
|
||||||
halService = new HALEndpointServiceStub(configEndpoint);
|
halService = new HALEndpointServiceStub(configEndpoint);
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
|
@@ -1,24 +1,25 @@
|
|||||||
import { Observable, of as observableOf, throwError as observableThrowError, merge as observableMerge } from 'rxjs';
|
import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
import { ConfigSuccessResponse } from '../cache/response.models';
|
||||||
import { ConfigSuccessResponse } from '../cache/response-cache.models';
|
|
||||||
import { ConfigRequest, FindAllOptions, RestRequest } from '../data/request.models';
|
import { ConfigRequest, FindAllOptions, RestRequest } from '../data/request.models';
|
||||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { ConfigData } from './config-data';
|
import { ConfigData } from './config-data';
|
||||||
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
|
import { getResponseFromEntry } from '../shared/operators';
|
||||||
|
|
||||||
export abstract class ConfigService {
|
export abstract class ConfigService {
|
||||||
protected request: ConfigRequest;
|
protected request: ConfigRequest;
|
||||||
protected abstract responseCache: ResponseCacheService;
|
|
||||||
protected abstract requestService: RequestService;
|
protected abstract requestService: RequestService;
|
||||||
protected abstract linkPath: string;
|
protected abstract linkPath: string;
|
||||||
protected abstract browseEndpoint: string;
|
protected abstract browseEndpoint: string;
|
||||||
protected abstract halService: HALEndpointService;
|
protected abstract halService: HALEndpointService;
|
||||||
|
|
||||||
protected getConfig(request: RestRequest): Observable<ConfigData> {
|
protected getConfig(request: RestRequest): Observable<ConfigData> {
|
||||||
const responses = this.responseCache.get(request.href).pipe(map((entry: ResponseCacheEntry) => entry.response));
|
const responses = this.requestService.getByHref(request.href).pipe(
|
||||||
|
getResponseFromEntry()
|
||||||
|
);
|
||||||
const errorResponses = responses.pipe(
|
const errorResponses = responses.pipe(
|
||||||
filter((response) => !response.isSuccessful),
|
filter((response) => !response.isSuccessful),
|
||||||
mergeMap(() => observableThrowError(new Error(`Couldn't retrieve the config`)))
|
mergeMap(() => observableThrowError(new Error(`Couldn't retrieve the config`)))
|
||||||
@@ -94,7 +95,6 @@ export abstract class ConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getConfigBySearch(options: FindAllOptions = {}): Observable<ConfigData> {
|
public getConfigBySearch(options: FindAllOptions = {}): Observable<ConfigData> {
|
||||||
console.log(this.halService.getEndpoint(this.linkPath));
|
|
||||||
return this.halService.getEndpoint(this.linkPath).pipe(
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
map((endpoint: string) => this.getConfigSearchHref(endpoint, options)),
|
map((endpoint: string) => this.getConfigSearchHref(endpoint, options)),
|
||||||
filter((href: string) => isNotEmpty(href)),
|
filter((href: string) => isNotEmpty(href)),
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { ConfigService } from './config.service';
|
import { ConfigService } from './config.service';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
|
||||||
@@ -11,7 +10,6 @@ export class SubmissionDefinitionsConfigService extends ConfigService {
|
|||||||
protected browseEndpoint = 'search/findByCollection';
|
protected browseEndpoint = 'search/findByCollection';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected halService: HALEndpointService) {
|
protected halService: HALEndpointService) {
|
||||||
super();
|
super();
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { ConfigService } from './config.service';
|
import { ConfigService } from './config.service';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
|
||||||
@@ -11,7 +10,6 @@ export class SubmissionFormsConfigService extends ConfigService {
|
|||||||
protected browseEndpoint = '';
|
protected browseEndpoint = '';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected halService: HALEndpointService) {
|
protected halService: HALEndpointService) {
|
||||||
super();
|
super();
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { ConfigService } from './config.service';
|
import { ConfigService } from './config.service';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
|
||||||
@@ -11,7 +10,6 @@ export class SubmissionSectionsConfigService extends ConfigService {
|
|||||||
protected browseEndpoint = '';
|
protected browseEndpoint = '';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected halService: HALEndpointService) {
|
protected halService: HALEndpointService) {
|
||||||
super();
|
super();
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
|
|
||||||
import { ObjectCacheEffects } from './cache/object-cache.effects';
|
import { ObjectCacheEffects } from './cache/object-cache.effects';
|
||||||
import { ResponseCacheEffects } from './cache/response-cache.effects';
|
|
||||||
import { UUIDIndexEffects } from './index/index.effects';
|
import { UUIDIndexEffects } from './index/index.effects';
|
||||||
import { RequestEffects } from './data/request.effects';
|
import { RequestEffects } from './data/request.effects';
|
||||||
import { AuthEffects } from './auth/auth.effects';
|
import { AuthEffects } from './auth/auth.effects';
|
||||||
|
import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects';
|
||||||
|
|
||||||
export const coreEffects = [
|
export const coreEffects = [
|
||||||
ResponseCacheEffects,
|
|
||||||
RequestEffects,
|
RequestEffects,
|
||||||
ObjectCacheEffects,
|
ObjectCacheEffects,
|
||||||
UUIDIndexEffects,
|
UUIDIndexEffects,
|
||||||
AuthEffects
|
AuthEffects,
|
||||||
|
ServerSyncBufferEffects
|
||||||
];
|
];
|
||||||
|
@@ -32,7 +32,6 @@ import { ObjectCacheService } from './cache/object-cache.service';
|
|||||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||||
import { RemoteDataBuildService } from './cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from './cache/builders/remote-data-build.service';
|
||||||
import { RequestService } from './data/request.service';
|
import { RequestService } from './data/request.service';
|
||||||
import { ResponseCacheService } from './cache/response-cache.service';
|
|
||||||
import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service';
|
import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service';
|
||||||
import { ServerResponseService } from '../shared/services/server-response.service';
|
import { ServerResponseService } from '../shared/services/server-response.service';
|
||||||
import { NativeWindowFactory, NativeWindowService } from '../shared/services/window.service';
|
import { NativeWindowFactory, NativeWindowService } from '../shared/services/window.service';
|
||||||
@@ -66,6 +65,8 @@ import { BrowseItemsResponseParsingService } from './data/browse-items-response-
|
|||||||
import { DSpaceObjectDataService } from './data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from './data/dspace-object-data.service';
|
||||||
import { CSSVariableService } from '../shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from '../shared/sass-helper/sass-helper.service';
|
||||||
import { MenuService } from '../shared/menu/menu.service';
|
import { MenuService } from '../shared/menu/menu.service';
|
||||||
|
import { NormalizedObjectBuildService } from './cache/builders/normalized-object-build.service';
|
||||||
|
import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service';
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -102,9 +103,9 @@ const PROVIDERS = [
|
|||||||
ObjectCacheService,
|
ObjectCacheService,
|
||||||
PaginationComponentOptions,
|
PaginationComponentOptions,
|
||||||
RegistryService,
|
RegistryService,
|
||||||
|
NormalizedObjectBuildService,
|
||||||
RemoteDataBuildService,
|
RemoteDataBuildService,
|
||||||
RequestService,
|
RequestService,
|
||||||
ResponseCacheService,
|
|
||||||
EndpointMapResponseParsingService,
|
EndpointMapResponseParsingService,
|
||||||
FacetValueResponseParsingService,
|
FacetValueResponseParsingService,
|
||||||
FacetValueMapResponseParsingService,
|
FacetValueMapResponseParsingService,
|
||||||
@@ -130,6 +131,7 @@ const PROVIDERS = [
|
|||||||
UploaderService,
|
UploaderService,
|
||||||
UUIDService,
|
UUIDService,
|
||||||
DSpaceObjectDataService,
|
DSpaceObjectDataService,
|
||||||
|
DSOChangeAnalyzer,
|
||||||
CSSVariableService,
|
CSSVariableService,
|
||||||
MenuService,
|
MenuService,
|
||||||
// register AuthInterceptor as HttpInterceptor
|
// register AuthInterceptor as HttpInterceptor
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user