From 819610476ef91fcee5748d3f49788ce79e2c86c6 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 20 Jul 2018 16:04:02 +0200 Subject: [PATCH 01/69] 54472: Intermediate commit --- .../create-community-page.component.html | 0 .../create-community-page.component.ts | 9 +++++++++ 2 files changed, 9 insertions(+) create mode 100644 src/app/+community-page/create-community-page/create-community-page.component.html create mode 100644 src/app/+community-page/create-community-page/create-community-page.component.ts diff --git a/src/app/+community-page/create-community-page/create-community-page.component.html b/src/app/+community-page/create-community-page/create-community-page.component.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts new file mode 100644 index 0000000000..ffc6642cdf --- /dev/null +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ds-create-community', + templateUrl: './create-community-page.component.html' +}) +export class CreateCommunityPageComponent { + +} From 0da645931cf5eea983abd805c5547189d10f0312 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 23 Jul 2018 14:29:32 +0200 Subject: [PATCH 02/69] 54472: Create Community page --- resources/i18n/en.json | 15 ++++++++++ .../community-form.component.html | 27 +++++++++++++++++ .../community-form.component.scss | 7 +++++ .../community-form.component.ts | 30 +++++++++++++++++++ .../community-page-routing.module.ts | 2 ++ .../+community-page/community-page.module.ts | 4 +++ .../create-community-page.component.html | 8 +++++ .../create-community-page.component.scss | 1 + .../create-community-page.component.ts | 5 ++++ src/app/core/comcol/comcol.service.ts | 0 10 files changed, 99 insertions(+) create mode 100644 src/app/+community-page/community-form/community-form.component.html create mode 100644 src/app/+community-page/community-form/community-form.component.scss create mode 100644 src/app/+community-page/community-form/community-form.component.ts create mode 100644 src/app/+community-page/create-community-page/create-community-page.component.scss create mode 100644 src/app/core/comcol/comcol.service.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index ba70b87e12..c0a213fe1b 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -22,6 +22,21 @@ }, "sub-collection-list": { "head": "Collections of this Community" + }, + "edit": { + "name": "Name", + "description": "Short Description", + "introductory": "Introductory text (HTML)", + "copyright": "Copyright text (HTML)", + "news": "News (HTML)", + "submit": "Submit", + "cancel": "Cancel", + "required": { + "name": "Please enter a community name" + } + }, + "create": { + "head": "Create a Community" } }, "item": { diff --git a/src/app/+community-page/community-form/community-form.component.html b/src/app/+community-page/community-form/community-form.component.html new file mode 100644 index 0000000000..bc45b582ac --- /dev/null +++ b/src/app/+community-page/community-form/community-form.component.html @@ -0,0 +1,27 @@ +
+
+ + +
{{ 'community.edit.required.name' | translate }}
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
diff --git a/src/app/+community-page/community-form/community-form.component.scss b/src/app/+community-page/community-form/community-form.component.scss new file mode 100644 index 0000000000..d5811186e7 --- /dev/null +++ b/src/app/+community-page/community-form/community-form.component.scss @@ -0,0 +1,7 @@ +@import '../../../styles/variables.scss'; + +// temporary fix for bootstrap 4 beta btn color issue +.btn-secondary { + background-color: $input-bg; + color: $input-color; +} diff --git a/src/app/+community-page/community-form/community-form.component.ts b/src/app/+community-page/community-form/community-form.component.ts new file mode 100644 index 0000000000..64d08ab862 --- /dev/null +++ b/src/app/+community-page/community-form/community-form.component.ts @@ -0,0 +1,30 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { isNotEmpty } from '../../shared/empty.util'; + +@Component({ + selector: 'ds-community-form', + styleUrls: ['./community-form.component.scss'], + templateUrl: './community-form.component.html' +}) +export class CommunityFormComponent { + + name: string; + description: string; + introductory: string; + copyright: string; + news: string; + + nameRequiredError = false; + + @Output() submitted: EventEmitter = new EventEmitter(); + + onSubmit(data: any) { + if (isNotEmpty(data.name)) { + this.submitted.emit(data); + this.nameRequiredError = false; + } else { + this.nameRequiredError = true; + } + } + +} diff --git a/src/app/+community-page/community-page-routing.module.ts b/src/app/+community-page/community-page-routing.module.ts index 6fd5cc8cb5..249b01ea18 100644 --- a/src/app/+community-page/community-page-routing.module.ts +++ b/src/app/+community-page/community-page-routing.module.ts @@ -2,10 +2,12 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { CommunityPageComponent } from './community-page.component'; +import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; @NgModule({ imports: [ RouterModule.forChild([ + { path: 'create', component: CreateCommunityPageComponent }, { path: ':id', component: CommunityPageComponent, pathMatch: 'full' } ]) ] diff --git a/src/app/+community-page/community-page.module.ts b/src/app/+community-page/community-page.module.ts index e00c3910c5..292e6aaf9c 100644 --- a/src/app/+community-page/community-page.module.ts +++ b/src/app/+community-page/community-page.module.ts @@ -6,6 +6,8 @@ import { SharedModule } from '../shared/shared.module'; import { CommunityPageComponent } from './community-page.component'; import { CommunityPageSubCollectionListComponent } from './sub-collection-list/community-page-sub-collection-list.component'; import { CommunityPageRoutingModule } from './community-page-routing.module'; +import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; +import { CommunityFormComponent } from './community-form/community-form.component'; @NgModule({ imports: [ @@ -16,6 +18,8 @@ import { CommunityPageRoutingModule } from './community-page-routing.module'; declarations: [ CommunityPageComponent, CommunityPageSubCollectionListComponent, + CreateCommunityPageComponent, + CommunityFormComponent ] }) export class CommunityPageModule { diff --git a/src/app/+community-page/create-community-page/create-community-page.component.html b/src/app/+community-page/create-community-page/create-community-page.component.html index e69de29bb2..ea270db92b 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.html +++ b/src/app/+community-page/create-community-page/create-community-page.component.html @@ -0,0 +1,8 @@ +
+
+
+ +
+
+ +
diff --git a/src/app/+community-page/create-community-page/create-community-page.component.scss b/src/app/+community-page/create-community-page/create-community-page.component.scss new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/app/+community-page/create-community-page/create-community-page.component.scss @@ -0,0 +1 @@ + diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index ffc6642cdf..db2e4f25fa 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -2,8 +2,13 @@ import { Component } from '@angular/core'; @Component({ selector: 'ds-create-community', + styleUrls: ['./create-community-page.component.scss'], templateUrl: './create-community-page.component.html' }) export class CreateCommunityPageComponent { + onSubmit(data: any) { + console.log('yay, made it with name: ' + data.name); + } + } diff --git a/src/app/core/comcol/comcol.service.ts b/src/app/core/comcol/comcol.service.ts new file mode 100644 index 0000000000..e69de29bb2 From aa73e874a57ec590718ef137c8aa15360960410b Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 8 Aug 2018 13:49:34 +0200 Subject: [PATCH 03/69] 54472: Create method in comcol-data-service intermediate commit --- .../create-community-page.component.ts | 5 ++++- src/app/core/comcol/comcol.service.ts | 0 src/app/core/data/comcol-data.service.ts | 19 +++++++++++++++++-- 3 files changed, 21 insertions(+), 3 deletions(-) delete mode 100644 src/app/core/comcol/comcol.service.ts diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index db2e4f25fa..b49ea1bbfa 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; +import { Community } from '../../core/shared/community.model'; @Component({ selector: 'ds-create-community', @@ -8,7 +9,9 @@ import { Component } from '@angular/core'; export class CreateCommunityPageComponent { onSubmit(data: any) { - console.log('yay, made it with name: ' + data.name); + Object.assign(new Community(), { + // TODO: Create community object to add to rest + }); } } diff --git a/src/app/core/comcol/comcol.service.ts b/src/app/core/comcol/comcol.service.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 112afa0bc8..5b9f10b957 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -1,5 +1,5 @@ import { Observable } from 'rxjs/Observable'; -import { isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { isEmpty, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { NormalizedCommunity } from '../cache/models/normalized-community.model'; import { CacheableObject } from '../cache/object-cache.reducer'; import { ObjectCacheService } from '../cache/object-cache.service'; @@ -8,9 +8,14 @@ import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { CommunityDataService } from './community-data.service'; import { DataService } from './data.service'; -import { FindByIDRequest } from './request.models'; +import { FindByIDRequest, PutRequest } from './request.models'; import { NormalizedObject } from '../cache/models/normalized-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { Community } from '../shared/community.model'; +import { Collection } from '../shared/collection.model'; +import { distinctUntilChanged, map } from 'rxjs/operators'; +import { configureRequest } from '../shared/operators'; export abstract class ComColDataService extends DataService { protected abstract cds: CommunityDataService; @@ -56,4 +61,14 @@ export abstract class ComColDataService new PutRequest(this.requestService.generateRequestId(), endpointURL, comcol)), + configureRequest(this.requestService) + ); + } + } From 79a3788b3bbe03fd656909ee5612c57189d006ca Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 10 Aug 2018 15:05:22 +0200 Subject: [PATCH 04/69] 54472: Intermediate commit --- .../create-community-page.component.ts | 12 ++++++++++-- src/app/core/data/collection-data.service.ts | 9 ++++++++- src/app/core/data/comcol-data.service.ts | 17 +++++++++++++++-- src/app/core/data/community-data.service.ts | 8 +++++++- 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index b49ea1bbfa..255d88c21b 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -1,5 +1,8 @@ import { Component } from '@angular/core'; import { Community } from '../../core/shared/community.model'; +import { ComColDataService } from '../../core/data/comcol-data.service'; +import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model'; +import { CommunityDataService } from '../../core/data/community-data.service'; @Component({ selector: 'ds-create-community', @@ -8,10 +11,15 @@ import { Community } from '../../core/shared/community.model'; }) export class CreateCommunityPageComponent { + public constructor(private communityDataService: CommunityDataService) { + + } + onSubmit(data: any) { - Object.assign(new Community(), { - // TODO: Create community object to add to rest + const community = Object.assign(new Community(), { + name: data.name }); + this.communityDataService.create(community); } } diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 7d1e463dbe..6e06a600cf 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -11,6 +11,8 @@ import { ComColDataService } from './comcol-data.service'; import { CommunityDataService } from './community-data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { AuthService } from '../auth/auth.service'; +import { Community } from '../shared/community.model'; @Injectable() export class CollectionDataService extends ComColDataService { @@ -23,8 +25,13 @@ export class CollectionDataService extends ComColDataService, protected cds: CommunityDataService, protected objectCache: ObjectCacheService, - protected halService: HALEndpointService + protected halService: HALEndpointService, + protected authService: AuthService ) { super(); } + + getName(collection: Collection) { + return collection.name; + } } diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 5b9f10b957..8c912b4353 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -8,7 +8,7 @@ import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { CommunityDataService } from './community-data.service'; import { DataService } from './data.service'; -import { FindByIDRequest, PutRequest } from './request.models'; +import { FindByIDRequest, PostRequest, PutRequest } from './request.models'; import { NormalizedObject } from '../cache/models/normalized-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { DSpaceObject } from '../shared/dspace-object.model'; @@ -16,11 +16,15 @@ import { Community } from '../shared/community.model'; import { Collection } from '../shared/collection.model'; import { distinctUntilChanged, map } from 'rxjs/operators'; import { configureRequest } from '../shared/operators'; +import { AuthService } from '../auth/auth.service'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { HttpHeaders } from '@angular/common/http'; export abstract class ComColDataService extends DataService { protected abstract cds: CommunityDataService; protected abstract objectCache: ObjectCacheService; protected abstract halService: HALEndpointService; + protected abstract authService: AuthService; /** * Get the scoped endpoint URL by fetching the object with @@ -66,9 +70,18 @@ export abstract class ComColDataService new PutRequest(this.requestService.generateRequestId(), endpointURL, comcol)), + map((endpointURL: string) => { + const options: HttpOptions = Object.create({}); + const headers = new HttpHeaders(); + headers.append('Authentication', this.authService.buildAuthHeader()); + options.headers = headers; + console.log(options); + return new PostRequest(this.requestService.generateRequestId(), endpointURL + '?name=' + this.getName(comcol), options); + }), configureRequest(this.requestService) ); } + abstract getName(comcol: TDomain): string; + } diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index 88ad3a5287..e02c9bc033 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -11,6 +11,7 @@ import { Community } from '../shared/community.model'; import { ComColDataService } from './comcol-data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { AuthService } from '../auth/auth.service'; @Injectable() export class CommunityDataService extends ComColDataService { @@ -23,7 +24,8 @@ export class CommunityDataService extends ComColDataService, protected objectCache: ObjectCacheService, - protected halService: HALEndpointService + protected halService: HALEndpointService, + protected authService: AuthService ) { super(); } @@ -31,4 +33,8 @@ export class CommunityDataService extends ComColDataService Date: Fri, 10 Aug 2018 16:52:13 +0200 Subject: [PATCH 05/69] 54472: Post request on submit and collection create page --- resources/i18n/en.json | 17 +++++++++ .../collection-form.component.html | 35 +++++++++++++++++++ .../collection-form.component.scss | 7 ++++ .../collection-form.component.ts | 32 +++++++++++++++++ .../collection-page-routing.module.ts | 2 ++ .../collection-page.module.ts | 4 +++ .../create-collection-page.component.html | 8 +++++ .../create-collection-page.component.scss | 1 + .../create-collection-page.component.ts | 27 ++++++++++++++ src/app/core/data/collection-data.service.ts | 5 +-- src/app/core/data/comcol-data.service.ts | 7 ++-- src/app/core/data/community-data.service.ts | 5 +-- 12 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 src/app/+collection-page/collection-form/collection-form.component.html create mode 100644 src/app/+collection-page/collection-form/collection-form.component.scss create mode 100644 src/app/+collection-page/collection-form/collection-form.component.ts create mode 100644 src/app/+collection-page/create-collection-page/create-collection-page.component.html create mode 100644 src/app/+collection-page/create-collection-page/create-collection-page.component.scss create mode 100644 src/app/+collection-page/create-collection-page/create-collection-page.component.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index c0a213fe1b..963dfae80f 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -13,6 +13,23 @@ "head": "Recent Submissions" } } + }, + "edit": { + "name": "Name", + "description": "Short Description", + "introductory": "Introductory text (HTML)", + "copyright": "Copyright text (HTML)", + "news": "News (HTML)", + "license": "License", + "provenance": "Provenance", + "submit": "Submit", + "cancel": "Cancel", + "required": { + "name": "Please enter a collection name" + } + }, + "create": { + "head": "Create a Collection" } }, "community": { diff --git a/src/app/+collection-page/collection-form/collection-form.component.html b/src/app/+collection-page/collection-form/collection-form.component.html new file mode 100644 index 0000000000..307d7406ed --- /dev/null +++ b/src/app/+collection-page/collection-form/collection-form.component.html @@ -0,0 +1,35 @@ +
+
+ + +
{{ 'collection.edit.required.name' | translate }}
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
diff --git a/src/app/+collection-page/collection-form/collection-form.component.scss b/src/app/+collection-page/collection-form/collection-form.component.scss new file mode 100644 index 0000000000..d5811186e7 --- /dev/null +++ b/src/app/+collection-page/collection-form/collection-form.component.scss @@ -0,0 +1,7 @@ +@import '../../../styles/variables.scss'; + +// temporary fix for bootstrap 4 beta btn color issue +.btn-secondary { + background-color: $input-bg; + color: $input-color; +} diff --git a/src/app/+collection-page/collection-form/collection-form.component.ts b/src/app/+collection-page/collection-form/collection-form.component.ts new file mode 100644 index 0000000000..c51633dfb6 --- /dev/null +++ b/src/app/+collection-page/collection-form/collection-form.component.ts @@ -0,0 +1,32 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { isNotEmpty } from '../../shared/empty.util'; + +@Component({ + selector: 'ds-collection-form', + styleUrls: ['./collection-form.component.scss'], + templateUrl: './collection-form.component.html' +}) +export class CollectionFormComponent { + + name: string; + description: string; + introductory: string; + copyright: string; + news: string; + license: string; + provenance: string; + + nameRequiredError = false; + + @Output() submitted: EventEmitter = new EventEmitter(); + + onSubmit(data: any) { + if (isNotEmpty(data.name)) { + this.submitted.emit(data); + this.nameRequiredError = false; + } else { + this.nameRequiredError = true; + } + } + +} diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index c886aa655c..1bd53dd2b3 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -2,10 +2,12 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { CollectionPageComponent } from './collection-page.component'; +import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component'; @NgModule({ imports: [ RouterModule.forChild([ + { path: 'create', component: CreateCollectionPageComponent }, { path: ':id', component: CollectionPageComponent, pathMatch: 'full' } ]) ] diff --git a/src/app/+collection-page/collection-page.module.ts b/src/app/+collection-page/collection-page.module.ts index d0bc918b22..3ce3b66d05 100644 --- a/src/app/+collection-page/collection-page.module.ts +++ b/src/app/+collection-page/collection-page.module.ts @@ -5,6 +5,8 @@ import { SharedModule } from '../shared/shared.module'; import { CollectionPageComponent } from './collection-page.component'; import { CollectionPageRoutingModule } from './collection-page-routing.module'; +import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component'; +import { CollectionFormComponent } from './collection-form/collection-form.component'; @NgModule({ imports: [ @@ -14,6 +16,8 @@ import { CollectionPageRoutingModule } from './collection-page-routing.module'; ], declarations: [ CollectionPageComponent, + CreateCollectionPageComponent, + CollectionFormComponent ] }) export class CollectionPageModule { diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.html b/src/app/+collection-page/create-collection-page/create-collection-page.component.html new file mode 100644 index 0000000000..53a55571f6 --- /dev/null +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.html @@ -0,0 +1,8 @@ +
+
+
+ +
+
+ +
diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.scss b/src/app/+collection-page/create-collection-page/create-collection-page.component.scss new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.scss @@ -0,0 +1 @@ + diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts new file mode 100644 index 0000000000..689bd30dfc --- /dev/null +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +import { Community } from '../../core/shared/community.model'; +import { ComColDataService } from '../../core/data/comcol-data.service'; +import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { CollectionDataService } from '../../core/data/collection-data.service'; +import { Collection } from '../../core/shared/collection.model'; + +@Component({ + selector: 'ds-create-collection', + styleUrls: ['./create-collection-page.component.scss'], + templateUrl: './create-collection-page.component.html' +}) +export class CreateCollectionPageComponent { + + public constructor(private collectionDataService: CollectionDataService) { + + } + + onSubmit(data: any) { + const collection = Object.assign(new Collection(), { + name: data.name + }); + this.collectionDataService.create(collection); + } + +} diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 6e06a600cf..765cba0a8b 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -31,7 +31,8 @@ export class CollectionDataService extends ComColDataService + diff --git a/src/app/+collection-page/collection-form/collection-form.component.ts b/src/app/+collection-page/collection-form/collection-form.component.ts index c51633dfb6..4238eb3811 100644 --- a/src/app/+collection-page/collection-form/collection-form.component.ts +++ b/src/app/+collection-page/collection-form/collection-form.component.ts @@ -1,5 +1,6 @@ import { Component, EventEmitter, Output } from '@angular/core'; import { isNotEmpty } from '../../shared/empty.util'; +import { Location } from '@angular/common'; @Component({ selector: 'ds-collection-form', @@ -20,6 +21,10 @@ export class CollectionFormComponent { @Output() submitted: EventEmitter = new EventEmitter(); + public constructor(private location: Location) { + + } + onSubmit(data: any) { if (isNotEmpty(data.name)) { this.submitted.emit(data); @@ -29,4 +34,8 @@ export class CollectionFormComponent { } } + cancel() { + this.location.back(); + } + } diff --git a/src/app/+community-page/community-form/community-form.component.html b/src/app/+community-page/community-form/community-form.component.html index bc45b582ac..1bb5e97ec1 100644 --- a/src/app/+community-page/community-form/community-form.component.html +++ b/src/app/+community-page/community-form/community-form.component.html @@ -21,7 +21,7 @@
- +
diff --git a/src/app/+community-page/community-form/community-form.component.ts b/src/app/+community-page/community-form/community-form.component.ts index 64d08ab862..d7797028b7 100644 --- a/src/app/+community-page/community-form/community-form.component.ts +++ b/src/app/+community-page/community-form/community-form.component.ts @@ -1,5 +1,6 @@ import { Component, EventEmitter, Output } from '@angular/core'; import { isNotEmpty } from '../../shared/empty.util'; +import { Location } from '@angular/common'; @Component({ selector: 'ds-community-form', @@ -18,6 +19,10 @@ export class CommunityFormComponent { @Output() submitted: EventEmitter = new EventEmitter(); + public constructor(private location: Location) { + + } + onSubmit(data: any) { if (isNotEmpty(data.name)) { this.submitted.emit(data); @@ -27,4 +32,8 @@ export class CommunityFormComponent { } } + cancel() { + this.location.back(); + } + } From dfa42acab5f691904c71bde7c41470a72510cea8 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 20 Aug 2018 16:09:42 +0200 Subject: [PATCH 07/69] 54472: Create community/collection with parent community + error messages --- resources/i18n/en.json | 6 ++-- .../create-collection-page.component.html | 9 ++++- .../create-collection-page.component.ts | 34 +++++++++++++++++-- .../create-community-page.component.html | 9 ++++- .../create-community-page.component.ts | 34 +++++++++++++++++-- src/app/core/data/collection-data.service.ts | 4 --- src/app/core/data/comcol-data.service.ts | 28 ++++++++++----- src/app/core/data/community-data.service.ts | 4 --- 8 files changed, 103 insertions(+), 25 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 963dfae80f..af9d1a08c8 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -29,7 +29,8 @@ } }, "create": { - "head": "Create a Collection" + "head": "Create a Collection", + "sub-head": "Create a Collection for Community {{ parent }}" } }, "community": { @@ -53,7 +54,8 @@ } }, "create": { - "head": "Create a Community" + "head": "Create a Community", + "sub-head": "Create a Sub-Community for Community {{ parent }}" } }, "item": { diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.html b/src/app/+collection-page/create-collection-page/create-collection-page.component.html index 53a55571f6..9cf01f7c88 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.html +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.html @@ -1,8 +1,15 @@
- + + +

{{ 'collection.create.sub-head' | translate:{ parent: community.name } }}

+
+
+ {{error.statusCode}} +

{{error.errorMessage}}

+
diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index 689bd30dfc..2e8d9b557d 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -5,6 +5,13 @@ import { NormalizedCommunity } from '../../core/cache/models/normalized-communit import { CommunityDataService } from '../../core/data/community-data.service'; import { CollectionDataService } from '../../core/data/collection-data.service'; import { Collection } from '../../core/shared/collection.model'; +import { RouteService } from '../../shared/services/route.service'; +import { Router } from '@angular/router'; +import { DSOSuccessResponse, ErrorResponse } from '../../core/cache/response-cache.models'; +import { Observable } from 'rxjs/Observable'; +import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; +import { map } from 'rxjs/operators'; +import { RemoteData } from '../../core/data/remote-data'; @Component({ selector: 'ds-create-collection', @@ -13,15 +20,38 @@ import { Collection } from '../../core/shared/collection.model'; }) export class CreateCollectionPageComponent { - public constructor(private collectionDataService: CollectionDataService) { + private error$: Observable; + private parentUUID$: Observable; + private communityRDObs: Observable>; + public constructor(private collectionDataService: CollectionDataService, private communityDataService: CommunityDataService, private routeService: RouteService, private router: Router) { + this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); + this.parentUUID$.subscribe((uuid: string) => { + this.communityRDObs = this.communityDataService.findById(uuid); + }); } onSubmit(data: any) { const collection = Object.assign(new Collection(), { name: data.name }); - this.collectionDataService.create(collection); + this.parentUUID$.subscribe((uuid: string) => { + let response$: Observable; + if (uuid) { + response$ = this.collectionDataService.create(collection, uuid); + } else { + response$ = this.collectionDataService.create(collection); + } + this.error$ = response$.pipe( + map((response: ResponseCacheEntry) => { + if (!response.response.isSuccessful && response.response instanceof ErrorResponse) { + return response.response; + } else if (response.response instanceof DSOSuccessResponse) { + this.router.navigateByUrl(''); + } + }) + ); + }); } } diff --git a/src/app/+community-page/create-community-page/create-community-page.component.html b/src/app/+community-page/create-community-page/create-community-page.component.html index ea270db92b..53ca4f5020 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.html +++ b/src/app/+community-page/create-community-page/create-community-page.component.html @@ -1,8 +1,15 @@
- + + +

{{ 'community.create.sub-head' | translate:{ parent: community.name } }}

+
+
+ {{error.statusCode}} +

{{error.errorMessage}}

+
diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index 255d88c21b..1da0299e66 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -3,6 +3,13 @@ import { Community } from '../../core/shared/community.model'; import { ComColDataService } from '../../core/data/comcol-data.service'; import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model'; import { CommunityDataService } from '../../core/data/community-data.service'; +import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; +import { DSOSuccessResponse, ErrorResponse, RestResponse } from '../../core/cache/response-cache.models'; +import { Observable } from 'rxjs/Observable'; +import { map } from 'rxjs/operators'; +import { RouteService } from '../../shared/services/route.service'; +import { Router } from '@angular/router'; +import { RemoteData } from '../../core/data/remote-data'; @Component({ selector: 'ds-create-community', @@ -11,15 +18,38 @@ import { CommunityDataService } from '../../core/data/community-data.service'; }) export class CreateCommunityPageComponent { - public constructor(private communityDataService: CommunityDataService) { + private error$: Observable; + private parentUUID$: Observable; + private communityRDObs: Observable>; + public constructor(private communityDataService: CommunityDataService, private routeService: RouteService, private router: Router) { + this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); + this.parentUUID$.subscribe((uuid: string) => { + this.communityRDObs = this.communityDataService.findById(uuid); + }); } onSubmit(data: any) { const community = Object.assign(new Community(), { name: data.name }); - this.communityDataService.create(community); + this.parentUUID$.subscribe((uuid: string) => { + let response$: Observable; + if (uuid) { + response$ = this.communityDataService.create(community, uuid); + } else { + response$ = this.communityDataService.create(community); + } + this.error$ = response$.pipe( + map((response: ResponseCacheEntry) => { + if (!response.response.isSuccessful && response.response instanceof ErrorResponse) { + return response.response; + } else if (response.response instanceof DSOSuccessResponse) { + this.router.navigateByUrl(''); + } + }) + ); + }); } } diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 765cba0a8b..70c11ca5fc 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -31,8 +31,4 @@ export class CollectionDataService extends ComColDataService { + return this.halService.getEndpoint(this.linkPath).pipe( isNotEmptyOperator(), distinctUntilChanged(), map((endpointURL: string) => { @@ -75,12 +75,22 @@ export abstract class ComColDataService request.href), + getResponseFromSelflink(this.responseCache) + ); } - abstract buildCreateParams(comcol: TDomain): string; + public buildCreateParams(comcol: TDomain, parentUUID?: string): string { + if (comcol instanceof Community || comcol instanceof Collection) { + let urlParams = '?name=' + comcol.name; + if (parentUUID) { + urlParams += '&parent=' + parentUUID; + } + return urlParams; + } + } } diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index acc915c093..9d6af5ee6f 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -34,8 +34,4 @@ export class CommunityDataService extends ComColDataService Date: Wed, 22 Aug 2018 16:10:55 +0200 Subject: [PATCH 08/69] 54472: Authorization + tests (intermediate) --- .../collection-page-routing.module.ts | 3 +- .../community-page-routing.module.ts | 3 +- .../create-community-page.component.spec.ts | 69 +++++++++++ src/app/core/data/comcol-data.service.spec.ts | 110 +++++++++++++++--- src/app/core/data/comcol-data.service.ts | 2 +- 5 files changed, 168 insertions(+), 19 deletions(-) create mode 100644 src/app/+community-page/create-community-page/create-community-page.component.spec.ts diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index 1bd53dd2b3..35faae7b02 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -3,11 +3,12 @@ import { RouterModule } from '@angular/router'; import { CollectionPageComponent } from './collection-page.component'; import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component'; +import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; @NgModule({ imports: [ RouterModule.forChild([ - { path: 'create', component: CreateCollectionPageComponent }, + { path: 'create', component: CreateCollectionPageComponent, canActivate: [AuthenticatedGuard] }, { path: ':id', component: CollectionPageComponent, pathMatch: 'full' } ]) ] diff --git a/src/app/+community-page/community-page-routing.module.ts b/src/app/+community-page/community-page-routing.module.ts index 249b01ea18..13d75628b9 100644 --- a/src/app/+community-page/community-page-routing.module.ts +++ b/src/app/+community-page/community-page-routing.module.ts @@ -3,11 +3,12 @@ import { RouterModule } from '@angular/router'; import { CommunityPageComponent } from './community-page.component'; import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; +import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; @NgModule({ imports: [ RouterModule.forChild([ - { path: 'create', component: CreateCommunityPageComponent }, + { path: 'create', component: CreateCommunityPageComponent, canActivate: [AuthenticatedGuard] }, { path: ':id', component: CommunityPageComponent, pathMatch: 'full' } ]) ] diff --git a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts new file mode 100644 index 0000000000..990a9895b9 --- /dev/null +++ b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts @@ -0,0 +1,69 @@ +import { CreateCommunityPageComponent } from './create-community-page.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { RouteService } from '../../shared/services/route.service'; +import { Router } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { Observable } from 'rxjs/Observable'; +import { RemoteData } from '../../core/data/remote-data'; +import { Community } from '../../core/shared/community.model'; +import { DSOSuccessResponse } from '../../core/cache/response-cache.models'; +import { BrowserModule } from '@angular/platform-browser'; +import { SharedModule } from '../../shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { CommunityFormComponent } from '../community-form/community-form.component'; + +describe('CreateCommunityPageComponent', () => { + let comp: CreateCommunityPageComponent; + let fixture: ComponentFixture; + let communityDataService: CommunityDataService; + let routeService: RouteService; + let router: any = {}; + + const community = Object.assign(new Community(), { + uuid: 'a20da287-e174-466a-9926-f66b9300d347', + name: 'test community' + }); + + const communityDataServiceStub = { + findById: (uuid) => Observable.of(new RemoteData(false, false, true, null, Object.assign(new Community(), { + uuid: uuid, + name: community.name + }))), + create: (com, uuid?) => Observable.of({ + response: new DSOSuccessResponse(null,'200',null) + }) + }; + const routeServiceStub = { + getQueryParameterValue: (param) => Observable.of(community.uuid) + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule], + declarations: [CreateCommunityPageComponent, CommunityFormComponent], + providers: [ + { provide: CommunityDataService, useValue: communityDataServiceStub }, + { provide: RouteService, useValue: routeServiceStub }, + { provide: Router, useValue: router } + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CreateCommunityPageComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + communityDataService = (comp as any).communityDataService; + routeService = (comp as any).routeService; + router = (comp as any).router; + }); + + it('should navigate on successful submit', () => { + spyOn(router, 'navigateByUrl'); + comp.onSubmit({ + name: 'test' + }); + expect(router.navigateByUrl).toHaveBeenCalled(); + }); +}); diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index b5727fb22f..5f440dd442 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -1,6 +1,6 @@ import { Store } from '@ngrx/store'; import { cold, getTestScheduler, hot } from 'jasmine-marbles'; -import { TestScheduler } from 'rxjs/Rx'; +import { Observable, TestScheduler } from 'rxjs/Rx'; import { GlobalConfig } from '../../../config'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; @@ -14,6 +14,8 @@ import { FindByIDRequest } from './request.models'; import { RequestService } from './request.service'; import { NormalizedObject } from '../cache/models/normalized-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { Community } from '../shared/community.model'; +import { AuthService } from '../auth/auth.service'; const LINK_NAME = 'test'; @@ -32,6 +34,7 @@ class TestService extends ComColDataService { protected cds: CommunityDataService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, + protected authService: AuthService, protected linkPath: string ) { super(); @@ -46,7 +49,8 @@ describe('ComColDataService', () => { let requestService: RequestService; let cds: CommunityDataService; let objectCache: ObjectCacheService; - const halService: any = {}; + let authService: AuthService; + let halService: any = {}; const rdbService = {} as RemoteDataBuildService; const store = {} as Store; @@ -57,6 +61,11 @@ describe('ComColDataService', () => { const communityEndpoint = `${communitiesEndpoint}/${scopeID}`; const scopedEndpoint = `${communityEndpoint}/${LINK_NAME}`; const serviceEndpoint = `https://rest.api/core/${LINK_NAME}`; + const authHeader = 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJlaWQiOiJhNjA4NmIzNC0zOTE4LTQ1YjctOGRkZC05MzI5YTcwMmEyNmEiLCJzZyI6W10sImV4cCI6MTUzNDk0MDcyNX0.RV5GAtiX6cpwBN77P_v16iG9ipeyiO7faNYSNMzq_sQ'; + + const mockHalService = { + getEndpoint: (linkPath) => Observable.of(communitiesEndpoint) + }; function initMockCommunityDataService(): CommunityDataService { return jasmine.createSpyObj('responseCache', { @@ -85,6 +94,14 @@ describe('ComColDataService', () => { }); } + function initMockAuthService(): AuthService { + return jasmine.createSpyObj('authService', { + buildAuthHeader: cold('c-', { + c: authHeader + }) + }); + } + function initTestService(): TestService { return new TestService( responseCache, @@ -95,22 +112,27 @@ describe('ComColDataService', () => { cds, objectCache, halService, + authService, LINK_NAME ); } + beforeEach(() => { + cds = initMockCommunityDataService(); + requestService = getMockRequestService(); + objectCache = initMockObjectCacheService(); + responseCache = initMockResponseCacheService(true); + halService = mockHalService; + authService = initMockAuthService(); + service = initTestService(); + }); + describe('getScopedEndpoint', () => { beforeEach(() => { scheduler = getTestScheduler(); }); it('should configure a new FindByIDRequest for the scope Community', () => { - cds = initMockCommunityDataService(); - requestService = getMockRequestService(); - objectCache = initMockObjectCacheService(); - responseCache = initMockResponseCacheService(true); - service = initTestService(); - const expected = new FindByIDRequest(requestService.generateRequestId(), communityEndpoint, scopeID); scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe()); @@ -120,14 +142,6 @@ describe('ComColDataService', () => { }); describe('if the scope Community can be found', () => { - beforeEach(() => { - cds = initMockCommunityDataService(); - requestService = getMockRequestService(); - objectCache = initMockObjectCacheService(); - responseCache = initMockResponseCacheService(true); - service = initTestService(); - }); - it('should fetch the scope Community from the cache', () => { scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe()); scheduler.flush(); @@ -160,4 +174,68 @@ describe('ComColDataService', () => { }); }); + + describe('create', () => { + let community: Community; + const name = 'test community'; + + beforeEach(() => { + community = Object.assign(new Community(), { + name: name + }); + spyOn(service, 'buildCreateParams'); + }); + + describe('when creating a top-level community', () => { + it('should build params without parent UUID', () => { + scheduler.schedule(() => service.create(community).subscribe()); + scheduler.flush(); + expect(service.buildCreateParams).toHaveBeenCalledWith(community); + }); + }); + + describe('when creating a community part of another community', () => { + let parentCommunity: Community; + const parentName = 'test parent community'; + const parentUUID = 'a20da287-e174-466a-9926-f66b9300d347'; + + beforeEach(() => { + parentCommunity = Object.assign(new Community(), { + id: parentUUID, + uuid: parentUUID, + name: parentName + }); + }); + + it('should build params with parent UUID', () => { + scheduler.schedule(() => service.create(community, parentUUID).subscribe()); + scheduler.flush(); + expect(service.buildCreateParams).toHaveBeenCalledWith(community, parentUUID); + }); + }); + }); + + describe('buildCreateParams', () => { + let community: Community; + const name = 'test community'; + let parentCommunity: Community; + const parentName = 'test parent community'; + const parentUUID = 'a20da287-e174-466a-9926-f66b9300d347'; + + beforeEach(() => { + community = Object.assign(new Community(), { + name: name + }); + parentCommunity = Object.assign(new Community(), { + id: parentUUID, + uuid: parentUUID, + name: parentName + }); + }); + + it('should return the correct url parameters', () => { + expect(service.buildCreateParams(community, parentUUID)).toEqual('?name=' + name + '&parent=' + parentUUID); + }); + }); + }); diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index b7a87913de..a57f5fa910 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -75,7 +75,7 @@ export abstract class ComColDataService request.href), From 3a0de2865a753eabddc29a6427639ab4812de73e Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 23 Aug 2018 11:03:06 +0200 Subject: [PATCH 09/69] 54472: CreateCommunityPageComponent and CreateCollectionPageComponent tests --- .../create-collection-page.component.spec.ts | 99 +++++++++++++++++++ .../create-community-page.component.spec.ts | 38 +++++-- 2 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts new file mode 100644 index 0000000000..efe0e3da97 --- /dev/null +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts @@ -0,0 +1,99 @@ +import { SharedModule } from '../../shared/shared.module'; +import { Community } from '../../core/shared/community.model'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { DSOSuccessResponse, ErrorResponse } from '../../core/cache/response-cache.models'; +import { CommonModule } from '@angular/common'; +import { CreateCommunityPageComponent } from '../../+community-page/create-community-page/create-community-page.component'; +import { Router } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { CommunityFormComponent } from '../../+community-page/community-form/community-form.component'; +import { Observable } from 'rxjs/Observable'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { RequestError } from '../../core/data/request.models'; +import { RouteService } from '../../shared/services/route.service'; +import { RemoteData } from '../../core/data/remote-data'; +import { CreateCollectionPageComponent } from './create-collection-page.component'; +import { CollectionDataService } from '../../core/data/collection-data.service'; +import { Collection } from '../../core/shared/collection.model'; +import { RouterTestingModule } from '@angular/router/testing'; +import { CollectionFormComponent } from '../collection-form/collection-form.component'; + +describe('CreateCollectionPageComponent', () => { + let comp: CreateCollectionPageComponent; + let fixture: ComponentFixture; + let collectionDataService: CollectionDataService; + let communityDataService: CommunityDataService; + let routeService: RouteService; + let router: Router; + + const community = Object.assign(new Community(), { + uuid: 'a20da287-e174-466a-9926-f66b9300d347', + name: 'test community' + }); + + const collectionDataServiceStub = { + create: (com, uuid?) => Observable.of({ + response: new DSOSuccessResponse(null,'200',null) + }) + }; + const communityDataServiceStub = { + findById: (uuid) => Observable.of(new RemoteData(false, false, true, null, Object.assign(new Community(), { + uuid: uuid, + name: community.name + }))) + }; + const routeServiceStub = { + getQueryParameterValue: (param) => Observable.of(community.uuid) + }; + const routerStub = { + navigateByUrl: (url) => url + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + declarations: [CreateCollectionPageComponent, CollectionFormComponent], + providers: [ + { provide: CollectionDataService, useValue: collectionDataServiceStub }, + { provide: CommunityDataService, useValue: communityDataServiceStub }, + { provide: RouteService, useValue: routeServiceStub }, + { provide: Router, useValue: routerStub } + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CreateCollectionPageComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + collectionDataService = (comp as any).collectionDataService; + communityDataService = (comp as any).communityDataService; + routeService = (comp as any).routeService; + router = (comp as any).router; + }); + + describe('onSubmit', () => { + const data = { + name: 'test' + }; + + it('should navigate when successful', () => { + spyOn(router, 'navigateByUrl'); + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigateByUrl).toHaveBeenCalled(); + }); + + it('should not navigate on failure', () => { + spyOn(router, 'navigateByUrl'); + spyOn(collectionDataService, 'create').and.returnValue(Observable.of({ + response: Object.assign(new ErrorResponse(new RequestError()), { + isSuccessful: false + }) + })); + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigateByUrl).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts index 990a9895b9..3c4c7648da 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts @@ -7,18 +7,20 @@ import { TranslateModule } from '@ngx-translate/core'; import { Observable } from 'rxjs/Observable'; import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; -import { DSOSuccessResponse } from '../../core/cache/response-cache.models'; +import { DSOSuccessResponse, ErrorResponse } from '../../core/cache/response-cache.models'; import { BrowserModule } from '@angular/platform-browser'; import { SharedModule } from '../../shared/shared.module'; import { CommonModule } from '@angular/common'; import { CommunityFormComponent } from '../community-form/community-form.component'; +import { RouterTestingModule } from '@angular/router/testing'; +import { RequestError } from '../../core/data/request.models'; describe('CreateCommunityPageComponent', () => { let comp: CreateCommunityPageComponent; let fixture: ComponentFixture; let communityDataService: CommunityDataService; let routeService: RouteService; - let router: any = {}; + let router: Router; const community = Object.assign(new Community(), { uuid: 'a20da287-e174-466a-9926-f66b9300d347', @@ -37,15 +39,18 @@ describe('CreateCommunityPageComponent', () => { const routeServiceStub = { getQueryParameterValue: (param) => Observable.of(community.uuid) }; + const routerStub = { + navigateByUrl: (url) => url + }; beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, CommonModule], + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], declarations: [CreateCommunityPageComponent, CommunityFormComponent], providers: [ { provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: RouteService, useValue: routeServiceStub }, - { provide: Router, useValue: router } + { provide: Router, useValue: routerStub } ] }).compileComponents(); })); @@ -59,11 +64,28 @@ describe('CreateCommunityPageComponent', () => { router = (comp as any).router; }); - it('should navigate on successful submit', () => { - spyOn(router, 'navigateByUrl'); - comp.onSubmit({ + describe('onSubmit', () => { + const data = { name: 'test' + }; + + it('should navigate when successful', () => { + spyOn(router, 'navigateByUrl'); + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigateByUrl).toHaveBeenCalled(); + }); + + it('should not navigate on failure', () => { + spyOn(router, 'navigateByUrl'); + spyOn(communityDataService, 'create').and.returnValue(Observable.of({ + response: Object.assign(new ErrorResponse(new RequestError()), { + isSuccessful: false + }) + })); + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigateByUrl).not.toHaveBeenCalled(); }); - expect(router.navigateByUrl).toHaveBeenCalled(); }); }); From 7bb264b8e6d3cebfa74aee2e63f1533866897e04 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 23 Aug 2018 11:42:36 +0200 Subject: [PATCH 10/69] 54472: CommunityFormComponent tests --- .../community-form.component.html | 4 +- .../community-form.component.spec.ts | 70 +++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 src/app/+community-page/community-form/community-form.component.spec.ts diff --git a/src/app/+community-page/community-form/community-form.component.html b/src/app/+community-page/community-form/community-form.component.html index 1bb5e97ec1..578e2a0d6a 100644 --- a/src/app/+community-page/community-form/community-form.component.html +++ b/src/app/+community-page/community-form/community-form.component.html @@ -21,7 +21,7 @@
- - + +
diff --git a/src/app/+community-page/community-form/community-form.component.spec.ts b/src/app/+community-page/community-form/community-form.component.spec.ts new file mode 100644 index 0000000000..b7b18dc70b --- /dev/null +++ b/src/app/+community-page/community-form/community-form.component.spec.ts @@ -0,0 +1,70 @@ +import { CommunityFormComponent } from './community-form.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { SharedModule } from '../../shared/shared.module'; +import { TranslateModule } from '@ngx-translate/core'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +fdescribe('CommunityFormComponent', () => { + let comp: CommunityFormComponent; + let fixture: ComponentFixture + let location: Location; + + /* tslint:disable:no-empty */ + const locationStub = { + back: () => {} + }; + /* tslint:enable:no-empty */ + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + declarations: [CommunityFormComponent], + providers: [ + { provide: Location, useValue: locationStub } + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CommunityFormComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + location = (comp as any).location; + }); + + describe('when submitting', () => { + let input: DebugElement; + let submit: DebugElement; + let cancel: DebugElement; + let error: DebugElement; + + beforeEach(() => { + input = fixture.debugElement.query(By.css('input#community-name')); + submit = fixture.debugElement.query(By.css('button#community-submit')); + cancel = fixture.debugElement.query(By.css('button#community-cancel')); + error = fixture.debugElement.query(By.css('div.invalid-feedback')); + }); + + it('should display an error when leaving name empty', () => { + const el = input.nativeElement; + + el.value = ''; + el.dispatchEvent(new Event('input')); + submit.nativeElement.click(); + fixture.detectChanges(); + + expect(error.nativeElement.style.display).not.toEqual('none'); + }); + + it('should navigate back when pressing cancel', () => { + spyOn(location, 'back'); + cancel.nativeElement.click(); + fixture.detectChanges(); + + expect(location.back).toHaveBeenCalled(); + }); + }) +}); From def2cfab3a05a4bd1ab2de69a01e5da6c3bdcc4b Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 23 Aug 2018 12:59:33 +0200 Subject: [PATCH 11/69] 54472: CollectionFormComponent tests --- .../collection-form.component.html | 4 +- .../collection-form.component.spec.ts | 71 +++++++++++++++++++ .../community-form.component.spec.ts | 4 +- 3 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 src/app/+collection-page/collection-form/collection-form.component.spec.ts diff --git a/src/app/+collection-page/collection-form/collection-form.component.html b/src/app/+collection-page/collection-form/collection-form.component.html index 808a2793c4..ea99c58c6a 100644 --- a/src/app/+collection-page/collection-form/collection-form.component.html +++ b/src/app/+collection-page/collection-form/collection-form.component.html @@ -29,7 +29,7 @@
- - + +
diff --git a/src/app/+collection-page/collection-form/collection-form.component.spec.ts b/src/app/+collection-page/collection-form/collection-form.component.spec.ts new file mode 100644 index 0000000000..9c65b7c305 --- /dev/null +++ b/src/app/+collection-page/collection-form/collection-form.component.spec.ts @@ -0,0 +1,71 @@ +import { CommunityFormComponent } from './community-form.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { SharedModule } from '../../shared/shared.module'; +import { TranslateModule } from '@ngx-translate/core'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; +import { CollectionFormComponent } from './collection-form.component'; + +describe('CommunityFormComponent', () => { + let comp: CollectionFormComponent; + let fixture: ComponentFixture + let location: Location; + + /* tslint:disable:no-empty */ + const locationStub = { + back: () => {} + }; + /* tslint:enable:no-empty */ + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + declarations: [CollectionFormComponent], + providers: [ + { provide: Location, useValue: locationStub } + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CollectionFormComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + location = (comp as any).location; + }); + + describe('when submitting', () => { + let input: DebugElement; + let submit: DebugElement; + let cancel: DebugElement; + let error: DebugElement; + + beforeEach(() => { + input = fixture.debugElement.query(By.css('input#collection-name')); + submit = fixture.debugElement.query(By.css('button#collection-submit')); + cancel = fixture.debugElement.query(By.css('button#collection-cancel')); + error = fixture.debugElement.query(By.css('div.invalid-feedback')); + }); + + it('should display an error when leaving name empty', () => { + const el = input.nativeElement; + + el.value = ''; + el.dispatchEvent(new Event('input')); + submit.nativeElement.click(); + fixture.detectChanges(); + + expect(error.nativeElement.style.display).not.toEqual('none'); + }); + + it('should navigate back when pressing cancel', () => { + spyOn(location, 'back'); + cancel.nativeElement.click(); + fixture.detectChanges(); + + expect(location.back).toHaveBeenCalled(); + }); + }) +}); diff --git a/src/app/+community-page/community-form/community-form.component.spec.ts b/src/app/+community-page/community-form/community-form.component.spec.ts index b7b18dc70b..d8eab13f20 100644 --- a/src/app/+community-page/community-form/community-form.component.spec.ts +++ b/src/app/+community-page/community-form/community-form.component.spec.ts @@ -7,9 +7,9 @@ import { RouterTestingModule } from '@angular/router/testing'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; -fdescribe('CommunityFormComponent', () => { +describe('CommunityFormComponent', () => { let comp: CommunityFormComponent; - let fixture: ComponentFixture + let fixture: ComponentFixture; let location: Location; /* tslint:disable:no-empty */ From 870c545357f07cf7eb9f704ade62b516452f0c0d Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 23 Aug 2018 13:56:16 +0200 Subject: [PATCH 12/69] 54472: Added metadata to community/collection creation --- .../collection-form/collection-form.component.html | 4 ++-- .../create-collection-page.component.ts | 9 ++++++++- .../create-community-page.component.ts | 8 +++++++- src/app/core/data/comcol-data.service.ts | 5 +++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/app/+collection-page/collection-form/collection-form.component.html b/src/app/+collection-page/collection-form/collection-form.component.html index ea99c58c6a..85135af15d 100644 --- a/src/app/+collection-page/collection-form/collection-form.component.html +++ b/src/app/+collection-page/collection-form/collection-form.component.html @@ -22,11 +22,11 @@
- +
- +
diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index 2e8d9b557d..92314a53c7 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -33,7 +33,14 @@ export class CreateCollectionPageComponent { onSubmit(data: any) { const collection = Object.assign(new Collection(), { - name: data.name + name: data.name, + metadata: [ + { key: 'dc.description', value: data.introductory }, + { key: 'dc.description.abstract', value: data.description }, + { key: 'dc.rights', value: data.copyright }, + { key: 'dc.rights.license', value: data.license } + // TODO: metadata for news and provenance + ] }); this.parentUUID$.subscribe((uuid: string) => { let response$: Observable; diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index 1da0299e66..edb332d807 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -31,7 +31,13 @@ export class CreateCommunityPageComponent { onSubmit(data: any) { const community = Object.assign(new Community(), { - name: data.name + name: data.name, + metadata: [ + { key: 'dc.description', value: data.introductory }, + { key: 'dc.description.abstract', value: data.description }, + { key: 'dc.rights', value: data.copyright } + // TODO: metadata for news + ] }); this.parentUUID$.subscribe((uuid: string) => { let response$: Observable; diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index a57f5fa910..745879be1f 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -89,6 +89,11 @@ export abstract class ComColDataService { let comp: CollectionFormComponent; diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index 92314a53c7..4f57482cbd 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -20,9 +20,9 @@ import { RemoteData } from '../../core/data/remote-data'; }) export class CreateCollectionPageComponent { - private error$: Observable; - private parentUUID$: Observable; - private communityRDObs: Observable>; + public error$: Observable; + public parentUUID$: Observable; + public communityRDObs: Observable>; public constructor(private collectionDataService: CollectionDataService, private communityDataService: CommunityDataService, private routeService: RouteService, private router: Router) { this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); diff --git a/src/app/+community-page/community-form/community-form.component.spec.ts b/src/app/+community-page/community-form/community-form.component.spec.ts index d8eab13f20..ea17d15942 100644 --- a/src/app/+community-page/community-form/community-form.component.spec.ts +++ b/src/app/+community-page/community-form/community-form.component.spec.ts @@ -6,6 +6,7 @@ import { CommonModule } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; +import { Location } from '@angular/common'; describe('CommunityFormComponent', () => { let comp: CommunityFormComponent; diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index edb332d807..8f2f61ab80 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -18,9 +18,9 @@ import { RemoteData } from '../../core/data/remote-data'; }) export class CreateCommunityPageComponent { - private error$: Observable; - private parentUUID$: Observable; - private communityRDObs: Observable>; + public error$: Observable; + public parentUUID$: Observable; + public communityRDObs: Observable>; public constructor(private communityDataService: CommunityDataService, private routeService: RouteService, private router: Router) { this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); From e7c5a2a29ced4dc9d414879fe4ec588e34f44445 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 5 Sep 2018 16:31:06 +0200 Subject: [PATCH 14/69] 54472: refactoring feedback pt1 --- .../create-collection-page.component.html | 2 +- .../create-collection-page.component.ts | 50 ++++++------ .../create-community-page.component.html | 2 +- .../create-community-page.component.ts | 48 ++++++------ src/app/core/data/collection-data.service.ts | 1 + src/app/core/data/comcol-data.service.spec.ts | 1 + src/app/core/data/comcol-data.service.ts | 45 ++++------- src/app/core/data/community-data.service.ts | 1 + src/app/core/data/data.service.ts | 76 ++++++++++++++----- src/app/core/data/item-data.service.ts | 10 ++- 10 files changed, 133 insertions(+), 103 deletions(-) diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.html b/src/app/+collection-page/create-collection-page/create-collection-page.component.html index 9cf01f7c88..11f4f53ecd 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.html +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.html @@ -1,7 +1,7 @@
- +

{{ 'collection.create.sub-head' | translate:{ parent: community.name } }}

diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index 4f57482cbd..8f31d30441 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -12,6 +12,7 @@ import { Observable } from 'rxjs/Observable'; import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; import { map } from 'rxjs/operators'; import { RemoteData } from '../../core/data/remote-data'; +import { isNotEmpty } from '../../shared/empty.util'; @Component({ selector: 'ds-create-collection', @@ -27,37 +28,34 @@ export class CreateCollectionPageComponent { public constructor(private collectionDataService: CollectionDataService, private communityDataService: CommunityDataService, private routeService: RouteService, private router: Router) { this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); this.parentUUID$.subscribe((uuid: string) => { - this.communityRDObs = this.communityDataService.findById(uuid); + if (isNotEmpty(uuid)) { + this.communityRDObs = this.communityDataService.findById(uuid); + } }); } onSubmit(data: any) { - const collection = Object.assign(new Collection(), { - name: data.name, - metadata: [ - { key: 'dc.description', value: data.introductory }, - { key: 'dc.description.abstract', value: data.description }, - { key: 'dc.rights', value: data.copyright }, - { key: 'dc.rights.license', value: data.license } - // TODO: metadata for news and provenance - ] - }); this.parentUUID$.subscribe((uuid: string) => { - let response$: Observable; - if (uuid) { - response$ = this.collectionDataService.create(collection, uuid); - } else { - response$ = this.collectionDataService.create(collection); - } - this.error$ = response$.pipe( - map((response: ResponseCacheEntry) => { - if (!response.response.isSuccessful && response.response instanceof ErrorResponse) { - return response.response; - } else if (response.response instanceof DSOSuccessResponse) { - this.router.navigateByUrl(''); - } - }) - ); + const collection = Object.assign(new Collection(), { + name: data.name, + metadata: [ + { key: 'dc.description', value: data.introductory }, + { key: 'dc.description.abstract', value: data.description }, + { key: 'dc.rights', value: data.copyright }, + { key: 'dc.rights.license', value: data.license } + // TODO: metadata for news and provenance + ], + owner: Observable.of(new RemoteData(false, false, true, null, Object.assign(new Community(), { + id: uuid, + uuid: uuid + }))) + }); + this.collectionDataService.create(collection).subscribe((rd: RemoteData) => { + console.log(rd); + if (rd.hasSucceeded) { + this.router.navigateByUrl(''); + } + }); }); } diff --git a/src/app/+community-page/create-community-page/create-community-page.component.html b/src/app/+community-page/create-community-page/create-community-page.component.html index 53ca4f5020..06fd643530 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.html +++ b/src/app/+community-page/create-community-page/create-community-page.component.html @@ -1,7 +1,7 @@
- +

{{ 'community.create.sub-head' | translate:{ parent: community.name } }}

diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index 8f2f61ab80..04d5b8fd39 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -10,6 +10,7 @@ import { map } from 'rxjs/operators'; import { RouteService } from '../../shared/services/route.service'; import { Router } from '@angular/router'; import { RemoteData } from '../../core/data/remote-data'; +import { isNotEmpty } from '../../shared/empty.util'; @Component({ selector: 'ds-create-community', @@ -25,36 +26,33 @@ export class CreateCommunityPageComponent { public constructor(private communityDataService: CommunityDataService, private routeService: RouteService, private router: Router) { this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); this.parentUUID$.subscribe((uuid: string) => { - this.communityRDObs = this.communityDataService.findById(uuid); + if (isNotEmpty(uuid)) { + this.communityRDObs = this.communityDataService.findById(uuid); + } }); } onSubmit(data: any) { - const community = Object.assign(new Community(), { - name: data.name, - metadata: [ - { key: 'dc.description', value: data.introductory }, - { key: 'dc.description.abstract', value: data.description }, - { key: 'dc.rights', value: data.copyright } - // TODO: metadata for news - ] - }); this.parentUUID$.subscribe((uuid: string) => { - let response$: Observable; - if (uuid) { - response$ = this.communityDataService.create(community, uuid); - } else { - response$ = this.communityDataService.create(community); - } - this.error$ = response$.pipe( - map((response: ResponseCacheEntry) => { - if (!response.response.isSuccessful && response.response instanceof ErrorResponse) { - return response.response; - } else if (response.response instanceof DSOSuccessResponse) { - this.router.navigateByUrl(''); - } - }) - ); + const community = Object.assign(new Community(), { + name: data.name, + metadata: [ + { key: 'dc.description', value: data.introductory }, + { key: 'dc.description.abstract', value: data.description }, + { key: 'dc.rights', value: data.copyright } + // TODO: metadata for news + ], + owner: Observable.of(new RemoteData(false, false, true, null, Object.assign(new Community(), { + id: uuid, + uuid: uuid + }))) + }); + this.communityDataService.create(community).subscribe((rd: RemoteData) => { + console.log(rd); + if (rd.hasSucceeded) { + this.router.navigateByUrl(''); + } + }); }); } diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 70c11ca5fc..dc3635595a 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -13,6 +13,7 @@ import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { AuthService } from '../auth/auth.service'; import { Community } from '../shared/community.model'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; @Injectable() export class CollectionDataService extends ComColDataService { diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index 5f440dd442..9cc44e65db 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -16,6 +16,7 @@ import { NormalizedObject } from '../cache/models/normalized-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Community } from '../shared/community.model'; import { AuthService } from '../auth/auth.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; const LINK_NAME = 'test'; diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 745879be1f..c47d48dc07 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -19,12 +19,12 @@ import { configureRequest, getResponseFromSelflink } from '../shared/operators'; import { AuthService } from '../auth/auth.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpHeaders } from '@angular/common/http'; +import { RemoteData } from './remote-data'; export abstract class ComColDataService extends DataService { protected abstract cds: CommunityDataService; protected abstract objectCache: ObjectCacheService; protected abstract halService: HALEndpointService; - protected abstract authService: AuthService; /** * Get the scoped endpoint URL by fetching the object with @@ -66,36 +66,21 @@ export abstract class ComColDataService { - return this.halService.getEndpoint(this.linkPath).pipe( - isNotEmptyOperator(), - distinctUntilChanged(), - map((endpointURL: string) => { - const options: HttpOptions = Object.create({}); - const headers = new HttpHeaders(); - headers.append('Authentication', this.authService.buildAuthHeader()); - options.headers = headers; - return new PostRequest(this.requestService.generateRequestId(), endpointURL + ((parentUUID) ? this.buildCreateParams(comcol, parentUUID) : this.buildCreateParams(comcol)), options); - }), - configureRequest(this.requestService), - map((request: RestRequest) => request.href), - getResponseFromSelflink(this.responseCache) + public buildCreateParams(comcol): Observable { + return comcol.owner.pipe( + map((rd: RemoteData) => { + let urlParams = '?name=' + comcol.name; + if (rd.payload.id) { + urlParams += '&parent=' + rd.payload.id; + } + if (comcol.metadata) { + for (const i of Object.keys(comcol.metadata)) { + urlParams += '&' + comcol.metadata[i].key + '=' + comcol.metadata[i].value; + } + } + return urlParams; + }) ); } - public buildCreateParams(comcol: TDomain, parentUUID?: string): string { - if (comcol instanceof Community || comcol instanceof Collection) { - let urlParams = '?name=' + comcol.name; - if (parentUUID) { - urlParams += '&parent=' + parentUUID; - } - if (comcol.metadata) { - for (const i of Object.keys(comcol.metadata)) { - urlParams += '&' + comcol.metadata[i].key + '=' + comcol.metadata[i].value; - } - } - return urlParams; - } - } - } diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index 9d6af5ee6f..d367b15f6b 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -12,6 +12,7 @@ import { ComColDataService } from './comcol-data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { AuthService } from '../auth/auth.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; @Injectable() export class CommunityDataService extends ComColDataService { diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index f532ff05ba..b3b69fdd54 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -1,6 +1,6 @@ import { Store } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ResponseCacheService } from '../cache/response-cache.service'; import { CoreState } from '../core.reducers'; @@ -8,9 +8,30 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { URLCombiner } from '../url-combiner/url-combiner'; import { PaginatedList } from './paginated-list'; import { RemoteData } from './remote-data'; -import { FindAllOptions, FindAllRequest, FindByIDRequest, GetRequest } from './request.models'; +import { + FindAllOptions, + FindAllRequest, + FindByIDRequest, + GetRequest, + PostRequest, + RestRequest +} from './request.models'; import { RequestService } from './request.service'; import { NormalizedObject } from '../cache/models/normalized-object.model'; +import { distinctUntilChanged, map, share, withLatestFrom } from 'rxjs/operators'; +import { + configureRequest, + filterSuccessfulResponses, + getRequestFromSelflink, + getResponseFromSelflink +} from '../shared/operators'; +import { ResponseCacheEntry } from '../cache/response-cache.reducer'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { HttpHeaders } from '@angular/common/http'; +import { ErrorResponse, GenericSuccessResponse } from '../cache/response-cache.models'; +import { BrowseEntry } from '../shared/browse-entry.model'; +import { AuthService } from '../auth/auth.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; export abstract class DataService { protected abstract responseCache: ResponseCacheService; @@ -19,6 +40,7 @@ export abstract class DataService protected abstract store: Store; protected abstract linkPath: string; protected abstract halService: HALEndpointService; + protected abstract authService: AuthService; public abstract getScopedEndpoint(scope: string): Observable @@ -91,21 +113,37 @@ export abstract class DataService return this.rdbService.buildSingle(href); } - // TODO implement, after the structure of the REST server's POST response is finalized - // create(dso: DSpaceObject): Observable> { - // const postHrefObs = this.getEndpoint(); - // - // // TODO ID is unknown at this point - // const idHrefObs = postHrefObs.map((href: string) => this.getFindByIDHref(href, dso.id)); - // - // postHrefObs - // .filter((href: string) => hasValue(href)) - // .take(1) - // .subscribe((href: string) => { - // const request = new RestRequest(this.requestService.generateRequestId(), href, RestRequestMethod.Post, dso); - // this.requestService.configure(request); - // }); - // - // return this.rdbService.buildSingle(idHrefObs, this.normalizedResourceType); - // } + public create(dso: TDomain): Observable> { + const request$ = this.halService.getEndpoint(this.linkPath).pipe( + isNotEmptyOperator(), + distinctUntilChanged(), + withLatestFrom(this.buildCreateParams(dso)), + map(([endpointURL, params]) => { + const options: HttpOptions = Object.create({}); + const headers = new HttpHeaders(); + headers.append('Authentication', this.authService.buildAuthHeader()); + options.headers = headers; + return new PostRequest(this.requestService.generateRequestId(), endpointURL + params, options); + }), + configureRequest(this.requestService), + share() + ); + + const href$ = request$.pipe(map((request: RestRequest) => request.href)); + + const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); + const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache)); + + const payload$ = responseCache$.pipe( + filterSuccessfulResponses(), + map((entry: ResponseCacheEntry) => entry.response), + map((response: GenericSuccessResponse) => response.payload), + distinctUntilChanged() + ); + + return this.rdbService.toRemoteDataObservable(requestEntry$, responseCache$, payload$); + } + + public abstract buildCreateParams(dso: TDomain): Observable; + } diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 6b0937d8e4..d1db71b78d 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -15,6 +15,8 @@ import { URLCombiner } from '../url-combiner/url-combiner'; import { DataService } from './data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { AuthService } from '../auth/auth.service'; @Injectable() export class ItemDataService extends DataService { @@ -26,7 +28,8 @@ export class ItemDataService extends DataService { protected rdbService: RemoteDataBuildService, protected store: Store, private bs: BrowseService, - protected halService: HALEndpointService) { + protected halService: HALEndpointService, + protected authService: AuthService) { super(); } @@ -41,4 +44,9 @@ export class ItemDataService extends DataService { } } + buildCreateParams(dso: Item): Observable { + // TODO: Build parameters for creating an Item on the REST service + return undefined; + } + } From 260ddd979a84e6a71ef4037f83c722927d7a9a6e Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 7 Sep 2018 11:07:04 +0200 Subject: [PATCH 15/69] 54472: request created dso --- .../create-community-page.component.ts | 6 +----- src/app/core/data/data.service.ts | 15 +++++---------- src/app/core/data/dso-response-parsing.service.ts | 8 ++++++-- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index 04d5b8fd39..19e777a454 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -1,12 +1,8 @@ import { Component } from '@angular/core'; import { Community } from '../../core/shared/community.model'; -import { ComColDataService } from '../../core/data/comcol-data.service'; -import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model'; import { CommunityDataService } from '../../core/data/community-data.service'; -import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; -import { DSOSuccessResponse, ErrorResponse, RestResponse } from '../../core/cache/response-cache.models'; +import { ErrorResponse } from '../../core/cache/response-cache.models'; import { Observable } from 'rxjs/Observable'; -import { map } from 'rxjs/operators'; import { RouteService } from '../../shared/services/route.service'; import { Router } from '@angular/router'; import { RemoteData } from '../../core/data/remote-data'; diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index b3b69fdd54..d03891065b 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -18,20 +18,17 @@ import { } from './request.models'; import { RequestService } from './request.service'; import { NormalizedObject } from '../cache/models/normalized-object.model'; -import { distinctUntilChanged, map, share, withLatestFrom } from 'rxjs/operators'; +import { distinctUntilChanged, map, share, switchMap, withLatestFrom } from 'rxjs/operators'; import { configureRequest, filterSuccessfulResponses, - getRequestFromSelflink, getResponseFromSelflink } from '../shared/operators'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpHeaders } from '@angular/common/http'; -import { ErrorResponse, GenericSuccessResponse } from '../cache/response-cache.models'; -import { BrowseEntry } from '../shared/browse-entry.model'; +import { DSOSuccessResponse } from '../cache/response-cache.models'; import { AuthService } from '../auth/auth.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; export abstract class DataService { protected abstract responseCache: ResponseCacheService; @@ -130,18 +127,16 @@ export abstract class DataService ); const href$ = request$.pipe(map((request: RestRequest) => request.href)); - - const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache)); - const payload$ = responseCache$.pipe( + const dsoHref$ = responseCache$.pipe( filterSuccessfulResponses(), map((entry: ResponseCacheEntry) => entry.response), - map((response: GenericSuccessResponse) => response.payload), + map((response: DSOSuccessResponse) => response.resourceSelfLinks[0]), distinctUntilChanged() ); - return this.rdbService.toRemoteDataObservable(requestEntry$, responseCache$, payload$); + return this.rdbService.buildSingle(dsoHref$); } public abstract buildCreateParams(dso: TDomain): Observable; diff --git a/src/app/core/data/dso-response-parsing.service.ts b/src/app/core/data/dso-response-parsing.service.ts index 9651eb3157..e2d930d88c 100644 --- a/src/app/core/data/dso-response-parsing.service.ts +++ b/src/app/core/data/dso-response-parsing.service.ts @@ -12,6 +12,7 @@ import { RestRequest } from './request.models'; import { ResponseParsingService } from './parsing.service'; import { BaseResponseParsingService } from './base-response-parsing.service'; +import { isNotEmpty } from '../../shared/empty.util'; @Injectable() export class DSOResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { @@ -27,8 +28,11 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { const processRequestDTO = this.process(data.payload, request.href); - const selfLinks = this.flattenSingleKeyObject(processRequestDTO).map((no) => no.self); - return new DSOSuccessResponse(selfLinks, data.statusCode, this.processPageInfo(data.payload)) + let selfLinks = []; + if (processRequestDTO !== undefined) { + selfLinks = this.flattenSingleKeyObject(processRequestDTO).map((no) => no.self); + } + return new DSOSuccessResponse(selfLinks, data.statusCode, (isNotEmpty(data.payload) ? this.processPageInfo(data.payload) : null)) } } From 7a2e0ae851a41902e9b2d1b8abb2720bd46e1217 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 7 Sep 2018 14:32:43 +0200 Subject: [PATCH 16/69] 54472: Post-merge error fixes --- src/app/core/data/dspace-object-data.service.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index 39feea4c30..bf2da345b8 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -10,6 +10,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { DataService } from './data.service'; import { RemoteData } from './remote-data'; import { RequestService } from './request.service'; +import { AuthService } from '../auth/auth.service'; /* tslint:disable:max-classes-per-file */ class DataServiceImpl extends DataService { @@ -20,7 +21,8 @@ class DataServiceImpl extends DataService protected requestService: RequestService, protected rdbService: RemoteDataBuildService, protected store: Store, - protected halService: HALEndpointService) { + protected halService: HALEndpointService, + protected authService: AuthService) { super(); } @@ -31,6 +33,10 @@ class DataServiceImpl extends DataService getFindByIDHref(endpoint, resourceID): string { return endpoint.replace(/\{\?uuid\}/,`?uuid=${resourceID}`); } + + buildCreateParams(dso: DSpaceObject): Observable { + return undefined; + } } @Injectable() @@ -41,8 +47,9 @@ export class DSpaceObjectDataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected halService: HALEndpointService) { - this.dataService = new DataServiceImpl(null, requestService, rdbService, null, halService); + protected halService: HALEndpointService, + protected authService: AuthService) { + this.dataService = new DataServiceImpl(null, requestService, rdbService, null, halService, authService); } findById(uuid: string): Observable> { From ea075fbd8f286b11f3760b5eaec153b3ed0230f9 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 10 Sep 2018 08:44:56 +0200 Subject: [PATCH 17/69] Correct parsing of AuthStatus and small changes --- src/app/core/auth/auth-response-parsing.service.ts | 5 +++-- src/app/core/auth/auth.service.ts | 2 +- src/app/core/auth/models/auth-status.model.ts | 2 +- src/app/core/auth/models/normalized-auth-status.model.ts | 2 +- src/app/core/auth/server-auth.service.ts | 2 +- src/app/shared/testing/auth-request-service-stub.ts | 4 ++-- src/app/shared/testing/auth-service-stub.ts | 2 +- 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/app/core/auth/auth-response-parsing.service.ts b/src/app/core/auth/auth-response-parsing.service.ts index 80c1b2eeca..f024035c06 100644 --- a/src/app/core/auth/auth-response-parsing.service.ts +++ b/src/app/core/auth/auth-response-parsing.service.ts @@ -26,8 +26,9 @@ export class AuthResponseParsingService extends BaseResponseParsingService imple parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '200' || data.statusCode === 'OK')) { - const response = this.process(data.payload, request.href); - return new AuthStatusResponse(response[Object.keys(response)[0]][0], data.statusCode); + const response: AuthStatus = this.process(data.payload, request.href); + response.eperson = data.payload._embedded.eperson; + return new AuthStatusResponse(response, data.statusCode); } else { return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode); } diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 2848b54b50..2eb6736d89 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -126,7 +126,7 @@ export class AuthService { return this.authRequestService.getRequest('status', options) .map((status: AuthStatus) => { if (status.authenticated) { - return status.eperson[0]; + return status.eperson; } else { throw(new Error('Not authenticated')); } diff --git a/src/app/core/auth/models/auth-status.model.ts b/src/app/core/auth/models/auth-status.model.ts index 22c9d14718..9d69c18388 100644 --- a/src/app/core/auth/models/auth-status.model.ts +++ b/src/app/core/auth/models/auth-status.model.ts @@ -13,7 +13,7 @@ export class AuthStatus { error?: AuthError; - eperson: Eperson[]; + eperson: Eperson; token?: AuthTokenInfo; diff --git a/src/app/core/auth/models/normalized-auth-status.model.ts b/src/app/core/auth/models/normalized-auth-status.model.ts index 19952f7c70..c63c611a75 100644 --- a/src/app/core/auth/models/normalized-auth-status.model.ts +++ b/src/app/core/auth/models/normalized-auth-status.model.ts @@ -21,6 +21,6 @@ export class NormalizedAuthStatus extends NormalizedDSpaceObject { authenticated: boolean; @autoserializeAs(Eperson) - eperson: Eperson[]; + eperson: Eperson; } diff --git a/src/app/core/auth/server-auth.service.ts b/src/app/core/auth/server-auth.service.ts index 96ee2e355a..00dfbc5615 100644 --- a/src/app/core/auth/server-auth.service.ts +++ b/src/app/core/auth/server-auth.service.ts @@ -35,7 +35,7 @@ export class ServerAuthService extends AuthService { return this.authRequestService.getRequest('status', options) .map((status: AuthStatus) => { if (status.authenticated) { - return status.eperson[0]; + return status.eperson; } else { throw(new Error('Not authenticated')); } diff --git a/src/app/shared/testing/auth-request-service-stub.ts b/src/app/shared/testing/auth-request-service-stub.ts index 2c47068af4..9703f49967 100644 --- a/src/app/shared/testing/auth-request-service-stub.ts +++ b/src/app/shared/testing/auth-request-service-stub.ts @@ -26,7 +26,7 @@ export class AuthRequestServiceStub { if (this.validateToken(token)) { authStatusStub.authenticated = true; authStatusStub.token = this.mockTokenInfo; - authStatusStub.eperson = [this.mockUser]; + authStatusStub.eperson = this.mockUser; } else { authStatusStub.authenticated = false; } @@ -45,7 +45,7 @@ export class AuthRequestServiceStub { if (this.validateToken(token)) { authStatusStub.authenticated = true; authStatusStub.token = this.mockTokenInfo; - authStatusStub.eperson = [this.mockUser]; + authStatusStub.eperson = this.mockUser; } else { authStatusStub.authenticated = false; } diff --git a/src/app/shared/testing/auth-service-stub.ts b/src/app/shared/testing/auth-service-stub.ts index c7d5556910..9e830930c1 100644 --- a/src/app/shared/testing/auth-service-stub.ts +++ b/src/app/shared/testing/auth-service-stub.ts @@ -19,7 +19,7 @@ export class AuthServiceStub { authStatus.okay = true; authStatus.authenticated = true; authStatus.token = this.token; - authStatus.eperson = [EpersonMock]; + authStatus.eperson = EpersonMock; return Observable.of(authStatus); } else { console.log('error'); From 2f139c5c9410a09d6ca8b4f97da969e390a68623 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 10 Sep 2018 12:03:00 +0200 Subject: [PATCH 18/69] 54472: Create request, response, responseparsingservice and data-service changes --- src/app/core/cache/response-cache.models.ts | 11 +++++ src/app/core/core.module.ts | 2 + src/app/core/data/data.service.ts | 17 +++++--- src/app/core/data/request.models.ts | 12 ++++++ .../single-dso-response-parsing.service.ts | 43 +++++++++++++++++++ 5 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 src/app/core/data/single-dso-response-parsing.service.ts diff --git a/src/app/core/cache/response-cache.models.ts b/src/app/core/cache/response-cache.models.ts index 9b1b5b89eb..03c48b62fe 100644 --- a/src/app/core/cache/response-cache.models.ts +++ b/src/app/core/cache/response-cache.models.ts @@ -10,6 +10,7 @@ import { MetadataSchema } from '../metadata/metadataschema.model'; import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model'; import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model'; import { AuthStatus } from '../auth/models/auth-status.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; /* tslint:disable:max-classes-per-file */ export class RestResponse { @@ -32,6 +33,16 @@ export class DSOSuccessResponse extends RestResponse { } } +export class SingleDSOSuccessResponse extends DSOSuccessResponse { + constructor( + public resourceSelfLinks: string[], + public statusCode: string, + public dso: DSpaceObject + ) { + super(resourceSelfLinks, statusCode); + } +} + export class RegistryMetadataschemasSuccessResponse extends RestResponse { constructor( public metadataschemasResponse: RegistryMetadataschemasResponse, diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index dabdfba0ab..61568b99a6 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -63,6 +63,7 @@ import { RegistryBitstreamformatsResponseParsingService } from './data/registry- import { NotificationsService } from '../shared/notifications/notifications.service'; import { UploaderService } from '../shared/uploader/uploader.service'; import { DSpaceObjectDataService } from './data/dspace-object-data.service'; +import { SingleDsoResponseParsingService } from './data/single-dso-response-parsing.service'; const IMPORTS = [ CommonModule, @@ -126,6 +127,7 @@ const PROVIDERS = [ UploaderService, UUIDService, DSpaceObjectDataService, + SingleDsoResponseParsingService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 6a652dbf99..a07adc0552 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -9,6 +9,7 @@ import { URLCombiner } from '../url-combiner/url-combiner'; import { PaginatedList } from './paginated-list'; import { RemoteData } from './remote-data'; import { + CreateRequest, FindAllOptions, FindAllRequest, FindByIDRequest, @@ -21,14 +22,15 @@ import { NormalizedObject } from '../cache/models/normalized-object.model'; import { distinctUntilChanged, map, share, switchMap, withLatestFrom } from 'rxjs/operators'; import { configureRequest, - filterSuccessfulResponses, + filterSuccessfulResponses, getRequestFromSelflink, getResponseFromSelflink } from '../shared/operators'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpHeaders } from '@angular/common/http'; -import { DSOSuccessResponse } from '../cache/response-cache.models'; +import { DSOSuccessResponse, SingleDSOSuccessResponse } from '../cache/response-cache.models'; import { AuthService } from '../auth/auth.service'; +import { DSpaceObject } from '../shared/dspace-object.model'; export abstract class DataService { protected abstract responseCache: ResponseCacheService; @@ -113,7 +115,7 @@ export abstract class DataService return this.rdbService.buildSingle(href); } - public create(dso: TDomain): Observable> { + public create(dso: TDomain): Observable> { const request$ = this.halService.getEndpoint(this.linkPath).pipe( isNotEmptyOperator(), distinctUntilChanged(), @@ -123,23 +125,24 @@ export abstract class DataService const headers = new HttpHeaders(); headers.append('Authentication', this.authService.buildAuthHeader()); options.headers = headers; - return new PostRequest(this.requestService.generateRequestId(), endpointURL + params, options); + return new CreateRequest(this.requestService.generateRequestId(), endpointURL + params, options); }), configureRequest(this.requestService), share() ); const href$ = request$.pipe(map((request: RestRequest) => request.href)); + const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache)); - const dsoHref$ = responseCache$.pipe( + const payload$ = responseCache$.pipe( filterSuccessfulResponses(), map((entry: ResponseCacheEntry) => entry.response), - map((response: DSOSuccessResponse) => response.resourceSelfLinks[0]), + map((response: SingleDSOSuccessResponse) => response.dso), distinctUntilChanged() ); - return this.rdbService.buildSingle(dsoHref$); + return this.rdbService.toRemoteDataObservable(requestEntry$, responseCache$, payload$); } public abstract buildCreateParams(dso: TDomain): Observable; diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index ce45d5b41c..f5b9127a6b 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -12,6 +12,7 @@ import { AuthResponseParsingService } from '../auth/auth-response-parsing.servic import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpHeaders } from '@angular/common/http'; import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service'; +import { SingleDsoResponseParsingService } from './single-dso-response-parsing.service'; /* tslint:disable:max-classes-per-file */ @@ -223,6 +224,17 @@ export class IntegrationRequest extends GetRequest { return IntegrationResponseParsingService; } } + +export class CreateRequest extends PostRequest { + constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) { + super(uuid, href, body, options); + } + + getResponseParser(): GenericConstructor { + return SingleDsoResponseParsingService; + } +} + export class RequestError extends Error { statusText: string; } diff --git a/src/app/core/data/single-dso-response-parsing.service.ts b/src/app/core/data/single-dso-response-parsing.service.ts new file mode 100644 index 0000000000..6b9817375c --- /dev/null +++ b/src/app/core/data/single-dso-response-parsing.service.ts @@ -0,0 +1,43 @@ +import { Inject, Injectable } from '@angular/core'; +import { ResponseParsingService } from './parsing.service'; +import { BaseResponseParsingService } from './base-response-parsing.service'; +import { GlobalConfig } from '../../../config/global-config.interface'; +import { GLOBAL_CONFIG } from '../../../config'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { RestRequest } from './request.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { DSOSuccessResponse, RestResponse, SingleDSOSuccessResponse } from '../cache/response-cache.models'; +import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; +import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { NormalizedObject } from '../cache/models/normalized-object.model'; +import { ResourceType } from '../shared/resource-type'; +import { hasNoValue, hasValue } from '../../shared/empty.util'; + +@Injectable() +export class SingleDsoResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { + protected objectFactory = NormalizedObjectFactory; + protected toCache = true; + + constructor( + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + protected objectCache: ObjectCacheService, + ) { super(); + } + + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + const processRequestDTO = this.process(data.payload, request.href); + if (hasNoValue(processRequestDTO)) { + return new SingleDSOSuccessResponse([], data.statusCode, undefined) + } + let objectList = processRequestDTO; + if (hasValue(processRequestDTO.page)) { + objectList = processRequestDTO.page; + } else if (!Array.isArray(processRequestDTO)) { + objectList = [processRequestDTO]; + } + const selfLinks = objectList.map((no) => no.self); + return new SingleDSOSuccessResponse(selfLinks, data.statusCode, processRequestDTO); + } + +} From 7b3e82c5afc140a94b125f5b577531078f2173c2 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 10 Sep 2018 13:48:34 +0200 Subject: [PATCH 19/69] 54472: Removed SingleDsoSuccessResponse and replaced by GenericSuccessResponse --- .../create-collection-page.component.ts | 3 ++- .../create-community-page.component.ts | 3 ++- src/app/core/cache/response-cache.models.ts | 10 ---------- src/app/core/data/data.service.ts | 8 +++++--- .../data/single-dso-response-parsing.service.ts | 17 +++++++---------- 5 files changed, 16 insertions(+), 25 deletions(-) diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index 8f31d30441..f94547c8fb 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -13,6 +13,7 @@ import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; import { map } from 'rxjs/operators'; import { RemoteData } from '../../core/data/remote-data'; import { isNotEmpty } from '../../shared/empty.util'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; @Component({ selector: 'ds-create-collection', @@ -50,7 +51,7 @@ export class CreateCollectionPageComponent { uuid: uuid }))) }); - this.collectionDataService.create(collection).subscribe((rd: RemoteData) => { + this.collectionDataService.create(collection).subscribe((rd: RemoteData) => { console.log(rd); if (rd.hasSucceeded) { this.router.navigateByUrl(''); diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index 19e777a454..b6c6fb3c13 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -7,6 +7,7 @@ import { RouteService } from '../../shared/services/route.service'; import { Router } from '@angular/router'; import { RemoteData } from '../../core/data/remote-data'; import { isNotEmpty } from '../../shared/empty.util'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; @Component({ selector: 'ds-create-community', @@ -43,7 +44,7 @@ export class CreateCommunityPageComponent { uuid: uuid }))) }); - this.communityDataService.create(community).subscribe((rd: RemoteData) => { + this.communityDataService.create(community).subscribe((rd: RemoteData) => { console.log(rd); if (rd.hasSucceeded) { this.router.navigateByUrl(''); diff --git a/src/app/core/cache/response-cache.models.ts b/src/app/core/cache/response-cache.models.ts index 03c48b62fe..94ddd48f83 100644 --- a/src/app/core/cache/response-cache.models.ts +++ b/src/app/core/cache/response-cache.models.ts @@ -33,16 +33,6 @@ export class DSOSuccessResponse extends RestResponse { } } -export class SingleDSOSuccessResponse extends DSOSuccessResponse { - constructor( - public resourceSelfLinks: string[], - public statusCode: string, - public dso: DSpaceObject - ) { - super(resourceSelfLinks, statusCode); - } -} - export class RegistryMetadataschemasSuccessResponse extends RestResponse { constructor( public metadataschemasResponse: RegistryMetadataschemasResponse, diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index a07adc0552..e245760d81 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -28,7 +28,7 @@ import { import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpHeaders } from '@angular/common/http'; -import { DSOSuccessResponse, SingleDSOSuccessResponse } from '../cache/response-cache.models'; +import { DSOSuccessResponse, GenericSuccessResponse } from '../cache/response-cache.models'; import { AuthService } from '../auth/auth.service'; import { DSpaceObject } from '../shared/dspace-object.model'; @@ -115,7 +115,7 @@ export abstract class DataService return this.rdbService.buildSingle(href); } - public create(dso: TDomain): Observable> { + public create(dso: TDomain): Observable> { const request$ = this.halService.getEndpoint(this.linkPath).pipe( isNotEmptyOperator(), distinctUntilChanged(), @@ -138,10 +138,12 @@ export abstract class DataService const payload$ = responseCache$.pipe( filterSuccessfulResponses(), map((entry: ResponseCacheEntry) => entry.response), - map((response: SingleDSOSuccessResponse) => response.dso), + map((response: GenericSuccessResponse) => response.payload), distinctUntilChanged() ); + payload$.subscribe((value) => console.log(value)); + return this.rdbService.toRemoteDataObservable(requestEntry$, responseCache$, payload$); } diff --git a/src/app/core/data/single-dso-response-parsing.service.ts b/src/app/core/data/single-dso-response-parsing.service.ts index 6b9817375c..03b9f5bea2 100644 --- a/src/app/core/data/single-dso-response-parsing.service.ts +++ b/src/app/core/data/single-dso-response-parsing.service.ts @@ -6,7 +6,11 @@ import { GLOBAL_CONFIG } from '../../../config'; import { ObjectCacheService } from '../cache/object-cache.service'; import { RestRequest } from './request.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSOSuccessResponse, RestResponse, SingleDSOSuccessResponse } from '../cache/response-cache.models'; +import { + DSOSuccessResponse, + GenericSuccessResponse, + RestResponse +} from '../cache/response-cache.models'; import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { DSpaceObject } from '../shared/dspace-object.model'; @@ -28,16 +32,9 @@ export class SingleDsoResponseParsingService extends BaseResponseParsingService parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { const processRequestDTO = this.process(data.payload, request.href); if (hasNoValue(processRequestDTO)) { - return new SingleDSOSuccessResponse([], data.statusCode, undefined) + return new GenericSuccessResponse(undefined, data.statusCode) } - let objectList = processRequestDTO; - if (hasValue(processRequestDTO.page)) { - objectList = processRequestDTO.page; - } else if (!Array.isArray(processRequestDTO)) { - objectList = [processRequestDTO]; - } - const selfLinks = objectList.map((no) => no.self); - return new SingleDSOSuccessResponse(selfLinks, data.statusCode, processRequestDTO); + return new GenericSuccessResponse(processRequestDTO, data.statusCode); } } From 7f8fa9c1b7a8e072931416e6d3f2bae15b2f1f0b Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 10 Sep 2018 15:20:16 +0200 Subject: [PATCH 20/69] 54472: GetRequestByUUID --- src/app/core/data/data.service.ts | 9 +++++---- src/app/core/shared/operators.ts | 7 +++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index e245760d81..045f98a14f 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -22,7 +22,7 @@ import { NormalizedObject } from '../cache/models/normalized-object.model'; import { distinctUntilChanged, map, share, switchMap, withLatestFrom } from 'rxjs/operators'; import { configureRequest, - filterSuccessfulResponses, getRequestFromSelflink, + filterSuccessfulResponses, getRequestFromSelflink, getRequestFromUUID, getResponseFromSelflink } from '../shared/operators'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; @@ -31,6 +31,8 @@ import { HttpHeaders } from '@angular/common/http'; import { DSOSuccessResponse, GenericSuccessResponse } from '../cache/response-cache.models'; import { AuthService } from '../auth/auth.service'; import { DSpaceObject } from '../shared/dspace-object.model'; +import { first } from 'rxjs/operator/first'; +import { take } from 'rxjs/operator/take'; export abstract class DataService { protected abstract responseCache: ResponseCacheService; @@ -132,7 +134,8 @@ export abstract class DataService ); const href$ = request$.pipe(map((request: RestRequest) => request.href)); - const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); + const uuid$ = request$.pipe(map((request: RestRequest) => request.uuid)); + const requestEntry$ = uuid$.pipe(getRequestFromUUID(this.requestService)); const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache)); const payload$ = responseCache$.pipe( @@ -142,8 +145,6 @@ export abstract class DataService distinctUntilChanged() ); - payload$.subscribe((value) => console.log(value)); - return this.rdbService.toRemoteDataObservable(requestEntry$, responseCache$, payload$); } diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index fcb4c2c256..350a664e9d 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -23,6 +23,13 @@ export const getRequestFromSelflink = (requestService: RequestService) => hasValueOperator() ); +export const getRequestFromUUID = (requestService: RequestService) => + (source: Observable): Observable => + source.pipe( + flatMap((uuid: string) => requestService.getByUUID(uuid)), + hasValueOperator() + ); + export const getResponseFromSelflink = (responseCache: ResponseCacheService) => (source: Observable): Observable => source.pipe( From 79afa38a67e136ffdf74067304cb4b343f18d9a8 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 11 Sep 2018 08:12:16 +0200 Subject: [PATCH 21/69] 54472: Fixed duplicate requests --- .../create-community-page.component.ts | 5 +-- src/app/core/data/data.service.ts | 34 +++++++++++-------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index b6c6fb3c13..69012ca2a4 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -8,6 +8,7 @@ import { Router } from '@angular/router'; import { RemoteData } from '../../core/data/remote-data'; import { isNotEmpty } from '../../shared/empty.util'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { take } from 'rxjs/operators'; @Component({ selector: 'ds-create-community', @@ -21,7 +22,7 @@ export class CreateCommunityPageComponent { public communityRDObs: Observable>; public constructor(private communityDataService: CommunityDataService, private routeService: RouteService, private router: Router) { - this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); + this.parentUUID$ = this.routeService.getQueryParameterValue('parent').pipe(take(1)); this.parentUUID$.subscribe((uuid: string) => { if (isNotEmpty(uuid)) { this.communityRDObs = this.communityDataService.findById(uuid); @@ -44,7 +45,7 @@ export class CreateCommunityPageComponent { uuid: uuid }))) }); - this.communityDataService.create(community).subscribe((rd: RemoteData) => { + this.communityDataService.create(community).pipe(take(1)).subscribe((rd: RemoteData) => { console.log(rd); if (rd.hasSucceeded) { this.router.navigateByUrl(''); diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 045f98a14f..a29cb7877f 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -19,7 +19,7 @@ import { } from './request.models'; import { RequestService } from './request.service'; import { NormalizedObject } from '../cache/models/normalized-object.model'; -import { distinctUntilChanged, map, share, switchMap, withLatestFrom } from 'rxjs/operators'; +import { distinctUntilChanged, map, share, switchMap, take, withLatestFrom } from 'rxjs/operators'; import { configureRequest, filterSuccessfulResponses, getRequestFromSelflink, getRequestFromUUID, @@ -30,9 +30,8 @@ import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpHeaders } from '@angular/common/http'; import { DSOSuccessResponse, GenericSuccessResponse } from '../cache/response-cache.models'; import { AuthService } from '../auth/auth.service'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import { first } from 'rxjs/operator/first'; -import { take } from 'rxjs/operator/take'; +import { Collection } from '../shared/collection.model'; +import { Community } from '../shared/community.model'; export abstract class DataService { protected abstract responseCache: ResponseCacheService; @@ -118,33 +117,38 @@ export abstract class DataService } public create(dso: TDomain): Observable> { - const request$ = this.halService.getEndpoint(this.linkPath).pipe( + const requestId = this.requestService.generateRequestId(); + const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe( isNotEmptyOperator(), distinctUntilChanged(), withLatestFrom(this.buildCreateParams(dso)), - map(([endpointURL, params]) => { + map(([endpointURL, params]) => endpointURL + params) + ); + + const request$ = endpoint$.pipe( + take(1), + map((endpoint) => { const options: HttpOptions = Object.create({}); const headers = new HttpHeaders(); headers.append('Authentication', this.authService.buildAuthHeader()); options.headers = headers; - return new CreateRequest(this.requestService.generateRequestId(), endpointURL + params, options); + return new CreateRequest(requestId, endpoint, options); }), - configureRequest(this.requestService), - share() + configureRequest(this.requestService) ); - const href$ = request$.pipe(map((request: RestRequest) => request.href)); - const uuid$ = request$.pipe(map((request: RestRequest) => request.uuid)); - const requestEntry$ = uuid$.pipe(getRequestFromUUID(this.requestService)); - const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache)); - - const payload$ = responseCache$.pipe( + const payload$ = request$.pipe( + map((request: RestRequest) => request.href), + getResponseFromSelflink(this.responseCache), filterSuccessfulResponses(), map((entry: ResponseCacheEntry) => entry.response), map((response: GenericSuccessResponse) => response.payload), distinctUntilChanged() ); + const requestEntry$ = this.requestService.getByUUID(requestId); + const responseCache$ = endpoint$.pipe(getResponseFromSelflink(this.responseCache)); + return this.rdbService.toRemoteDataObservable(requestEntry$, responseCache$, payload$); } From 12058cb4dbcb2ec8135dfa8f57b20b7448f17307 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 11 Sep 2018 08:31:41 +0200 Subject: [PATCH 22/69] 54472: Remove console logs --- .../create-collection-page/create-collection-page.component.ts | 1 - .../create-community-page/create-community-page.component.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index f94547c8fb..1c2f78bb06 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -52,7 +52,6 @@ export class CreateCollectionPageComponent { }))) }); this.collectionDataService.create(collection).subscribe((rd: RemoteData) => { - console.log(rd); if (rd.hasSucceeded) { this.router.navigateByUrl(''); } diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index 69012ca2a4..c79b6fa9cd 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -46,7 +46,6 @@ export class CreateCommunityPageComponent { }))) }); this.communityDataService.create(community).pipe(take(1)).subscribe((rd: RemoteData) => { - console.log(rd); if (rd.hasSucceeded) { this.router.navigateByUrl(''); } From dfb4a3bd9c28889f7599a86d03f03477256440c1 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 11 Sep 2018 09:21:35 +0200 Subject: [PATCH 23/69] 54472: Notification errors --- src/app/core/data/collection-data.service.ts | 3 ++- src/app/core/data/comcol-data.service.spec.ts | 1 + src/app/core/data/community-data.service.ts | 4 +++- src/app/core/data/data.service.spec.ts | 8 ++++++-- src/app/core/data/data.service.ts | 12 +++++++++++- src/app/core/data/dspace-object-data.service.ts | 9 ++++++--- src/app/core/data/item-data.service.ts | 3 ++- 7 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index dc3635595a..fd2404650f 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -27,7 +27,8 @@ export class CollectionDataService extends ComColDataService { protected objectCache: ObjectCacheService, protected halService: HALEndpointService, protected authService: AuthService, + protected notificationsService: NotificationsService, protected linkPath: string ) { super(); diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index 4570e749d8..4689fac854 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -16,6 +16,7 @@ import { RemoteData } from './remote-data'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { Observable } from 'rxjs/Observable'; import { PaginatedList } from './paginated-list'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; @Injectable() export class CommunityDataService extends ComColDataService { @@ -30,7 +31,8 @@ export class CommunityDataService extends ComColDataService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, - protected authService: AuthService + protected authService: AuthService, + protected notificationsService: NotificationsService ) { super(); } diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index 8377afe92e..8abf4e51ca 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -9,6 +9,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Observable } from 'rxjs/Observable'; import { FindAllOptions } from './request.models'; import { SortOptions, SortDirection } from '../cache/models/sort-options.model'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; const LINK_NAME = 'test' @@ -23,7 +24,8 @@ class TestService extends DataService { protected rdbService: RemoteDataBuildService, protected store: Store, protected linkPath: string, - protected halService: HALEndpointService + protected halService: HALEndpointService, + protected notificationsService: NotificationsService ) { super(); } @@ -41,6 +43,7 @@ describe('DataService', () => { const requestService = {} as RequestService; const halService = {} as HALEndpointService; const rdbService = {} as RemoteDataBuildService; + const notificationsService = {} as NotificationsService; const store = {} as Store; const endpoint = 'https://rest.api/core'; @@ -51,7 +54,8 @@ describe('DataService', () => { rdbService, store, LINK_NAME, - halService + halService, + notificationsService ); } diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index a29cb7877f..9d7422cbe1 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -28,10 +28,12 @@ import { import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpHeaders } from '@angular/common/http'; -import { DSOSuccessResponse, GenericSuccessResponse } from '../cache/response-cache.models'; +import { DSOSuccessResponse, ErrorResponse, GenericSuccessResponse } from '../cache/response-cache.models'; import { AuthService } from '../auth/auth.service'; import { Collection } from '../shared/collection.model'; import { Community } from '../shared/community.model'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; export abstract class DataService { protected abstract responseCache: ResponseCacheService; @@ -41,6 +43,7 @@ export abstract class DataService protected abstract linkPath: string; protected abstract halService: HALEndpointService; protected abstract authService: AuthService; + protected abstract notificationsService: NotificationsService; public abstract getScopedEndpoint(scope: string): Observable @@ -140,6 +143,13 @@ export abstract class DataService const payload$ = request$.pipe( map((request: RestRequest) => request.href), getResponseFromSelflink(this.responseCache), + map((response: ResponseCacheEntry) => { + if (!response.response.isSuccessful && response.response instanceof ErrorResponse) { + const errorResponse: ErrorResponse = response.response; + this.notificationsService.error('Server Error:', errorResponse.errorMessage, new NotificationOptions(-1)); + } + return response; + }), filterSuccessfulResponses(), map((entry: ResponseCacheEntry) => entry.response), map((response: GenericSuccessResponse) => response.payload), diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index bf2da345b8..eb657e1737 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -11,6 +11,7 @@ import { DataService } from './data.service'; import { RemoteData } from './remote-data'; import { RequestService } from './request.service'; import { AuthService } from '../auth/auth.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; /* tslint:disable:max-classes-per-file */ class DataServiceImpl extends DataService { @@ -22,7 +23,8 @@ class DataServiceImpl extends DataService protected rdbService: RemoteDataBuildService, protected store: Store, protected halService: HALEndpointService, - protected authService: AuthService) { + protected authService: AuthService, + protected notificationsService: NotificationsService) { super(); } @@ -48,8 +50,9 @@ export class DSpaceObjectDataService { protected requestService: RequestService, protected rdbService: RemoteDataBuildService, protected halService: HALEndpointService, - protected authService: AuthService) { - this.dataService = new DataServiceImpl(null, requestService, rdbService, null, halService, authService); + protected authService: AuthService, + protected notificationsService: NotificationsService) { + this.dataService = new DataServiceImpl(null, requestService, rdbService, null, halService, authService, notificationsService); } findById(uuid: string): Observable> { diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index d1db71b78d..448d3fc84b 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -29,7 +29,8 @@ export class ItemDataService extends DataService { protected store: Store, private bs: BrowseService, protected halService: HALEndpointService, - protected authService: AuthService) { + protected authService: AuthService, + protected notificationsService: NotificationsService) { super(); } From 2740f690250a092e2be093b687d3e5090e113bff Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 11 Sep 2018 09:22:45 +0200 Subject: [PATCH 24/69] 54472: Removed old error messages --- .../create-collection-page.component.html | 4 ---- .../create-collection-page.component.ts | 1 - .../create-community-page.component.html | 4 ---- .../create-community-page/create-community-page.component.ts | 1 - 4 files changed, 10 deletions(-) diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.html b/src/app/+collection-page/create-collection-page/create-collection-page.component.html index 11f4f53ecd..1d1f8deb6c 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.html +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.html @@ -7,9 +7,5 @@
-
- {{error.statusCode}} -

{{error.errorMessage}}

-
diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index 1c2f78bb06..0f79558a44 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -22,7 +22,6 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model'; }) export class CreateCollectionPageComponent { - public error$: Observable; public parentUUID$: Observable; public communityRDObs: Observable>; diff --git a/src/app/+community-page/create-community-page/create-community-page.component.html b/src/app/+community-page/create-community-page/create-community-page.component.html index 06fd643530..35f1deaa73 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.html +++ b/src/app/+community-page/create-community-page/create-community-page.component.html @@ -7,9 +7,5 @@
-
- {{error.statusCode}} -

{{error.errorMessage}}

-
diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index c79b6fa9cd..9a4730f74f 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -17,7 +17,6 @@ import { take } from 'rxjs/operators'; }) export class CreateCommunityPageComponent { - public error$: Observable; public parentUUID$: Observable; public communityRDObs: Observable>; From ad9852f1941609f9364affc04ab6cc32f13b5364 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 11 Sep 2018 14:50:23 +0200 Subject: [PATCH 25/69] 54472: form-data intermediate commit --- src/app/core/data/comcol-data.service.ts | 14 +++++++---- src/app/core/data/data.service.ts | 24 ++++++++----------- .../core/data/dspace-object-data.service.ts | 2 +- src/app/core/data/item-data.service.ts | 4 ++-- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index c47d48dc07..c9dff8fbe1 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -66,19 +66,23 @@ export abstract class ComColDataService { + public buildCreateBody(comcol): Observable { return comcol.owner.pipe( map((rd: RemoteData) => { - let urlParams = '?name=' + comcol.name; + const form: any = { + name: comcol.name + }; if (rd.payload.id) { - urlParams += '&parent=' + rd.payload.id; + form.parent = rd.payload.id; } if (comcol.metadata) { for (const i of Object.keys(comcol.metadata)) { - urlParams += '&' + comcol.metadata[i].key + '=' + comcol.metadata[i].value; + if (isNotEmpty(comcol.metadata[i].value)) { + form[comcol.metadata[i].key] = comcol.metadata[i].value; + } } } - return urlParams; + return form; }) ); } diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 9d7422cbe1..3dc32e9e69 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -14,24 +14,21 @@ import { FindAllRequest, FindByIDRequest, GetRequest, - PostRequest, RestRequest } from './request.models'; import { RequestService } from './request.service'; import { NormalizedObject } from '../cache/models/normalized-object.model'; -import { distinctUntilChanged, map, share, switchMap, take, withLatestFrom } from 'rxjs/operators'; +import { distinctUntilChanged, map, take, withLatestFrom } from 'rxjs/operators'; import { configureRequest, - filterSuccessfulResponses, getRequestFromSelflink, getRequestFromUUID, + filterSuccessfulResponses, getResponseFromSelflink } from '../shared/operators'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpHeaders } from '@angular/common/http'; -import { DSOSuccessResponse, ErrorResponse, GenericSuccessResponse } from '../cache/response-cache.models'; +import { ErrorResponse, GenericSuccessResponse } from '../cache/response-cache.models'; import { AuthService } from '../auth/auth.service'; -import { Collection } from '../shared/collection.model'; -import { Community } from '../shared/community.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; @@ -123,19 +120,18 @@ export abstract class DataService const requestId = this.requestService.generateRequestId(); const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe( isNotEmptyOperator(), - distinctUntilChanged(), - withLatestFrom(this.buildCreateParams(dso)), - map(([endpointURL, params]) => endpointURL + params) + distinctUntilChanged() ); const request$ = endpoint$.pipe( take(1), - map((endpoint) => { + withLatestFrom(this.buildCreateBody(dso)), + map(([endpoint, formdata]) => { const options: HttpOptions = Object.create({}); - const headers = new HttpHeaders(); - headers.append('Authentication', this.authService.buildAuthHeader()); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type','multipart/form-data'); options.headers = headers; - return new CreateRequest(requestId, endpoint, options); + return new CreateRequest(requestId, endpoint, formdata, options); }), configureRequest(this.requestService) ); @@ -162,6 +158,6 @@ export abstract class DataService return this.rdbService.toRemoteDataObservable(requestEntry$, responseCache$, payload$); } - public abstract buildCreateParams(dso: TDomain): Observable; + public abstract buildCreateBody(dso: TDomain): Observable; } diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index eb657e1737..8bae4e56ed 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -36,7 +36,7 @@ class DataServiceImpl extends DataService return endpoint.replace(/\{\?uuid\}/,`?uuid=${resourceID}`); } - buildCreateParams(dso: DSpaceObject): Observable { + buildCreateBody(dso: DSpaceObject): Observable { return undefined; } } diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 448d3fc84b..343d4efede 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -45,8 +45,8 @@ export class ItemDataService extends DataService { } } - buildCreateParams(dso: Item): Observable { - // TODO: Build parameters for creating an Item on the REST service + buildCreateBody(dso: Item): Observable { + // TODO: Build http body for creating an Item on the REST service return undefined; } From acb5f5197d693697ddfa4762e1c89569822f5706 Mon Sep 17 00:00:00 2001 From: lotte Date: Tue, 11 Sep 2018 16:05:21 +0200 Subject: [PATCH 26/69] patching changes --- package.json | 5 +-- src/app/core/cache/object-cache.actions.ts | 31 +++++++++++++++-- src/app/core/cache/object-cache.reducer.ts | 32 ++++++++++++++++-- src/app/core/cache/object-cache.service.ts | 39 +++++++++++++++++++--- src/app/shared/form/form.reducer.ts | 1 - yarn.lock | 6 ++++ 6 files changed, 102 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index dca2420c62..c725029a12 100644 --- a/package.json +++ b/package.json @@ -41,8 +41,8 @@ "server:watch": "nodemon dist/server.js", "server:watch:debug": "nodemon --debug dist/server.js", "webpack:watch": "webpack -w --mode development", - "watch": "yarn run build && npm-run-all -p webpack:watch server:watch --mode development", - "watch:debug": "yarn run build && npm-run-all -p webpack:watch server:watch:debug --mode development", + "watch": "yarn run build && npm-run-all -p webpack:watch server:watch", + "watch:debug": "yarn run build && npm-run-all -p webpack:watch server:watch:debug", "predebug": "yarn run build", "predebug:server": "yarn run build", "debug": "node --debug-brk dist/server.js", @@ -96,6 +96,7 @@ "core-js": "2.5.3", "express": "4.16.2", "express-session": "1.15.6", + "fast-json-patch": "^2.0.7", "font-awesome": "4.7.0", "http-server": "0.11.1", "https": "1.0.0", diff --git a/src/app/core/cache/object-cache.actions.ts b/src/app/core/cache/object-cache.actions.ts index a136b04248..957c0f7269 100644 --- a/src/app/core/cache/object-cache.actions.ts +++ b/src/app/core/cache/object-cache.actions.ts @@ -2,6 +2,7 @@ import { Action } from '@ngrx/store'; import { type } from '../../shared/ngrx/type'; import { CacheableObject } from './object-cache.reducer'; +import { Operation } from 'fast-json-patch'; /** * The list of ObjectCacheAction type definitions @@ -9,7 +10,8 @@ import { CacheableObject } from './object-cache.reducer'; export const ObjectCacheActionTypes = { ADD: type('dspace/core/cache/object/ADD'), 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'), + PATCH: type('dspace/core/cache/object/PATCH') }; /* tslint:disable:max-classes-per-file */ @@ -79,6 +81,30 @@ export class ResetObjectCacheTimestampsAction implements Action { this.payload = newTimestamp; } } + +/** + * An ngrx action to add new operations to a specified cached objects + */ +export class PatchObjectCacheAction implements Action { + type = ObjectCacheActionTypes.PATCH; + payload: { + uuid: string, + operations: Operation[] + }; + + /** + * Create a new PatchObjectCacheAction + * + * @param uuid + * the uuid of the object that should be updated + * @param operations + * the list of operations to add + */ + constructor(uuid: string, operations: Operation[]) { + this.payload = { uuid, operations }; + } +} + /* tslint:enable:max-classes-per-file */ /** @@ -87,4 +113,5 @@ export class ResetObjectCacheTimestampsAction implements Action { export type ObjectCacheAction = AddToObjectCacheAction | RemoveFromObjectCacheAction - | ResetObjectCacheTimestampsAction; + | ResetObjectCacheTimestampsAction + | PatchObjectCacheAction; diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index 3a1830e14a..5cc4b0551a 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -1,10 +1,11 @@ import { ObjectCacheAction, ObjectCacheActionTypes, AddToObjectCacheAction, - RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction + RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction, PatchObjectCacheAction } from './object-cache.actions'; -import { hasValue } from '../../shared/empty.util'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { CacheEntry } from './cache-entry'; import { ResourceType } from '../shared/resource-type'; +import { Operation } from 'fast-json-patch'; export enum DirtyType { Created = 'Created', @@ -36,6 +37,7 @@ export class ObjectCacheEntry implements CacheEntry { timeAdded: number; msToLive: number; requestHref: string; + operations: Operation[]; } /** @@ -76,6 +78,9 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi return resetObjectCacheTimestamps(state, action as ResetObjectCacheTimestampsAction) } + case ObjectCacheActionTypes.PATCH: { + return patchObjectCache(state, action as PatchObjectCacheAction); + } default: { return state; } @@ -98,7 +103,8 @@ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheActio data: action.payload.objectToCache, timeAdded: action.payload.timeAdded, msToLive: action.payload.msToLive, - requestHref: action.payload.requestHref + requestHref: action.payload.requestHref, + operations: [] } }); } @@ -143,3 +149,23 @@ function resetObjectCacheTimestamps(state: ObjectCacheState, action: ResetObject }); return newState; } + +/** + * Add the list of patch operations to a cached object + * + * @param state + * the current state + * @param action + * a PatchObjectCacheAction + * @return ObjectCacheState + * the new state, with the new operations added to the state of the specified ObjectCacheEntry + */ +function patchObjectCache(state: ObjectCacheState, action: PatchObjectCacheAction): ObjectCacheState { + const uuid = action.payload.uuid; + const operations = action.payload.operations; + const newState = Object.assign({}, state); + if (hasValue(newState[uuid])) { + newState[uuid].operations = state[uuid].operations.concat(operations); + } + return newState; +} diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index dbe241ffb3..d9b3077d55 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -1,18 +1,23 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { distinctUntilChanged, filter, first, map, mergeMap, take } from 'rxjs/operators'; +import { distinctUntilChanged, filter, first, map, mergeMap, take, tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { MemoizedSelector, select, Store } from '@ngrx/store'; import { IndexName } from '../index/index.reducer'; import { CacheableObject, ObjectCacheEntry } from './object-cache.reducer'; -import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions'; -import { hasNoValue } from '../../shared/empty.util'; +import { + AddToObjectCacheAction, + PatchObjectCacheAction, + RemoveFromObjectCacheAction +} from './object-cache.actions'; +import { hasNoValue, isNotEmpty } from '../../shared/empty.util'; import { GenericConstructor } from '../shared/generic-constructor'; import { coreSelector, CoreState } from '../core.reducers'; import { pathSelector } from '../shared/selectors'; import { NormalizedObjectFactory } from './models/normalized-object-factory'; import { NormalizedObject } from './models/normalized-object.model'; +import { applyPatch, Operation } from 'fast-json-patch'; function selfLinkFromUuidSelector(uuid: string): MemoizedSelector { return pathSelector(coreSelector, 'index', IndexName.OBJECT, uuid); @@ -85,7 +90,11 @@ export class ObjectCacheService { map((entry: ObjectCacheEntry) => { const type: GenericConstructor = NormalizedObjectFactory.getConstructor(entry.data.type); return Object.assign(new type(), entry.data) as T - })); + }), + // map((entry: ObjectCacheEntry) => + // applyPatch(entry.data, entry.operations).newDocument + // ) + ); } private getEntry(selfLink: string): Observable { @@ -195,4 +204,26 @@ export class ObjectCacheService { } } + /** + * Add operations to a the existing list of operations for an ObjectCacheEntry + * @param {string} uuid + * the uuid of the ObjectCacheEntry + * @param {Operation[]} patch + * list of operations to perform + */ + private addOperations(uuid: string, patch: Operation[]) { + this.store.dispatch(new PatchObjectCacheAction(uuid, 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.operations); + } } diff --git a/src/app/shared/form/form.reducer.ts b/src/app/shared/form/form.reducer.ts index 1a5d2da30b..eea5f99f20 100644 --- a/src/app/shared/form/form.reducer.ts +++ b/src/app/shared/form/form.reducer.ts @@ -30,7 +30,6 @@ export interface FormState { const initialState: FormState = Object.create(null); export function formReducer(state = initialState, action: FormAction): FormState { - console.log('TEST'); switch (action.type) { case FormActionTypes.FORM_INIT: { diff --git a/yarn.lock b/yarn.lock index 6129ef4e7e..b834144eb8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3026,6 +3026,12 @@ fast-glob@^2.0.2: merge2 "^1.2.1" micromatch "^3.1.10" +fast-json-patch@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-2.0.7.tgz#55864b08b1e50381d2f37fd472bb2e18fe54a733" + dependencies: + deep-equal "^1.0.1" + fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" From 758127e40981752adfb0d425e2723f98fdaf76c9 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 11 Sep 2018 16:23:57 +0200 Subject: [PATCH 27/69] 54472: Working post request with form-data (contains test code) --- .../create-collection-page.component.ts | 4 ++ src/app/core/data/collection-data.service.ts | 4 +- src/app/core/data/community-data.service.ts | 4 +- src/app/core/data/data.service.ts | 39 ++++++++++++++----- .../core/data/dspace-object-data.service.ts | 9 +++-- src/app/core/data/item-data.service.ts | 4 +- src/app/core/data/request.effects.ts | 2 +- 7 files changed, 50 insertions(+), 16 deletions(-) diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index 0f79558a44..93c3c16646 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -14,6 +14,7 @@ import { map } from 'rxjs/operators'; import { RemoteData } from '../../core/data/remote-data'; import { isNotEmpty } from '../../shared/empty.util'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { HttpEvent } from '@angular/common/http'; @Component({ selector: 'ds-create-collection', @@ -55,6 +56,9 @@ export class CreateCollectionPageComponent { this.router.navigateByUrl(''); } }); + // this.collectionDataService.createSimple(collection).subscribe((httpEvent: HttpEvent<{}>) => { + // console.log(httpEvent); + // }); }); } diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index fd2404650f..4157d96a9e 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -14,6 +14,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { AuthService } from '../auth/auth.service'; import { Community } from '../shared/community.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; @Injectable() export class CollectionDataService extends ComColDataService { @@ -28,7 +29,8 @@ export class CollectionDataService extends ComColDataService { @@ -32,7 +33,8 @@ export class CommunityDataService extends ComColDataService protected abstract halService: HALEndpointService; protected abstract authService: AuthService; protected abstract notificationsService: NotificationsService; + protected abstract http: HttpClient; public abstract getScopedEndpoint(scope: string): Observable @@ -126,17 +127,12 @@ export abstract class DataService const request$ = endpoint$.pipe( take(1), withLatestFrom(this.buildCreateBody(dso)), - map(([endpoint, formdata]) => { - const options: HttpOptions = Object.create({}); - let headers = new HttpHeaders(); - headers = headers.append('Content-Type','multipart/form-data'); - options.headers = headers; - return new CreateRequest(requestId, endpoint, formdata, options); - }), + map(([endpoint, formdata]) => new CreateRequest(requestId, endpoint, this.buildFormData(formdata))), configureRequest(this.requestService) ); const payload$ = request$.pipe( + take(1), map((request: RestRequest) => request.href), getResponseFromSelflink(this.responseCache), map((response: ResponseCacheEntry) => { @@ -158,6 +154,31 @@ export abstract class DataService return this.rdbService.toRemoteDataObservable(requestEntry$, responseCache$, payload$); } + public createSimple(dso: TDomain): Observable> { + const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe( + isNotEmptyOperator(), + distinctUntilChanged() + ); + + return endpoint$.pipe( + withLatestFrom(this.buildCreateBody(dso)), + switchMap(([endpoint, form]) => { + const req = new HttpRequest('POST', endpoint, this.buildFormData(form)); + return this.http.request(req); + }) + ); + } + public abstract buildCreateBody(dso: TDomain): Observable; + protected buildFormData(form: any): FormData { + const formdata = new FormData(); + for (const param in form) { + if (form.hasOwnProperty(param)) { + formdata.append(param, form[param]); + } + } + return formdata; + } + } diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index 8bae4e56ed..ecf52098ee 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -12,6 +12,7 @@ import { RemoteData } from './remote-data'; import { RequestService } from './request.service'; import { AuthService } from '../auth/auth.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; /* tslint:disable:max-classes-per-file */ class DataServiceImpl extends DataService { @@ -24,7 +25,8 @@ class DataServiceImpl extends DataService protected store: Store, protected halService: HALEndpointService, protected authService: AuthService, - protected notificationsService: NotificationsService) { + protected notificationsService: NotificationsService, + protected http: HttpClient) { super(); } @@ -51,8 +53,9 @@ export class DSpaceObjectDataService { protected rdbService: RemoteDataBuildService, protected halService: HALEndpointService, protected authService: AuthService, - protected notificationsService: NotificationsService) { - this.dataService = new DataServiceImpl(null, requestService, rdbService, null, halService, authService, notificationsService); + protected notificationsService: NotificationsService, + protected http: HttpClient) { + this.dataService = new DataServiceImpl(null, requestService, rdbService, null, halService, authService, notificationsService, http); } findById(uuid: string): Observable> { diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 343d4efede..4c003e15c9 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -17,6 +17,7 @@ import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { AuthService } from '../auth/auth.service'; +import { HttpClient } from '@angular/common/http'; @Injectable() export class ItemDataService extends DataService { @@ -30,7 +31,8 @@ export class ItemDataService extends DataService { private bs: BrowseService, protected halService: HALEndpointService, protected authService: AuthService, - protected notificationsService: NotificationsService) { + protected notificationsService: NotificationsService, + protected http: HttpClient) { super(); } diff --git a/src/app/core/data/request.effects.ts b/src/app/core/data/request.effects.ts index 5fadd316f4..9d27717dc6 100644 --- a/src/app/core/data/request.effects.ts +++ b/src/app/core/data/request.effects.ts @@ -38,7 +38,7 @@ export class RequestEffects { }), map((entry: RequestEntry) => entry.request), flatMap((request: RestRequest) => { - let body; + let body = request.body; if (isNotEmpty(request.body)) { const serializer = new DSpaceRESTv2Serializer(NormalizedObjectFactory.getConstructor(request.body.type)); body = serializer.serialize(request.body); From 0197b4a9dc8ec90032d185528c87906d51fc0eaf Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 12 Sep 2018 08:11:33 +0200 Subject: [PATCH 28/69] 54472: Passing parent as parameter and fixed duplicate collection created --- .../create-collection-page.component.ts | 15 +++------ .../create-community-page.component.ts | 10 ++---- src/app/core/data/comcol-data.service.ts | 31 ++++++++---------- src/app/core/data/data.service.ts | 32 ++----------------- .../core/data/dspace-object-data.service.ts | 2 +- src/app/core/data/item-data.service.ts | 4 +-- 6 files changed, 26 insertions(+), 68 deletions(-) diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index 93c3c16646..6cca8f1d09 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -10,7 +10,7 @@ import { Router } from '@angular/router'; import { DSOSuccessResponse, ErrorResponse } from '../../core/cache/response-cache.models'; import { Observable } from 'rxjs/Observable'; import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; -import { map } from 'rxjs/operators'; +import { map, take } from 'rxjs/operators'; import { RemoteData } from '../../core/data/remote-data'; import { isNotEmpty } from '../../shared/empty.util'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; @@ -36,7 +36,7 @@ export class CreateCollectionPageComponent { } onSubmit(data: any) { - this.parentUUID$.subscribe((uuid: string) => { + this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { const collection = Object.assign(new Collection(), { name: data.name, metadata: [ @@ -45,20 +45,13 @@ export class CreateCollectionPageComponent { { key: 'dc.rights', value: data.copyright }, { key: 'dc.rights.license', value: data.license } // TODO: metadata for news and provenance - ], - owner: Observable.of(new RemoteData(false, false, true, null, Object.assign(new Community(), { - id: uuid, - uuid: uuid - }))) + ] }); - this.collectionDataService.create(collection).subscribe((rd: RemoteData) => { + this.collectionDataService.create(collection, uuid).pipe(take(1)).subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { this.router.navigateByUrl(''); } }); - // this.collectionDataService.createSimple(collection).subscribe((httpEvent: HttpEvent<{}>) => { - // console.log(httpEvent); - // }); }); } diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index 9a4730f74f..afb90141c9 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -30,7 +30,7 @@ export class CreateCommunityPageComponent { } onSubmit(data: any) { - this.parentUUID$.subscribe((uuid: string) => { + this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { const community = Object.assign(new Community(), { name: data.name, metadata: [ @@ -38,13 +38,9 @@ export class CreateCommunityPageComponent { { key: 'dc.description.abstract', value: data.description }, { key: 'dc.rights', value: data.copyright } // TODO: metadata for news - ], - owner: Observable.of(new RemoteData(false, false, true, null, Object.assign(new Community(), { - id: uuid, - uuid: uuid - }))) + ] }); - this.communityDataService.create(community).pipe(take(1)).subscribe((rd: RemoteData) => { + this.communityDataService.create(community, uuid).pipe(take(1)).subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { this.router.navigateByUrl(''); } diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index c9dff8fbe1..8519bb2b25 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -66,25 +66,20 @@ export abstract class ComColDataService { - return comcol.owner.pipe( - map((rd: RemoteData) => { - const form: any = { - name: comcol.name - }; - if (rd.payload.id) { - form.parent = rd.payload.id; + public buildFormData(comcol, parentUUID): FormData { + const form: FormData = new FormData(); + form.append('name', comcol.name); + if (isNotEmpty(parentUUID)) { + form.append('parent', parentUUID); + } + if (comcol.metadata) { + for (const i of Object.keys(comcol.metadata)) { + if (isNotEmpty(comcol.metadata[i].value)) { + form.append(comcol.metadata[i].key, comcol.metadata[i].value); } - if (comcol.metadata) { - for (const i of Object.keys(comcol.metadata)) { - if (isNotEmpty(comcol.metadata[i].value)) { - form[comcol.metadata[i].key] = comcol.metadata[i].value; - } - } - } - return form; - }) - ); + } + } + return form; } } diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index fb3002b5d7..b2946b2164 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -117,7 +117,7 @@ export abstract class DataService return this.rdbService.buildSingle(href); } - public create(dso: TDomain): Observable> { + public create(dso: TDomain, parentUUID: string): Observable> { const requestId = this.requestService.generateRequestId(); const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe( isNotEmptyOperator(), @@ -126,8 +126,7 @@ export abstract class DataService const request$ = endpoint$.pipe( take(1), - withLatestFrom(this.buildCreateBody(dso)), - map(([endpoint, formdata]) => new CreateRequest(requestId, endpoint, this.buildFormData(formdata))), + map((endpoint: string) => new CreateRequest(requestId, endpoint, this.buildFormData(dso, parentUUID))), configureRequest(this.requestService) ); @@ -154,31 +153,6 @@ export abstract class DataService return this.rdbService.toRemoteDataObservable(requestEntry$, responseCache$, payload$); } - public createSimple(dso: TDomain): Observable> { - const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe( - isNotEmptyOperator(), - distinctUntilChanged() - ); - - return endpoint$.pipe( - withLatestFrom(this.buildCreateBody(dso)), - switchMap(([endpoint, form]) => { - const req = new HttpRequest('POST', endpoint, this.buildFormData(form)); - return this.http.request(req); - }) - ); - } - - public abstract buildCreateBody(dso: TDomain): Observable; - - protected buildFormData(form: any): FormData { - const formdata = new FormData(); - for (const param in form) { - if (form.hasOwnProperty(param)) { - formdata.append(param, form[param]); - } - } - return formdata; - } + public abstract buildFormData(dso: TDomain, parentUUID: string): FormData; } diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index ecf52098ee..14540d0782 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -38,7 +38,7 @@ class DataServiceImpl extends DataService return endpoint.replace(/\{\?uuid\}/,`?uuid=${resourceID}`); } - buildCreateBody(dso: DSpaceObject): Observable { + buildFormData(dso: DSpaceObject, parentUUID: string): FormData { return undefined; } } diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 4c003e15c9..c3c32691ce 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -47,8 +47,8 @@ export class ItemDataService extends DataService { } } - buildCreateBody(dso: Item): Observable { - // TODO: Build http body for creating an Item on the REST service + buildFormData(dso: Item, parentUUID: string): FormData { + // TODO: build FormData for creating an Item return undefined; } From 6973c66d797563fd722db900ccf43aede7aa3e8b Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 12 Sep 2018 10:49:31 +0200 Subject: [PATCH 29/69] 54472: Intermediate commit --- src/app/core/data/comcol-data.service.ts | 16 ---------------- src/app/core/data/data.service.ts | 7 +++---- .../core/data/dspace-object-data.service.ts | 4 ---- src/app/core/data/item-data.service.ts | 5 ----- src/app/core/data/request.effects.ts | 2 +- .../dspace-rest-v2/dspace-rest-v2.service.ts | 18 ++++++++++++++++++ 6 files changed, 22 insertions(+), 30 deletions(-) diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 8519bb2b25..0859d075ca 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -66,20 +66,4 @@ export abstract class ComColDataService const requestId = this.requestService.generateRequestId(); const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe( isNotEmptyOperator(), - distinctUntilChanged() + distinctUntilChanged(), + map((endpoint: string) => parentUUID ? `${endpoint}?parent=${parentUUID}` : endpoint) ); const request$ = endpoint$.pipe( take(1), - map((endpoint: string) => new CreateRequest(requestId, endpoint, this.buildFormData(dso, parentUUID))), + map((endpoint: string) => new CreateRequest(requestId, endpoint, dso)), configureRequest(this.requestService) ); @@ -153,6 +154,4 @@ export abstract class DataService return this.rdbService.toRemoteDataObservable(requestEntry$, responseCache$, payload$); } - public abstract buildFormData(dso: TDomain, parentUUID: string): FormData; - } diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index 14540d0782..d14d7f8a0b 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -37,10 +37,6 @@ class DataServiceImpl extends DataService getFindByIDHref(endpoint, resourceID): string { return endpoint.replace(/\{\?uuid\}/,`?uuid=${resourceID}`); } - - buildFormData(dso: DSpaceObject, parentUUID: string): FormData { - return undefined; - } } @Injectable() diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index c3c32691ce..6b62fe6688 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -47,9 +47,4 @@ export class ItemDataService extends DataService { } } - buildFormData(dso: Item, parentUUID: string): FormData { - // TODO: build FormData for creating an Item - return undefined; - } - } diff --git a/src/app/core/data/request.effects.ts b/src/app/core/data/request.effects.ts index 9d27717dc6..5fadd316f4 100644 --- a/src/app/core/data/request.effects.ts +++ b/src/app/core/data/request.effects.ts @@ -38,7 +38,7 @@ export class RequestEffects { }), map((entry: RequestEntry) => entry.request), flatMap((request: RestRequest) => { - let body = request.body; + let body; if (isNotEmpty(request.body)) { const serializer = new DSpaceRESTv2Serializer(NormalizedObjectFactory.getConstructor(request.body.type)); body = serializer.serialize(request.body); diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts index 78c93b8c08..c8cced9e8a 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts @@ -6,6 +6,8 @@ import { RestRequestMethod } from '../data/request.models'; import { DSpaceRESTV2Response } from './dspace-rest-v2-response.model'; import { HttpObserve } from '@angular/common/http/src/client'; +import { isNotEmpty } from '../../shared/empty.util'; +import { DSpaceObject } from '../shared/dspace-object.model'; export interface HttpOptions { body?: any; @@ -59,6 +61,9 @@ export class DSpaceRESTv2Service { request(method: RestRequestMethod, url: string, body?: any, options?: HttpOptions): Observable { const requestOptions: HttpOptions = {}; requestOptions.body = body; + if (method === RestRequestMethod.Post && isNotEmpty(body)) { + requestOptions.body = this.buildFormData(body); + } requestOptions.observe = 'response'; if (options && options.headers) { requestOptions.headers = Object.assign(new HttpHeaders(), options.headers); @@ -74,4 +79,17 @@ export class DSpaceRESTv2Service { }); } + buildFormData(dso: DSpaceObject): FormData { + const form: FormData = new FormData(); + form.append('name', dso.name); + if (dso.metadata) { + for (const i of Object.keys(dso.metadata)) { + if (isNotEmpty(dso.metadata[i].value)) { + form.append(dso.metadata[i].key, dso.metadata[i].value); + } + } + } + return form; + } + } From 6c8e49889174a105e4c012030c843d80f2317356 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 13 Sep 2018 14:19:02 +0200 Subject: [PATCH 30/69] 54472: Small changes --- .../create-collection-page.component.ts | 16 ++++++++++------ .../dspace-rest-v2/dspace-rest-v2.service.ts | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index 6cca8f1d09..aea405ea09 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -10,7 +10,7 @@ import { Router } from '@angular/router'; import { DSOSuccessResponse, ErrorResponse } from '../../core/cache/response-cache.models'; import { Observable } from 'rxjs/Observable'; import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; -import { map, take } from 'rxjs/operators'; +import { first, map, take } from 'rxjs/operators'; import { RemoteData } from '../../core/data/remote-data'; import { isNotEmpty } from '../../shared/empty.util'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; @@ -26,7 +26,12 @@ export class CreateCollectionPageComponent { public parentUUID$: Observable; public communityRDObs: Observable>; - public constructor(private collectionDataService: CollectionDataService, private communityDataService: CommunityDataService, private routeService: RouteService, private router: Router) { + public constructor( + private collectionDataService: CollectionDataService, + private communityDataService: CommunityDataService, + private routeService: RouteService, + private router: Router + ) { this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); this.parentUUID$.subscribe((uuid: string) => { if (isNotEmpty(uuid)) { @@ -47,10 +52,9 @@ export class CreateCollectionPageComponent { // TODO: metadata for news and provenance ] }); - this.collectionDataService.create(collection, uuid).pipe(take(1)).subscribe((rd: RemoteData) => { - if (rd.hasSucceeded) { - this.router.navigateByUrl(''); - } + this.collectionDataService.create(collection, uuid).subscribe((rd: RemoteData) => { + const url: string = '/collections/' + rd.payload.id; + this.router.navigateByUrl(url); }); }); } diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts index c8cced9e8a..eafa9c46a0 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts @@ -61,7 +61,7 @@ export class DSpaceRESTv2Service { request(method: RestRequestMethod, url: string, body?: any, options?: HttpOptions): Observable { const requestOptions: HttpOptions = {}; requestOptions.body = body; - if (method === RestRequestMethod.Post && isNotEmpty(body)) { + if (method === RestRequestMethod.Post && isNotEmpty(body.name)) { requestOptions.body = this.buildFormData(body); } requestOptions.observe = 'response'; From 17ad62c172dfffeba9d397fc374d4b4f95aa83e2 Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 14 Sep 2018 09:26:30 +0200 Subject: [PATCH 31/69] implementation of server synchronization --- config/environment.default.js | 7 +- src/app/core/cache/object-cache.actions.ts | 50 +++++++--- src/app/core/cache/object-cache.reducer.ts | 63 ++++++++++--- src/app/core/cache/object-cache.service.ts | 47 +++++++--- src/app/core/cache/response-cache.service.ts | 2 +- .../core/cache/server-sync-buffer.actions.ts | 83 +++++++++++++++++ .../core/cache/server-sync-buffer.effects.ts | 55 +++++++++++ .../core/cache/server-sync-buffer.reducer.ts | 92 +++++++++++++++++++ src/app/core/core.reducers.ts | 11 ++- src/config/auto-sync-config.interface.ts | 12 +++ src/config/cache-config.interface.ts | 4 +- 11 files changed, 378 insertions(+), 48 deletions(-) create mode 100644 src/app/core/cache/server-sync-buffer.actions.ts create mode 100644 src/app/core/cache/server-sync-buffer.effects.ts create mode 100644 src/app/core/cache/server-sync-buffer.reducer.ts create mode 100644 src/config/auto-sync-config.interface.ts diff --git a/config/environment.default.js b/config/environment.default.js index 22a70f3513..32edd1391d 100644 --- a/config/environment.default.js +++ b/config/environment.default.js @@ -20,7 +20,12 @@ module.exports = { // NOTE: how long should objects be cached for by default msToLive: 15 * 60 * 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': 30} //time in seconds + } }, // Form settings form: { diff --git a/src/app/core/cache/object-cache.actions.ts b/src/app/core/cache/object-cache.actions.ts index 957c0f7269..024a0e7061 100644 --- a/src/app/core/cache/object-cache.actions.ts +++ b/src/app/core/cache/object-cache.actions.ts @@ -11,7 +11,8 @@ export const ObjectCacheActionTypes = { ADD: type('dspace/core/cache/object/ADD'), REMOVE: type('dspace/core/cache/object/REMOVE'), RESET_TIMESTAMPS: type('dspace/core/cache/object/RESET_TIMESTAMPS'), - PATCH: type('dspace/core/cache/object/PATCH') + ADD_PATCH: type('dspace/core/cache/object/ADD_PATCH'), + APPLY_PATCH: type('dspace/core/cache/object/APPLY_PATCH') }; /* tslint:disable:max-classes-per-file */ @@ -56,11 +57,11 @@ export class RemoveFromObjectCacheAction implements Action { /** * Create a new RemoveFromObjectCacheAction * - * @param uuid - * the UUID of the object to remove + * @param href + * the unique href of the object to remove */ - constructor(uuid: string) { - this.payload = uuid; + constructor(href: string) { + this.payload = href; } } @@ -83,25 +84,43 @@ export class ResetObjectCacheTimestampsAction implements Action { } /** - * An ngrx action to add new operations to a specified cached objects + * An ngrx action to add new operations to a specified cached object */ -export class PatchObjectCacheAction implements Action { - type = ObjectCacheActionTypes.PATCH; +export class AddPatchObjectCacheAction implements Action { + type = ObjectCacheActionTypes.ADD_PATCH; payload: { - uuid: string, + href: string, operations: Operation[] }; /** - * Create a new PatchObjectCacheAction + * Create a new AddPatchObjectCacheAction * - * @param uuid - * the uuid of the object that should be updated + * @param href + * the unique href of the object that should be updated * @param operations * the list of operations to add */ - constructor(uuid: string, operations: Operation[]) { - this.payload = { uuid, operations }; + 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; } } @@ -114,4 +133,5 @@ export type ObjectCacheAction = AddToObjectCacheAction | RemoveFromObjectCacheAction | ResetObjectCacheTimestampsAction - | PatchObjectCacheAction; + | AddPatchObjectCacheAction + | ApplyPatchObjectCacheAction; diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index 5cc4b0551a..cd0a927c34 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -1,11 +1,15 @@ import { - ObjectCacheAction, ObjectCacheActionTypes, AddToObjectCacheAction, - RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction, PatchObjectCacheAction + ObjectCacheAction, + ObjectCacheActionTypes, + AddToObjectCacheAction, + RemoveFromObjectCacheAction, + ResetObjectCacheTimestampsAction, + AddPatchObjectCacheAction, ApplyPatchObjectCacheAction } from './object-cache.actions'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { CacheEntry } from './cache-entry'; import { ResourceType } from '../shared/resource-type'; -import { Operation } from 'fast-json-patch'; +import { applyPatch, Operation } from 'fast-json-patch'; export enum DirtyType { Created = 'Created', @@ -13,7 +17,11 @@ export enum DirtyType { Deleted = 'Deleted' } -/** +export interface Patch { + uuid?: string; + operations: Operation[]; +} +/**conca * An interface to represent objects that can be cached * * A cacheable object should have a self link @@ -37,7 +45,8 @@ export class ObjectCacheEntry implements CacheEntry { timeAdded: number; msToLive: number; requestHref: string; - operations: Operation[]; + patches: Patch[] = []; + isDirty: boolean; } /** @@ -78,9 +87,14 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi return resetObjectCacheTimestamps(state, action as ResetObjectCacheTimestampsAction) } - case ObjectCacheActionTypes.PATCH: { - return patchObjectCache(state, action as PatchObjectCacheAction); + case ObjectCacheActionTypes.ADD_PATCH: { + return addPatchObjectCache(state, action as AddPatchObjectCacheAction); } + + case ObjectCacheActionTypes.APPLY_PATCH: { + return applyPatchObjectCache(state, action as ApplyPatchObjectCacheAction); + } + default: { return state; } @@ -104,7 +118,7 @@ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheActio timeAdded: action.payload.timeAdded, msToLive: action.payload.msToLive, requestHref: action.payload.requestHref, - operations: [] + isDirty: false } }); } @@ -156,16 +170,41 @@ function resetObjectCacheTimestamps(state: ObjectCacheState, action: ResetObject * @param state * the current state * @param action - * a PatchObjectCacheAction + * an AddPatchObjectCacheAction * @return ObjectCacheState * the new state, with the new operations added to the state of the specified ObjectCacheEntry */ -function patchObjectCache(state: ObjectCacheState, action: PatchObjectCacheAction): ObjectCacheState { - const uuid = action.payload.uuid; +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])) { - newState[uuid].operations = state[uuid].operations.concat(operations); + const patches = newState[uuid].patches; + newState[uuid].patches = [...patches, {operations} as Patch]; + newState[uuid].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); + const newData = applyPatch( newState[uuid].data, flatPatch); + newState[uuid].data = newData.newDocument; + newState[uuid].patches = []; } return newState; } diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index d9b3077d55..d7bf59500c 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -7,8 +7,8 @@ import { IndexName } from '../index/index.reducer'; import { CacheableObject, ObjectCacheEntry } from './object-cache.reducer'; import { - AddToObjectCacheAction, - PatchObjectCacheAction, + AddPatchObjectCacheAction, + AddToObjectCacheAction, ApplyPatchObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions'; import { hasNoValue, isNotEmpty } from '../../shared/empty.util'; @@ -18,13 +18,15 @@ import { pathSelector } from '../shared/selectors'; import { NormalizedObjectFactory } from './models/normalized-object-factory'; import { NormalizedObject } from './models/normalized-object.model'; import { applyPatch, Operation } from 'fast-json-patch'; +import { RestRequestMethod } from '../data/request.models'; +import { AddToSSBAction } from './server-sync-buffer.actions'; function selfLinkFromUuidSelector(uuid: string): MemoizedSelector { return pathSelector(coreSelector, 'index', IndexName.OBJECT, uuid); } function entryFromSelfLinkSelector(selfLink: string): MemoizedSelector { - return pathSelector(coreSelector, 'data/object', selfLink); + return pathSelector(coreSelector, 'cache/object', selfLink); } /** @@ -52,10 +54,10 @@ export class ObjectCacheService { } /** - * Remove the object with the supplied UUID from the cache + * Remove the object with the supplied href from the cache * - * @param uuid - * The UUID of the object to be removed + * @param href + * The unique href of the object to be removed */ remove(uuid: string): void { this.store.dispatch(new RemoveFromObjectCacheAction(uuid)); @@ -87,13 +89,17 @@ export class ObjectCacheService { getBySelfLink(selfLink: string): Observable { return this.getEntry(selfLink).pipe( + map((entry: ObjectCacheEntry) => { + // flatten two dimensional array + const flatPatch: Operation[] = [].concat(...entry.patches); + const patchedData = applyPatch(entry.data, flatPatch).newDocument; + return Object.assign({}, entry, { data: patchedData }); + } + ), map((entry: ObjectCacheEntry) => { const type: GenericConstructor = NormalizedObjectFactory.getConstructor(entry.data.type); return Object.assign(new type(), entry.data) as T - }), - // map((entry: ObjectCacheEntry) => - // applyPatch(entry.data, entry.operations).newDocument - // ) + }) ); } @@ -205,14 +211,16 @@ export class ObjectCacheService { } /** - * Add operations to a the existing list of operations for an ObjectCacheEntry + * 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 */ - private addOperations(uuid: string, patch: Operation[]) { - this.store.dispatch(new PatchObjectCacheAction(uuid, patch)); + private addPatch(uuid: string, patch: Operation[]) { + this.store.dispatch(new AddPatchObjectCacheAction(uuid, patch)); + this.store.dispatch(new AddToSSBAction(uuid, RestRequestMethod.Patch)); } /** @@ -224,6 +232,17 @@ export class ObjectCacheService { * false if the entry is there are no operations left in the ObjectCacheEntry, true otherwise */ private isDirty(entry: ObjectCacheEntry): boolean { - return isNotEmpty(entry.operations); + 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(uuid: string) { + this.store.dispatch(new ApplyPatchObjectCacheAction(uuid)); + } + } diff --git a/src/app/core/cache/response-cache.service.ts b/src/app/core/cache/response-cache.service.ts index 21430d451c..973d3620ff 100644 --- a/src/app/core/cache/response-cache.service.ts +++ b/src/app/core/cache/response-cache.service.ts @@ -12,7 +12,7 @@ import { coreSelector, CoreState } from '../core.reducers'; import { pathSelector } from '../shared/selectors'; function entryFromKeySelector(key: string): MemoizedSelector { - return pathSelector(coreSelector, 'data/response', key); + return pathSelector(coreSelector, 'cache/response', key); } /** diff --git a/src/app/core/cache/server-sync-buffer.actions.ts b/src/app/core/cache/server-sync-buffer.actions.ts new file mode 100644 index 0000000000..9189af4471 --- /dev/null +++ b/src/app/core/cache/server-sync-buffer.actions.ts @@ -0,0 +1,83 @@ +import { Action } from '@ngrx/store'; + +import { type } from '../../shared/ngrx/type'; +import { CacheableObject } from './object-cache.reducer'; +import { Operation } from 'fast-json-patch'; +import { RestRequest, RestRequestMethod } from '../data/request.models'; + +/** + * 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's 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 }; + } +} + +/** + * 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 + */ + constructor(method?: RestRequestMethod) { + this.payload = method; + } +} + +/* tslint:enable:max-classes-per-file */ + +/** + * A type to encompass all ServerSyncBufferActions + */ +export type ServerSyncBufferAction + = AddToSSBAction + | CommitSSBAction + | EmptySSBAction diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts new file mode 100644 index 0000000000..517c9837c2 --- /dev/null +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -0,0 +1,55 @@ +import { delay, exhaustMap, filter, first, map } 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 { CoreState } from '../core.reducers'; +import { select, Store } from '@ngrx/store'; +import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer'; +import { of as observableOf } from 'rxjs'; +import { RequestService } from '../data/request.service'; +import { PutRequest } from '../data/request.models'; + +@Injectable() +export class ObjectCacheEffects { + @Effect() setTimeoutForServerSync = this.actions$ + .pipe(ofType(ServerSyncBufferActionTypes.ADD), + exhaustMap((action: AddToSSBAction) => { + const autoSyncConfig = this.EnvConfig.cache.autoSync; + const timeoutInSeconds = autoSyncConfig.timePerMethod[action.type] || autoSyncConfig.defaultTime; + return observableOf(new CommitSSBAction(action.payload.method)).pipe(delay( timeoutInSeconds * 1000)) + }) + ); + + @Effect() commitServerSyncBuffer = this.actions$ + .pipe(ofType(ServerSyncBufferActionTypes.COMMIT), + map((action: CommitSSBAction) => { + this.store.pipe( + select(serverSyncBufferSelector), + first() + ).subscribe((bufferState: ServerSyncBufferState) => { + bufferState.buffer + .filter((entry: ServerSyncBufferEntry) => entry.method === action.payload) + .forEach((entry: ServerSyncBufferEntry) => { + this.requestService.configure(new PutRequest(this.requestService.generateRequestId(), ,)) + }) + }); + return new EmptySSBAction(action.payload); + }) + ); + + constructor(private actions$: Actions, + private store: Store, + private requestService: RequestService, + @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) { + + } +} + +export const serverSyncBufferSelector = (state: CoreState) => state['cache/syncbuffer']; diff --git a/src/app/core/cache/server-sync-buffer.reducer.ts b/src/app/core/cache/server-sync-buffer.reducer.ts new file mode 100644 index 0000000000..1fabe5a50a --- /dev/null +++ b/src/app/core/cache/server-sync-buffer.reducer.ts @@ -0,0 +1,92 @@ +import { RestRequestMethod } from '../data/request.models'; +import { hasNoValue, hasValue } from '../../shared/empty.util'; +import { + AddToSSBAction, + EmptySSBAction, + ServerSyncBufferAction, + ServerSyncBufferActionTypes +} from './server-sync-buffer.actions'; + +/** + * 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 }); +} diff --git a/src/app/core/core.reducers.ts b/src/app/core/core.reducers.ts index c764a2acff..6905eb1300 100644 --- a/src/app/core/core.reducers.ts +++ b/src/app/core/core.reducers.ts @@ -5,18 +5,21 @@ import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reduc import { indexReducer, IndexState } from './index/index.reducer'; import { requestReducer, RequestState } from './data/request.reducer'; import { authReducer, AuthState } from './auth/auth.reducer'; +import { serverSyncBufferReducer, ServerSyncBufferState } from './cache/server-sync-buffer.reducer'; export interface CoreState { - 'data/object': ObjectCacheState, - 'data/response': ResponseCacheState, + 'cache/object': ObjectCacheState, + 'cache/response': ResponseCacheState, + 'cache/syncbuffer': ServerSyncBufferState, 'data/request': RequestState, 'index': IndexState, 'auth': AuthState, } export const coreReducers: ActionReducerMap = { - 'data/object': objectCacheReducer, - 'data/response': responseCacheReducer, + 'cache/object': objectCacheReducer, + 'cache/response': responseCacheReducer, + 'cache/syncbuffer': serverSyncBufferReducer, 'data/request': requestReducer, 'index': indexReducer, 'auth': authReducer diff --git a/src/config/auto-sync-config.interface.ts b/src/config/auto-sync-config.interface.ts new file mode 100644 index 0000000000..4ad74005bf --- /dev/null +++ b/src/config/auto-sync-config.interface.ts @@ -0,0 +1,12 @@ +import { RestRequestMethod } from '../app/core/data/request.models'; + +/* enum indices */ +type TimePerMethod = { + [method in RestRequestMethod]: number; +}; + +export interface AutoSyncConfig { + defaultTime: number; + timePerMethod: TimePerMethod; + maxBufferSize: number; +}; diff --git a/src/config/cache-config.interface.ts b/src/config/cache-config.interface.ts index f8a2c88640..3e9334e300 100644 --- a/src/config/cache-config.interface.ts +++ b/src/config/cache-config.interface.ts @@ -1,6 +1,8 @@ import { Config } from './config.interface'; +import { AutoSyncConfig } from './auto-sync-config.interface'; export interface CacheConfig extends Config { msToLive: number, - control: string + control: string, + autoSync: AutoSyncConfig } From f9203a5cb693ee49c246c061cc1273cd401dd497 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 14 Sep 2018 12:02:20 +0200 Subject: [PATCH 32/69] 54472: Tests + caching fix attempts intermediate commit --- .../create-collection-page.component.spec.ts | 25 ++++--- .../create-collection-page.component.ts | 18 +++-- .../create-community-page.component.spec.ts | 25 ++++--- .../create-community-page.component.ts | 14 +++- src/app/core/data/comcol-data.service.spec.ts | 69 ++----------------- src/app/core/data/data.service.spec.ts | 8 ++- src/app/core/data/data.service.ts | 10 +-- .../dspace-rest-v2/dspace-rest-v2.service.ts | 2 +- .../core/metadata/metadata.service.spec.ts | 9 +++ src/app/core/shared/operators.ts | 2 +- 10 files changed, 76 insertions(+), 106 deletions(-) diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts index efe0e3da97..6196945d80 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts @@ -31,10 +31,13 @@ describe('CreateCollectionPageComponent', () => { name: 'test community' }); + const collection = Object.assign(new Collection(), { + uuid: 'ce41d451-97ed-4a9c-94a1-7de34f16a9f4', + name: 'new collection' + }); + const collectionDataServiceStub = { - create: (com, uuid?) => Observable.of({ - response: new DSOSuccessResponse(null,'200',null) - }) + create: (col, uuid?) => Observable.of(new RemoteData(false, false, true, undefined, collection)) }; const communityDataServiceStub = { findById: (uuid) => Observable.of(new RemoteData(false, false, true, null, Object.assign(new Community(), { @@ -46,7 +49,7 @@ describe('CreateCollectionPageComponent', () => { getQueryParameterValue: (param) => Observable.of(community.uuid) }; const routerStub = { - navigateByUrl: (url) => url + navigate: (commands) => commands }; beforeEach(async(() => { @@ -78,22 +81,18 @@ describe('CreateCollectionPageComponent', () => { }; it('should navigate when successful', () => { - spyOn(router, 'navigateByUrl'); + spyOn(router, 'navigate'); comp.onSubmit(data); fixture.detectChanges(); - expect(router.navigateByUrl).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalled(); }); it('should not navigate on failure', () => { - spyOn(router, 'navigateByUrl'); - spyOn(collectionDataService, 'create').and.returnValue(Observable.of({ - response: Object.assign(new ErrorResponse(new RequestError()), { - isSuccessful: false - }) - })); + spyOn(router, 'navigate'); + spyOn(collectionDataService, 'create').and.returnValue(Observable.of(new RemoteData(true, true, false, undefined, collection))); comp.onSubmit(data); fixture.detectChanges(); - expect(router.navigateByUrl).not.toHaveBeenCalled(); + expect(router.navigate).not.toHaveBeenCalled(); }); }); }); diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index aea405ea09..4d94a53035 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -10,11 +10,14 @@ import { Router } from '@angular/router'; import { DSOSuccessResponse, ErrorResponse } from '../../core/cache/response-cache.models'; import { Observable } from 'rxjs/Observable'; import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; -import { first, map, take } from 'rxjs/operators'; +import { first, flatMap, map, take } from 'rxjs/operators'; import { RemoteData } from '../../core/data/remote-data'; -import { isNotEmpty } from '../../shared/empty.util'; +import { hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { HttpEvent } from '@angular/common/http'; +import { getSucceededRemoteData } from '../../core/shared/operators'; +import { ObjectCacheService } from '../../core/cache/object-cache.service'; +import { NormalizedCollection } from '../../core/cache/models/normalized-collection.model'; @Component({ selector: 'ds-create-collection', @@ -30,7 +33,8 @@ export class CreateCollectionPageComponent { private collectionDataService: CollectionDataService, private communityDataService: CommunityDataService, private routeService: RouteService, - private router: Router + private router: Router, + private objectCache: ObjectCacheService ) { this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); this.parentUUID$.subscribe((uuid: string) => { @@ -52,9 +56,11 @@ export class CreateCollectionPageComponent { // TODO: metadata for news and provenance ] }); - this.collectionDataService.create(collection, uuid).subscribe((rd: RemoteData) => { - const url: string = '/collections/' + rd.payload.id; - this.router.navigateByUrl(url); + this.collectionDataService.create(collection, uuid).pipe( + flatMap((rd: RemoteData) => this.objectCache.getByUUID(rd.payload.id)), + isNotEmptyOperator() + ).subscribe((col: NormalizedCollection) => { + this.router.navigate(['collections', col.id]); }); }); } diff --git a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts index 3c4c7648da..6c5eb5f591 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts @@ -27,20 +27,23 @@ describe('CreateCommunityPageComponent', () => { name: 'test community' }); + const newCommunity = Object.assign(new Community(), { + uuid: '1ff59938-a69a-4e62-b9a4-718569c55d48', + name: 'new community' + }); + const communityDataServiceStub = { findById: (uuid) => Observable.of(new RemoteData(false, false, true, null, Object.assign(new Community(), { uuid: uuid, name: community.name }))), - create: (com, uuid?) => Observable.of({ - response: new DSOSuccessResponse(null,'200',null) - }) + create: (com, uuid?) => Observable.of(new RemoteData(false, false, true, undefined, newCommunity)) }; const routeServiceStub = { getQueryParameterValue: (param) => Observable.of(community.uuid) }; const routerStub = { - navigateByUrl: (url) => url + navigate: (commands) => commands }; beforeEach(async(() => { @@ -70,22 +73,18 @@ describe('CreateCommunityPageComponent', () => { }; it('should navigate when successful', () => { - spyOn(router, 'navigateByUrl'); + spyOn(router, 'navigate'); comp.onSubmit(data); fixture.detectChanges(); - expect(router.navigateByUrl).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalled(); }); it('should not navigate on failure', () => { - spyOn(router, 'navigateByUrl'); - spyOn(communityDataService, 'create').and.returnValue(Observable.of({ - response: Object.assign(new ErrorResponse(new RequestError()), { - isSuccessful: false - }) - })); + spyOn(router, 'navigate'); + spyOn(communityDataService, 'create').and.returnValue(Observable.of(new RemoteData(true, true, false, undefined, newCommunity))); comp.onSubmit(data); fixture.detectChanges(); - expect(router.navigateByUrl).not.toHaveBeenCalled(); + expect(router.navigate).not.toHaveBeenCalled(); }); }); }); diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index afb90141c9..a991bf7127 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -20,8 +20,12 @@ export class CreateCommunityPageComponent { public parentUUID$: Observable; public communityRDObs: Observable>; - public constructor(private communityDataService: CommunityDataService, private routeService: RouteService, private router: Router) { - this.parentUUID$ = this.routeService.getQueryParameterValue('parent').pipe(take(1)); + public constructor( + private communityDataService: CommunityDataService, + private routeService: RouteService, + private router: Router + ) { + this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); this.parentUUID$.subscribe((uuid: string) => { if (isNotEmpty(uuid)) { this.communityRDObs = this.communityDataService.findById(uuid); @@ -42,7 +46,11 @@ export class CreateCommunityPageComponent { }); this.communityDataService.create(community, uuid).pipe(take(1)).subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { - this.router.navigateByUrl(''); + if (uuid) { + this.router.navigate(['communities', uuid]); + } else { + this.router.navigate([]); + } } }); }); diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index ce3c581b31..0c03b64a1d 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -17,6 +17,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Community } from '../shared/community.model'; import { AuthService } from '../auth/auth.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; const LINK_NAME = 'test'; @@ -37,6 +38,7 @@ class TestService extends ComColDataService { protected halService: HALEndpointService, protected authService: AuthService, protected notificationsService: NotificationsService, + protected http: HttpClient, protected linkPath: string ) { super(); @@ -57,6 +59,8 @@ describe('ComColDataService', () => { const rdbService = {} as RemoteDataBuildService; const store = {} as Store; const EnvConfig = {} as GlobalConfig; + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d'; const communitiesEndpoint = 'https://rest.api/core/communities'; @@ -115,6 +119,8 @@ describe('ComColDataService', () => { objectCache, halService, authService, + notificationsService, + http, LINK_NAME ); } @@ -177,67 +183,4 @@ describe('ComColDataService', () => { }); - describe('create', () => { - let community: Community; - const name = 'test community'; - - beforeEach(() => { - community = Object.assign(new Community(), { - name: name - }); - spyOn(service, 'buildCreateParams'); - }); - - describe('when creating a top-level community', () => { - it('should build params without parent UUID', () => { - scheduler.schedule(() => service.create(community).subscribe()); - scheduler.flush(); - expect(service.buildCreateParams).toHaveBeenCalledWith(community); - }); - }); - - describe('when creating a community part of another community', () => { - let parentCommunity: Community; - const parentName = 'test parent community'; - const parentUUID = 'a20da287-e174-466a-9926-f66b9300d347'; - - beforeEach(() => { - parentCommunity = Object.assign(new Community(), { - id: parentUUID, - uuid: parentUUID, - name: parentName - }); - }); - - it('should build params with parent UUID', () => { - scheduler.schedule(() => service.create(community, parentUUID).subscribe()); - scheduler.flush(); - expect(service.buildCreateParams).toHaveBeenCalledWith(community, parentUUID); - }); - }); - }); - - describe('buildCreateParams', () => { - let community: Community; - const name = 'test community'; - let parentCommunity: Community; - const parentName = 'test parent community'; - const parentUUID = 'a20da287-e174-466a-9926-f66b9300d347'; - - beforeEach(() => { - community = Object.assign(new Community(), { - name: name - }); - parentCommunity = Object.assign(new Community(), { - id: parentUUID, - uuid: parentUUID, - name: parentName - }); - }); - - it('should return the correct url parameters', () => { - expect(service.buildCreateParams(community, parentUUID)).toEqual('?name=' + name + '&parent=' + parentUUID); - }); - }); - }); diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index 8abf4e51ca..4faea85755 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -10,6 +10,7 @@ import { Observable } from 'rxjs/Observable'; import { FindAllOptions } from './request.models'; import { SortOptions, SortDirection } from '../cache/models/sort-options.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; const LINK_NAME = 'test' @@ -25,7 +26,8 @@ class TestService extends DataService { protected store: Store, protected linkPath: string, protected halService: HALEndpointService, - protected notificationsService: NotificationsService + protected notificationsService: NotificationsService, + protected http: HttpClient ) { super(); } @@ -44,6 +46,7 @@ describe('DataService', () => { const halService = {} as HALEndpointService; const rdbService = {} as RemoteDataBuildService; const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; const store = {} as Store; const endpoint = 'https://rest.api/core'; @@ -55,7 +58,8 @@ describe('DataService', () => { store, LINK_NAME, halService, - notificationsService + notificationsService, + http ); } diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 13550b2358..0d746816c7 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -18,11 +18,11 @@ import { } from './request.models'; import { RequestService } from './request.service'; import { NormalizedObject } from '../cache/models/normalized-object.model'; -import { distinctUntilChanged, map, switchMap, take, withLatestFrom } from 'rxjs/operators'; +import { distinctUntilChanged, flatMap, map, switchMap, take, withLatestFrom } from 'rxjs/operators'; import { configureRequest, filterSuccessfulResponses, - getResponseFromSelflink + getResponseFromSelflink, getSucceededRemoteData } from '../shared/operators'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; @@ -31,6 +31,7 @@ import { ErrorResponse, GenericSuccessResponse } from '../cache/response-cache.m import { AuthService } from '../auth/auth.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; +import { combineLatest } from 'rxjs/observable/combineLatest'; export abstract class DataService { protected abstract responseCache: ResponseCacheService; @@ -132,7 +133,6 @@ export abstract class DataService ); const payload$ = request$.pipe( - take(1), map((request: RestRequest) => request.href), getResponseFromSelflink(this.responseCache), map((response: ResponseCacheEntry) => { @@ -151,7 +151,9 @@ export abstract class DataService const requestEntry$ = this.requestService.getByUUID(requestId); const responseCache$ = endpoint$.pipe(getResponseFromSelflink(this.responseCache)); - return this.rdbService.toRemoteDataObservable(requestEntry$, responseCache$, payload$); + return this.rdbService.toRemoteDataObservable(requestEntry$, responseCache$, payload$).pipe( + getSucceededRemoteData() + ); } } diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts index eafa9c46a0..8c5b5f1ac6 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts @@ -61,7 +61,7 @@ export class DSpaceRESTv2Service { request(method: RestRequestMethod, url: string, body?: any, options?: HttpOptions): Observable { const requestOptions: HttpOptions = {}; requestOptions.body = body; - if (method === RestRequestMethod.Post && isNotEmpty(body.name)) { + if (method === RestRequestMethod.Post && isNotEmpty(body) && isNotEmpty(body.name)) { requestOptions.body = this.buildFormData(body); } requestOptions.observe = 'response'; diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index f8f36a358e..ca3d3b0e95 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -34,6 +34,10 @@ import { MockItem } from '../../shared/mocks/mock-item'; import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; import { BrowseService } from '../browse/browse.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { AuthService } from '../auth/auth.service'; +import { REQUEST } from '@nguniversal/express-engine/tokens'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; /* tslint:disable:max-classes-per-file */ @Component({ @@ -69,6 +73,7 @@ describe('MetadataService', () => { let uuidService: UUIDService; let remoteDataBuildService: RemoteDataBuildService; let itemDataService: ItemDataService; + let authService: AuthService; let location: Location; let router: Router; @@ -115,6 +120,9 @@ describe('MetadataService', () => { { provide: RemoteDataBuildService, useValue: remoteDataBuildService }, { provide: GLOBAL_CONFIG, useValue: ENV_CONFIG }, { provide: HALEndpointService, useValue: {}}, + { provide: AuthService, useValue: {} }, + { provide: NotificationsService, useValue: {} }, + { provide: HttpClient, useValue: {} }, Meta, Title, ItemDataService, @@ -127,6 +135,7 @@ describe('MetadataService', () => { title = TestBed.get(Title); itemDataService = TestBed.get(ItemDataService); metadataService = TestBed.get(MetadataService); + authService = TestBed.get(AuthService); envConfig = TestBed.get(GLOBAL_CONFIG); diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index 350a664e9d..b44be403b8 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -58,7 +58,7 @@ export const getRemoteDataPayload = () => export const getSucceededRemoteData = () => (source: Observable>): Observable> => - source.pipe(first((rd: RemoteData) => rd.hasSucceeded)); + source.pipe(first((rd: RemoteData) => rd.hasSucceeded && !rd.isLoading)); export const toDSpaceObjectListRD = () => (source: Observable>>>): Observable>> => From 6aee6e668c5e0e9e6c598a5f991c0698dee83c7f Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 14 Sep 2018 16:27:41 +0200 Subject: [PATCH 33/69] 54472: DataService create method tests pt1 --- src/app/core/data/data.service.spec.ts | 59 +++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index 4faea85755..7ebe536219 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -11,8 +11,15 @@ import { FindAllOptions } from './request.models'; import { SortOptions, SortDirection } from '../cache/models/sort-options.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; +import { AuthService } from '../auth/auth.service'; +import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { RemoteData } from './remote-data'; +import { RequestEntry } from './request.reducer'; +import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; -const LINK_NAME = 'test' +const LINK_NAME = 'test'; // tslint:disable:max-classes-per-file class NormalizedTestObject extends NormalizedObject { @@ -26,6 +33,7 @@ class TestService extends DataService { protected store: Store, protected linkPath: string, protected halService: HALEndpointService, + protected authService: AuthService, protected notificationsService: NotificationsService, protected http: HttpClient ) { @@ -41,14 +49,19 @@ class TestService extends DataService { describe('DataService', () => { let service: TestService; let options: FindAllOptions; - const responseCache = {} as ResponseCacheService; - const requestService = {} as RequestService; - const halService = {} as HALEndpointService; - const rdbService = {} as RemoteDataBuildService; + const responseCache = getMockResponseCacheService(); + let rdbService = {} as RemoteDataBuildService; + const authService = {} as AuthService; const notificationsService = {} as NotificationsService; const http = {} as HttpClient; const store = {} as Store; const endpoint = 'https://rest.api/core'; + const halService = Object.assign({ + getEndpoint: () => Observable.of(endpoint) + }); + const requestService = Object.assign(getMockRequestService(), { + getByUUID: () => Observable.of(new RequestEntry()) + }); function initTestService(): TestService { return new TestService( @@ -58,6 +71,7 @@ describe('DataService', () => { store, LINK_NAME, halService, + authService, notificationsService, http ); @@ -138,4 +152,39 @@ describe('DataService', () => { }) }); + fdescribe('create', () => { + const dso = new DSpaceObject(); + const successfulRd$ = Observable.of(new RemoteData(false, false, true, undefined, dso)); + const failingRd$ = Observable.of(new RemoteData(false, false, false, undefined, dso)); + + describe('when the request was successful', () => { + beforeEach(() => { + rdbService = getMockRemoteDataBuildService(successfulRd$); + service = initTestService(); + }); + + it('should return a RemoteData of a DSpaceObject', () => { + service.create(dso, undefined).subscribe((rd: RemoteData) => { + expect(rd.payload).toBe(dso); + }); + }); + }); + + describe('when the request was unsuccessful', () => { + beforeEach(() => { + rdbService = getMockRemoteDataBuildService(failingRd$); + service = initTestService(); + }); + + it('should not return anything', () => { + service.create(dso, undefined); + + // TODO: Expect create to emit nothing + }); + }); + + // TODO: Create tests with passing parent + + }); + }); From 9da839e318886f59b49f3a208a8947df2fc50989 Mon Sep 17 00:00:00 2001 From: lotte Date: Mon, 17 Sep 2018 08:47:30 +0200 Subject: [PATCH 34/69] refactoring --- .../builders/remote-data-build.service.ts | 4 ++ src/app/core/core.module.ts | 2 - src/app/core/data/data.service.ts | 26 +++++------- src/app/core/data/request.models.ts | 6 +-- .../single-dso-response-parsing.service.ts | 40 ------------------- src/app/core/shared/hal-endpoint.service.ts | 4 +- 6 files changed, 17 insertions(+), 65 deletions(-) delete mode 100644 src/app/core/data/single-dso-response-parsing.service.ts diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index d934f60e48..841a4483ca 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -22,6 +22,7 @@ import { getResponseFromSelflink, filterSuccessfulResponses } from '../../shared/operators'; +import { ReplaySubject } from 'rxjs/ReplaySubject'; @Injectable() export class RemoteDataBuildService { @@ -34,6 +35,8 @@ export class RemoteDataBuildService { if (typeof href$ === 'string') { href$ = Observable.of(href$); } + href$ = href$.multicast(new ReplaySubject(1)).refCount(); + const requestHref$ = href$.pipe(flatMap((href: string) => this.objectCache.getRequestHrefBySelfLink(href))); @@ -112,6 +115,7 @@ export class RemoteDataBuildService { if (typeof href$ === 'string') { href$ = Observable.of(href$); } + href$ = href$.shareReplay(); const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache)); diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 61568b99a6..dabdfba0ab 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -63,7 +63,6 @@ import { RegistryBitstreamformatsResponseParsingService } from './data/registry- import { NotificationsService } from '../shared/notifications/notifications.service'; import { UploaderService } from '../shared/uploader/uploader.service'; import { DSpaceObjectDataService } from './data/dspace-object-data.service'; -import { SingleDsoResponseParsingService } from './data/single-dso-response-parsing.service'; const IMPORTS = [ CommonModule, @@ -127,7 +126,6 @@ const PROVIDERS = [ UploaderService, UUIDService, DSpaceObjectDataService, - SingleDsoResponseParsingService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 0d746816c7..53327952c1 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -18,20 +18,18 @@ import { } from './request.models'; import { RequestService } from './request.service'; import { NormalizedObject } from '../cache/models/normalized-object.model'; -import { distinctUntilChanged, flatMap, map, switchMap, take, withLatestFrom } from 'rxjs/operators'; +import { distinctUntilChanged, first, map, take, tap } from 'rxjs/operators'; import { configureRequest, filterSuccessfulResponses, - getResponseFromSelflink, getSucceededRemoteData + getResponseFromSelflink } from '../shared/operators'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; -import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; -import { HttpClient, HttpEvent, HttpHeaders, HttpRequest } from '@angular/common/http'; -import { ErrorResponse, GenericSuccessResponse } from '../cache/response-cache.models'; +import { HttpClient } from '@angular/common/http'; +import { DSOSuccessResponse, ErrorResponse } from '../cache/response-cache.models'; import { AuthService } from '../auth/auth.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; -import { combineLatest } from 'rxjs/observable/combineLatest'; export abstract class DataService { protected abstract responseCache: ResponseCacheService; @@ -118,7 +116,7 @@ export abstract class DataService return this.rdbService.buildSingle(href); } - public create(dso: TDomain, parentUUID: string): Observable> { + create(dso: TDomain, parentUUID: string): Observable> { const requestId = this.requestService.generateRequestId(); const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe( isNotEmptyOperator(), @@ -132,7 +130,7 @@ export abstract class DataService configureRequest(this.requestService) ); - const payload$ = request$.pipe( + const selfLink$ = request$.pipe( map((request: RestRequest) => request.href), getResponseFromSelflink(this.responseCache), map((response: ResponseCacheEntry) => { @@ -144,16 +142,12 @@ export abstract class DataService }), filterSuccessfulResponses(), map((entry: ResponseCacheEntry) => entry.response), - map((response: GenericSuccessResponse) => response.payload), + map((response: DSOSuccessResponse) => { + return response.resourceSelfLinks[0]; + }), distinctUntilChanged() ); - - const requestEntry$ = this.requestService.getByUUID(requestId); - const responseCache$ = endpoint$.pipe(getResponseFromSelflink(this.responseCache)); - - return this.rdbService.toRemoteDataObservable(requestEntry$, responseCache$, payload$).pipe( - getSucceededRemoteData() - ); + return this.rdbService.buildSingle(selfLink$) as Observable>; } } diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index f5b9127a6b..7ca152a204 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -1,7 +1,5 @@ import { SortOptions } from '../cache/models/sort-options.model'; import { GenericConstructor } from '../shared/generic-constructor'; -import { GlobalConfig } from '../../../config/global-config.interface'; -import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service'; import { DSOResponseParsingService } from './dso-response-parsing.service'; import { ResponseParsingService } from './parsing.service'; @@ -10,9 +8,7 @@ import { BrowseResponseParsingService } from './browse-response-parsing.service' import { ConfigResponseParsingService } from './config-response-parsing.service'; import { AuthResponseParsingService } from '../auth/auth-response-parsing.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; -import { HttpHeaders } from '@angular/common/http'; import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service'; -import { SingleDsoResponseParsingService } from './single-dso-response-parsing.service'; /* tslint:disable:max-classes-per-file */ @@ -231,7 +227,7 @@ export class CreateRequest extends PostRequest { } getResponseParser(): GenericConstructor { - return SingleDsoResponseParsingService; + return DSOResponseParsingService; } } diff --git a/src/app/core/data/single-dso-response-parsing.service.ts b/src/app/core/data/single-dso-response-parsing.service.ts deleted file mode 100644 index 03b9f5bea2..0000000000 --- a/src/app/core/data/single-dso-response-parsing.service.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Inject, Injectable } from '@angular/core'; -import { ResponseParsingService } from './parsing.service'; -import { BaseResponseParsingService } from './base-response-parsing.service'; -import { GlobalConfig } from '../../../config/global-config.interface'; -import { GLOBAL_CONFIG } from '../../../config'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { RestRequest } from './request.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { - DSOSuccessResponse, - GenericSuccessResponse, - RestResponse -} from '../cache/response-cache.models'; -import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; -import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import { NormalizedObject } from '../cache/models/normalized-object.model'; -import { ResourceType } from '../shared/resource-type'; -import { hasNoValue, hasValue } from '../../shared/empty.util'; - -@Injectable() -export class SingleDsoResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - protected objectFactory = NormalizedObjectFactory; - protected toCache = true; - - constructor( - @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, - protected objectCache: ObjectCacheService, - ) { super(); - } - - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - const processRequestDTO = this.process(data.payload, request.href); - if (hasNoValue(processRequestDTO)) { - return new GenericSuccessResponse(undefined, data.statusCode) - } - return new GenericSuccessResponse(processRequestDTO, data.statusCode); - } - -} diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index 3bedeb9915..a0be10f454 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -1,5 +1,5 @@ import { Observable } from 'rxjs/Observable'; -import { distinctUntilChanged, map, flatMap, startWith, tap } from 'rxjs/operators'; +import { distinctUntilChanged, map, flatMap, startWith, tap, switchMap } from 'rxjs/operators'; import { RequestService } from '../data/request.service'; import { ResponseCacheService } from '../cache/response-cache.service'; import { GlobalConfig } from '../../../config/global-config.interface'; @@ -48,7 +48,7 @@ export class HALEndpointService { let currentPath; const pipeArguments = path .map((subPath: string, index: number) => [ - flatMap((href: string) => this.getEndpointMapAt(href)), + switchMap((href: string) => this.getEndpointMapAt(href)), map((endpointMap: EndpointMap) => { if (hasValue(endpointMap) && hasValue(endpointMap[subPath])) { currentPath = endpointMap[subPath]; From 30284ea78f2df462e5a7b58b42e3c3a596fc1ea8 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 17 Sep 2018 10:33:20 +0200 Subject: [PATCH 35/69] 54472: DataService create method tests + buildFormData test pt2 --- .../create-collection-page.component.ts | 12 ++-- src/app/core/data/data.service.spec.ts | 72 +++++++++++++------ .../dspace-rest-v2.service.spec.ts | 12 ++++ 3 files changed, 69 insertions(+), 27 deletions(-) diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index 4d94a53035..6429e623bf 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -33,8 +33,7 @@ export class CreateCollectionPageComponent { private collectionDataService: CollectionDataService, private communityDataService: CommunityDataService, private routeService: RouteService, - private router: Router, - private objectCache: ObjectCacheService + private router: Router ) { this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); this.parentUUID$.subscribe((uuid: string) => { @@ -56,11 +55,10 @@ export class CreateCollectionPageComponent { // TODO: metadata for news and provenance ] }); - this.collectionDataService.create(collection, uuid).pipe( - flatMap((rd: RemoteData) => this.objectCache.getByUUID(rd.payload.id)), - isNotEmptyOperator() - ).subscribe((col: NormalizedCollection) => { - this.router.navigate(['collections', col.id]); + this.collectionDataService.create(collection, uuid).subscribe((rd: RemoteData) => { + if (rd.hasSucceeded) { + this.router.navigate(['collections', rd.payload.id]); + } }); }); } diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index 7ebe536219..d16af124e4 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -18,6 +18,12 @@ import { DSpaceObject } from '../shared/dspace-object.model'; import { RemoteData } from './remote-data'; import { RequestEntry } from './request.reducer'; import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; +import { EmptyError } from 'rxjs/util/EmptyError'; +import { ResponseCacheEntry } from '../cache/response-cache.reducer'; +import { ErrorResponse, RestResponse } from '../cache/response-cache.models'; +import { hasValue } from '../../shared/empty.util'; +import { map } from 'rxjs/operators'; +import { RemoteDataError } from './remote-data-error'; const LINK_NAME = 'test'; @@ -49,7 +55,7 @@ class TestService extends DataService { describe('DataService', () => { let service: TestService; let options: FindAllOptions; - const responseCache = getMockResponseCacheService(); + let responseCache = getMockResponseCacheService(); let rdbService = {} as RemoteDataBuildService; const authService = {} as AuthService; const notificationsService = {} as NotificationsService; @@ -57,12 +63,38 @@ describe('DataService', () => { const store = {} as Store; const endpoint = 'https://rest.api/core'; const halService = Object.assign({ - getEndpoint: () => Observable.of(endpoint) + getEndpoint: (linkpath) => Observable.of(endpoint) }); const requestService = Object.assign(getMockRequestService(), { - getByUUID: () => Observable.of(new RequestEntry()) + getByUUID: () => Observable.of(new RequestEntry()), + configure: (request) => request }); + const dso = new DSpaceObject(); + const successfulRd$ = Observable.of(new RemoteData(false, false, true, undefined, dso)); + const successfulResponseCacheEntry = { + response: { + isSuccessful: true, + payload: dso, + toCache: true, + statusCode: '200' + } as RestResponse + } as ResponseCacheEntry; + + function initSuccessfulRemoteDataBuildService(): RemoteDataBuildService { + return { + toRemoteDataObservable: (requestEntry$: Observable, responseCache$: Observable, payload$: Observable) => { + requestEntry$.subscribe(); + responseCache$.subscribe(); + payload$.subscribe(); + return successfulRd$; + } + } as RemoteDataBuildService; + } + function initSuccessfulResponseCacheService(): ResponseCacheService { + return getMockResponseCacheService(Observable.of(new ResponseCacheEntry()), Observable.of(successfulResponseCacheEntry)); + } + function initTestService(): TestService { return new TestService( responseCache, @@ -136,13 +168,13 @@ describe('DataService', () => { }); it('should include all provided options in href', () => { - const sortOptions = new SortOptions('field1', SortDirection.DESC) + const sortOptions = new SortOptions('field1', SortDirection.DESC); options = { currentPage: 6, elementsPerPage: 10, sort: sortOptions, startsWith: 'ab' - } + }; const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` + `&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`; @@ -152,14 +184,12 @@ describe('DataService', () => { }) }); - fdescribe('create', () => { - const dso = new DSpaceObject(); - const successfulRd$ = Observable.of(new RemoteData(false, false, true, undefined, dso)); - const failingRd$ = Observable.of(new RemoteData(false, false, false, undefined, dso)); + describe('create', () => { describe('when the request was successful', () => { beforeEach(() => { - rdbService = getMockRemoteDataBuildService(successfulRd$); + responseCache = initSuccessfulResponseCacheService(); + rdbService = initSuccessfulRemoteDataBuildService(); service = initTestService(); }); @@ -168,23 +198,25 @@ describe('DataService', () => { expect(rd.payload).toBe(dso); }); }); - }); - describe('when the request was unsuccessful', () => { - beforeEach(() => { - rdbService = getMockRemoteDataBuildService(failingRd$); - service = initTestService(); + it('should get the response from cache with the correct url when parent is empty', () => { + const expectedUrl = endpoint; + + service.create(dso, undefined).subscribe((value) => { + expect(responseCache.get).toHaveBeenCalledWith(expectedUrl); + }); }); - it('should not return anything', () => { - service.create(dso, undefined); + it('should get the response from cache with the correct url when parent is not empty', () => { + const parent = 'fake-parent-uuid'; + const expectedUrl = `${endpoint}?parent=${parent}`; - // TODO: Expect create to emit nothing + service.create(dso, parent).subscribe((value) => { + expect(responseCache.get).toHaveBeenCalledWith(expectedUrl); + }); }); }); - // TODO: Create tests with passing parent - }); }); diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts index 4893908627..256fffaa24 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts @@ -2,6 +2,7 @@ import { TestBed, inject } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { DSpaceRESTv2Service } from './dspace-rest-v2.service'; +import { DSpaceObject } from '../shared/dspace-object.model'; describe('DSpaceRESTv2Service', () => { let dSpaceRESTv2Service: DSpaceRESTv2Service; @@ -65,4 +66,15 @@ describe('DSpaceRESTv2Service', () => { expect(req.request.method).toBe('GET'); req.error(mockError); }); + + fdescribe('buildFormData', () => { + it('should return the correct data', () => { + const name = 'testname'; + const dso: DSpaceObject = { + name: name + } as DSpaceObject; + const formdata = dSpaceRESTv2Service.buildFormData(dso); + expect(formdata.get('name')).toBe(name); + }); + }); }); From e011ec207c075c42558fcd2d4bb1c7b8a4f39277 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 17 Sep 2018 10:46:47 +0200 Subject: [PATCH 36/69] 54472: Adjustment of tests with new code + removal of fdescribe --- src/app/core/data/data.service.spec.ts | 19 ++++++++++--------- .../dspace-rest-v2.service.spec.ts | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index d16af124e4..8a2f5897c2 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -20,7 +20,7 @@ import { RequestEntry } from './request.reducer'; import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; import { EmptyError } from 'rxjs/util/EmptyError'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; -import { ErrorResponse, RestResponse } from '../cache/response-cache.models'; +import { DSOSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models'; import { hasValue } from '../../shared/empty.util'; import { map } from 'rxjs/operators'; import { RemoteDataError } from './remote-data-error'; @@ -72,21 +72,22 @@ describe('DataService', () => { const dso = new DSpaceObject(); const successfulRd$ = Observable.of(new RemoteData(false, false, true, undefined, dso)); - const successfulResponseCacheEntry = { + const successfulResponseCacheEntry = Object.assign({ response: { isSuccessful: true, payload: dso, toCache: true, - statusCode: '200' - } as RestResponse - } as ResponseCacheEntry; + statusCode: '200', + resourceSelfLinks: [ + endpoint + ] + } as DSOSuccessResponse + }) as ResponseCacheEntry; function initSuccessfulRemoteDataBuildService(): RemoteDataBuildService { return { - toRemoteDataObservable: (requestEntry$: Observable, responseCache$: Observable, payload$: Observable) => { - requestEntry$.subscribe(); - responseCache$.subscribe(); - payload$.subscribe(); + buildSingle: (selfLinks$: Observable) => { + selfLinks$.subscribe(); return successfulRd$; } } as RemoteDataBuildService; diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts index 256fffaa24..26bd1ba5de 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts @@ -67,7 +67,7 @@ describe('DSpaceRESTv2Service', () => { req.error(mockError); }); - fdescribe('buildFormData', () => { + describe('buildFormData', () => { it('should return the correct data', () => { const name = 'testname'; const dso: DSpaceObject = { From e959542e2d708bb1f97673892f4844a0e4efce4a Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 19 Sep 2018 13:10:23 +0200 Subject: [PATCH 37/69] implemented applying of a patch --- .../auth/auth-response-parsing.service.ts | 2 +- .../core/cache/object-cache.reducer.spec.ts | 8 +- src/app/core/cache/object-cache.reducer.ts | 4 +- src/app/core/cache/object-cache.service.ts | 3 +- .../core/cache/server-sync-buffer.effects.ts | 70 ++++-- .../data/base-response-parsing.service.ts | 1 - src/app/core/data/collection-data.service.ts | 4 +- src/app/core/data/comcol-data.service.spec.ts | 4 +- src/app/core/data/community-data.service.ts | 4 +- src/app/core/data/data.service.spec.ts | 208 +++++++++--------- src/app/core/data/data.service.ts | 13 ++ .../data/dspace-object-data.service.spec.ts | 6 +- .../core/data/dspace-object-data.service.ts | 9 +- src/app/core/data/item-data.service.spec.ts | 5 +- src/app/core/data/item-data.service.ts | 4 +- src/app/core/data/request.service.ts | 5 + .../ds-dynamic-form-control.component.html | 4 +- src/app/shared/mocks/mock-form-service.ts | 7 +- .../number-picker/number-picker.component.ts | 2 +- tslint.json | 10 +- 20 files changed, 226 insertions(+), 147 deletions(-) diff --git a/src/app/core/auth/auth-response-parsing.service.ts b/src/app/core/auth/auth-response-parsing.service.ts index 80c1b2eeca..337bb0fd0a 100644 --- a/src/app/core/auth/auth-response-parsing.service.ts +++ b/src/app/core/auth/auth-response-parsing.service.ts @@ -20,7 +20,7 @@ export class AuthResponseParsingService extends BaseResponseParsingService imple protected toCache = false; constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, - protected objectCache: ObjectCacheService,) { + protected objectCache: ObjectCacheService) { super(); } diff --git a/src/app/core/cache/object-cache.reducer.spec.ts b/src/app/core/cache/object-cache.reducer.spec.ts index 2c059c4dd3..ce41f96bd4 100644 --- a/src/app/core/cache/object-cache.reducer.spec.ts +++ b/src/app/core/cache/object-cache.reducer.spec.ts @@ -26,7 +26,9 @@ describe('objectCacheReducer', () => { }, timeAdded: new Date().getTime(), msToLive: 900000, - requestHref: selfLink1 + requestHref: selfLink1, + patches: [], + isDirty: false }, [selfLink2]: { data: { @@ -35,7 +37,9 @@ describe('objectCacheReducer', () => { }, timeAdded: new Date().getTime(), msToLive: 900000, - requestHref: selfLink2 + requestHref: selfLink2, + patches: [], + isDirty: false } }; deepFreeze(testState); diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index cd0a927c34..adea46bc57 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -112,13 +112,15 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi * the new state, with the object added, or overwritten. */ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheAction): ObjectCacheState { + const existing = state[action.payload.objectToCache.self]; return Object.assign({}, state, { [action.payload.objectToCache.self]: { data: action.payload.objectToCache, timeAdded: action.payload.timeAdded, msToLive: action.payload.msToLive, requestHref: action.payload.requestHref, - isDirty: false + isDirty: false, + patches: (hasValue(existing) ? existing.patches : []) } }); } diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index d7bf59500c..f6b295afb4 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -90,7 +90,6 @@ export class ObjectCacheService { getBySelfLink(selfLink: string): Observable { return this.getEntry(selfLink).pipe( map((entry: ObjectCacheEntry) => { - // flatten two dimensional array const flatPatch: Operation[] = [].concat(...entry.patches); const patchedData = applyPatch(entry.data, flatPatch).newDocument; return Object.assign({}, entry, { data: patchedData }); @@ -218,7 +217,7 @@ export class ObjectCacheService { * @param {Operation[]} patch * list of operations to perform */ - private addPatch(uuid: string, patch: Operation[]) { + public addPatch(uuid: string, patch: Operation[]) { this.store.dispatch(new AddPatchObjectCacheAction(uuid, patch)); this.store.dispatch(new AddToSSBAction(uuid, RestRequestMethod.Patch)); } diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts index 517c9837c2..d6c1941e86 100644 --- a/src/app/core/cache/server-sync-buffer.effects.ts +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -1,4 +1,4 @@ -import { delay, exhaustMap, filter, first, map } from 'rxjs/operators'; +import { delay, exhaustMap, first, map, switchMap, tap } from 'rxjs/operators'; import { Inject, Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { @@ -10,43 +10,77 @@ import { import { GLOBAL_CONFIG } from '../../../config'; import { GlobalConfig } from '../../../config/global-config.interface'; import { CoreState } from '../core.reducers'; -import { select, Store } from '@ngrx/store'; +import { Action, select, Store } from '@ngrx/store'; import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer'; -import { of as observableOf } from 'rxjs'; +import { of as observableOf, combineLatest as observableCombineLatest } from 'rxjs'; import { RequestService } from '../data/request.service'; -import { PutRequest } from '../data/request.models'; +import { PutRequest, RestRequestMethod } 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 } from '../../shared/empty.util'; +import { Observable } from 'rxjs/internal/Observable'; @Injectable() export class ObjectCacheEffects { @Effect() setTimeoutForServerSync = this.actions$ - .pipe(ofType(ServerSyncBufferActionTypes.ADD), + .pipe( + ofType(ServerSyncBufferActionTypes.ADD), exhaustMap((action: AddToSSBAction) => { const autoSyncConfig = this.EnvConfig.cache.autoSync; const timeoutInSeconds = autoSyncConfig.timePerMethod[action.type] || autoSyncConfig.defaultTime; - return observableOf(new CommitSSBAction(action.payload.method)).pipe(delay( timeoutInSeconds * 1000)) + return observableOf(new CommitSSBAction(action.payload.method)).pipe(delay(timeoutInSeconds * 1000)) }) ); @Effect() commitServerSyncBuffer = this.actions$ - .pipe(ofType(ServerSyncBufferActionTypes.COMMIT), - map((action: CommitSSBAction) => { - this.store.pipe( + .pipe( + ofType(ServerSyncBufferActionTypes.COMMIT), + switchMap((action: CommitSSBAction) => { + return this.store.pipe( select(serverSyncBufferSelector), - first() - ).subscribe((bufferState: ServerSyncBufferState) => { - bufferState.buffer - .filter((entry: ServerSyncBufferEntry) => entry.method === action.payload) - .forEach((entry: ServerSyncBufferEntry) => { - this.requestService.configure(new PutRequest(this.requestService.generateRequestId(), ,)) - }) - }); - return new EmptySSBAction(action.payload); + map((bufferState: ServerSyncBufferState) => { + const actions: Array> = 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 other request stuff */ + } + }); + /* Add extra action to array, to make sure the ServerSyncBuffer is emptied afterwards */ + return observableCombineLatest(actions).pipe( + map((array) => array.push(new EmptySSBAction(action.payload))) + ); + }) + ) }) ); + private applyPatch(href: string): Observable { + const patchObject = this.objectCache.getBySelfLink(href); + 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, private requestService: RequestService, + private objectCache: ObjectCacheService, @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) { } diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index 050b3c2da5..374a1ea0a9 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -25,7 +25,6 @@ export abstract class BaseResponseParsingService { protected abstract toCache: boolean; protected process(data: any, requestHref: string): any { - if (isNotEmpty(data)) { if (hasNoValue(data) || (typeof data !== 'object')) { return data; diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 7d1e463dbe..2642c4b5e6 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -22,8 +22,8 @@ export class CollectionDataService extends ComColDataService, protected cds: CommunityDataService, - protected objectCache: ObjectCacheService, - protected halService: HALEndpointService + protected halService: HALEndpointService, + protected objectCache: ObjectCacheService ) { super(); } diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index eb2e1bd1cf..ff05d73fff 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -29,8 +29,8 @@ class TestService extends ComColDataService { protected store: Store, protected EnvConfig: GlobalConfig, protected cds: CommunityDataService, - protected objectCache: ObjectCacheService, protected halService: HALEndpointService, + protected objectCache: ObjectCacheService, protected linkPath: string ) { super(); @@ -92,8 +92,8 @@ describe('ComColDataService', () => { store, EnvConfig, cds, - objectCache, halService, + objectCache, LINK_NAME ); } diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index b8e8bd5ce0..a9163fd6b9 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -29,8 +29,8 @@ export class CommunityDataService extends ComColDataService, - protected objectCache: ObjectCacheService, - protected halService: HALEndpointService + protected halService: HALEndpointService, + protected objectCache: ObjectCacheService ) { super(); } diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index add84818de..1bb051a9a6 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -9,6 +9,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Observable } from 'rxjs'; import { FindAllOptions } from './request.models'; import { SortOptions, SortDirection } from '../cache/models/sort-options.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; const LINK_NAME = 'test' @@ -17,117 +18,122 @@ class NormalizedTestObject extends NormalizedObject { } class TestService extends DataService { - constructor( - protected responseCache: ResponseCacheService, - protected requestService: RequestService, - protected rdbService: RemoteDataBuildService, - protected store: Store, - protected linkPath: string, - protected halService: HALEndpointService - ) { - super(); - } + constructor( + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected linkPath: string, + protected halService: HALEndpointService, + protected objectCache: ObjectCacheService + ) { + super(); + } - public getScopedEndpoint(scope: string): Observable { - throw new Error('getScopedEndpoint is abstract in DataService'); - } + public getScopedEndpoint(scope: string): Observable { + throw new Error('getScopedEndpoint is abstract in DataService'); + } } describe('DataService', () => { - let service: TestService; - let options: FindAllOptions; - const responseCache = {} as ResponseCacheService; - const requestService = {} as RequestService; - const halService = {} as HALEndpointService; - const rdbService = {} as RemoteDataBuildService; - const store = {} as Store; - const endpoint = 'https://rest.api/core'; + let service: TestService; + let options: FindAllOptions; + const responseCache = {} as ResponseCacheService; + const requestService = {} as RequestService; + const halService = {} as HALEndpointService; + const rdbService = {} as RemoteDataBuildService; + const objectCache = {} as ObjectCacheService; + const store = {} as Store; + const endpoint = 'https://rest.api/core'; - function initTestService(): TestService { - return new TestService( - responseCache, - requestService, - rdbService, - store, - LINK_NAME, - halService - ); - } + function initTestService(): TestService { + return new TestService( + responseCache, + requestService, + rdbService, + store, + LINK_NAME, + halService, + objectCache + ); + } - service = initTestService(); + service = initTestService(); - describe('getFindAllHref', () => { + describe('getFindAllHref', () => { - it('should return an observable with the endpoint', () => { - options = {}; + it('should return an observable with the endpoint', () => { + options = {}; - (service as any).getFindAllHref(endpoint).subscribe((value) => { - expect(value).toBe(endpoint); - } - ); - }); - - // getScopedEndpoint is not implemented in abstract DataService - it('should throw error if scopeID provided in options', () => { - options = { scopeID: 'somevalue' }; - - expect(() => { (service as any).getFindAllHref(endpoint, options) }) - .toThrowError('getScopedEndpoint is abstract in DataService'); - }); - - it('should include page in href if currentPage provided in options', () => { - options = { currentPage: 2 }; - const expected = `${endpoint}?page=${options.currentPage - 1}`; - - (service as any).getFindAllHref(endpoint, options).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should include size in href if elementsPerPage provided in options', () => { - options = { elementsPerPage: 5 }; - const expected = `${endpoint}?size=${options.elementsPerPage}`; - - (service as any).getFindAllHref(endpoint, options).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should include sort href if SortOptions provided in options', () => { - const sortOptions = new SortOptions('field1', SortDirection.ASC); - options = { sort: sortOptions}; - const expected = `${endpoint}?sort=${sortOptions.field},${sortOptions.direction}`; - - (service as any).getFindAllHref(endpoint, options).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should include startsWith in href if startsWith provided in options', () => { - options = { startsWith: 'ab' }; - const expected = `${endpoint}?startsWith=${options.startsWith}`; - - (service as any).getFindAllHref(endpoint, options).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should include all provided options in href', () => { - const sortOptions = new SortOptions('field1', SortDirection.DESC) - options = { - currentPage: 6, - elementsPerPage: 10, - sort: sortOptions, - startsWith: 'ab' - } - const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` + - `&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`; - - (service as any).getFindAllHref(endpoint, options).subscribe((value) => { - expect(value).toBe(expected); - }); - }) + (service as any).getFindAllHref(endpoint).subscribe((value) => { + expect(value).toBe(endpoint); + } + ); }); + // getScopedEndpoint is not implemented in abstract DataService + it('should throw error if scopeID provided in options', () => { + options = { scopeID: 'somevalue' }; + + expect(() => { + (service as any).getFindAllHref(endpoint, options) + }) + .toThrowError('getScopedEndpoint is abstract in DataService'); + }); + + it('should include page in href if currentPage provided in options', () => { + options = { currentPage: 2 }; + const expected = `${endpoint}?page=${options.currentPage - 1}`; + + (service as any).getFindAllHref(endpoint, options).subscribe((value) => { + expect(value).toBe(expected); + }); + }); + + it('should include size in href if elementsPerPage provided in options', () => { + options = { elementsPerPage: 5 }; + const expected = `${endpoint}?size=${options.elementsPerPage}`; + + (service as any).getFindAllHref(endpoint, options).subscribe((value) => { + expect(value).toBe(expected); + }); + }); + + it('should include sort href if SortOptions provided in options', () => { + const sortOptions = new SortOptions('field1', SortDirection.ASC); + options = { sort: sortOptions }; + const expected = `${endpoint}?sort=${sortOptions.field},${sortOptions.direction}`; + + (service as any).getFindAllHref(endpoint, options).subscribe((value) => { + expect(value).toBe(expected); + }); + }); + + it('should include startsWith in href if startsWith provided in options', () => { + options = { startsWith: 'ab' }; + const expected = `${endpoint}?startsWith=${options.startsWith}`; + + (service as any).getFindAllHref(endpoint, options).subscribe((value) => { + expect(value).toBe(expected); + }); + }); + + it('should include all provided options in href', () => { + const sortOptions = new SortOptions('field1', SortDirection.DESC) + options = { + currentPage: 6, + elementsPerPage: 10, + sort: sortOptions, + startsWith: 'ab' + } + const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` + + `&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`; + + (service as any).getFindAllHref(endpoint, options).subscribe((value) => { + expect(value).toBe(expected); + }); + }) + }); + }); diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 1d0703bb47..f58868d2ce 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -14,6 +14,9 @@ import { RemoteData } from './remote-data'; import { FindAllOptions, FindAllRequest, FindByIDRequest, GetRequest } from './request.models'; import { RequestService } from './request.service'; import { NormalizedObject } from '../cache/models/normalized-object.model'; +import { compare, Operation } from 'fast-json-patch'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { DSpaceObject } from '../shared/dspace-object.model'; export abstract class DataService { protected abstract responseCache: ResponseCacheService; @@ -22,6 +25,7 @@ export abstract class DataService protected abstract store: Store; protected abstract linkPath: string; protected abstract halService: HALEndpointService; + protected abstract objectCache: ObjectCacheService; public abstract getScopedEndpoint(scope: string): Observable @@ -97,6 +101,15 @@ export abstract class DataService return this.rdbService.buildSingle(href); } + patch(href: string, operations: Operation[]) { + this.objectCache.addPatch(href, operations); + } + + update(object: DSpaceObject) { + const oldVersion = this.objectCache.getBySelfLink(object.self); + const operations = compare(oldVersion, object); + this.objectCache.addPatch(object.self, operations); + } // TODO implement, after the structure of the REST server's POST response is finalized // create(dso: DSpaceObject): Observable> { // const postHrefObs = this.getEndpoint(); diff --git a/src/app/core/data/dspace-object-data.service.spec.ts b/src/app/core/data/dspace-object-data.service.spec.ts index ef767c5d2d..cdddcb7ce6 100644 --- a/src/app/core/data/dspace-object-data.service.spec.ts +++ b/src/app/core/data/dspace-object-data.service.spec.ts @@ -6,6 +6,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { FindByIDRequest } from './request.models'; import { RequestService } from './request.service'; import { DSpaceObjectDataService } from './dspace-object-data.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; describe('DSpaceObjectDataService', () => { let scheduler: TestScheduler; @@ -13,6 +14,7 @@ describe('DSpaceObjectDataService', () => { let halService: HALEndpointService; let requestService: RequestService; let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; const testObject = { uuid: '9b4f22f4-164a-49db-8817-3316b6ee5746' } as DSpaceObject; @@ -37,11 +39,13 @@ describe('DSpaceObjectDataService', () => { } }) }); + objectCache = {} as ObjectCacheService; service = new DSpaceObjectDataService( requestService, rdbService, - halService + halService, + objectCache ) }); diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index 09eea8a95f..5b92b1beef 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -10,6 +10,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { DataService } from './data.service'; import { RemoteData } from './remote-data'; import { RequestService } from './request.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; /* tslint:disable:max-classes-per-file */ class DataServiceImpl extends DataService { @@ -20,7 +21,8 @@ class DataServiceImpl extends DataService protected requestService: RequestService, protected rdbService: RemoteDataBuildService, protected store: Store, - protected halService: HALEndpointService) { + protected halService: HALEndpointService, + protected objectCache: ObjectCacheService) { super(); } @@ -41,8 +43,9 @@ export class DSpaceObjectDataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected halService: HALEndpointService) { - this.dataService = new DataServiceImpl(null, requestService, rdbService, null, halService); + protected halService: HALEndpointService, + protected objectCache: ObjectCacheService) { + this.dataService = new DataServiceImpl(null, requestService, rdbService, null, halService, objectCache); } findById(uuid: string): Observable> { diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index 0a233d086c..df0be98f7a 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -8,6 +8,7 @@ import { CoreState } from '../core.reducers'; import { ItemDataService } from './item-data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; describe('ItemDataService', () => { let scheduler: TestScheduler; @@ -16,6 +17,7 @@ describe('ItemDataService', () => { const requestService = {} as RequestService; const responseCache = {} as ResponseCacheService; const rdbService = {} as RemoteDataBuildService; + const objectCache = {} as ObjectCacheService; const store = {} as Store; const halEndpointService = {} as HALEndpointService; @@ -42,7 +44,8 @@ describe('ItemDataService', () => { rdbService, store, bs, - halEndpointService + halEndpointService, + objectCache ); } diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index dead4e5f48..7a0a28729c 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -17,6 +17,7 @@ import { URLCombiner } from '../url-combiner/url-combiner'; import { DataService } from './data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; @Injectable() export class ItemDataService extends DataService { @@ -28,7 +29,8 @@ export class ItemDataService extends DataService { protected rdbService: RemoteDataBuildService, protected store: Store, private bs: BrowseService, - protected halService: HALEndpointService) { + protected halService: HALEndpointService, + protected objectCache: ObjectCacheService) { super(); } diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 101825e3db..e944bcb19f 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -17,6 +17,7 @@ import { RequestConfigureAction, RequestExecuteAction } from './request.actions' import { GetRequest, RestRequest, RestRequestMethod } from './request.models'; import { RequestEntry } from './request.reducer'; +import { CommitSSBAction } from '../cache/server-sync-buffer.actions'; @Injectable() export class RequestService { @@ -123,4 +124,8 @@ export class RequestService { this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((pendingHref: string) => pendingHref !== request.href) }); } + + commit(method?: RestRequestMethod) { + this.store.dispatch(new CommitSSBAction(method)) + } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html index d838a90f20..750ef721c2 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html @@ -1,8 +1,8 @@ -
- diff --git a/src/app/shared/mocks/mock-form-service.ts b/src/app/shared/mocks/mock-form-service.ts index e92be02fcc..1f340f6df6 100644 --- a/src/app/shared/mocks/mock-form-service.ts +++ b/src/app/shared/mocks/mock-form-service.ts @@ -10,7 +10,12 @@ export function getMockFormService( getUniqueId: id$, resetForm: {}, validateAllFormFields: {}, - getForm: errors.pipe(map((err) => { return {data: {}, valid: true, errors: err} })), + getForm: errors.pipe( + map((err) => { + return { data: {}, valid: true, errors: err } + } + ) + ), removeForm: undefined, removeError: undefined, changeForm: undefined, diff --git a/src/app/shared/number-picker/number-picker.component.ts b/src/app/shared/number-picker/number-picker.component.ts index 0bc47e9438..bf3a7e9e7a 100644 --- a/src/app/shared/number-picker/number-picker.component.ts +++ b/src/app/shared/number-picker/number-picker.component.ts @@ -99,7 +99,7 @@ export class NumberPickerComponent implements OnInit, ControlValueAccessor { update(event) { try { - const i = Number.parseInt(event.target.value); + const i = Number.parseInt(event.target.value, 10); if (i >= this.min && i <= this.max) { this.value = i; diff --git a/tslint.json b/tslint.json index cd7ffadac9..b4f905d324 100644 --- a/tslint.json +++ b/tslint.json @@ -151,10 +151,10 @@ "use-input-property-decorator": true, "use-life-cycle-interface": false, "use-output-property-decorator": true, - "use-pipe-transform-interface": true, - "rxjs-collapse-imports": true, - "rxjs-pipeable-operators-only": true, - "rxjs-no-static-observable-methods": true, - "rxjs-proper-imports": true + "use-pipe-transform-interface": true +// "rxjs-collapse-imports": true, +// "rxjs-pipeable-operators-only": true, +// "rxjs-no-static-observable-methods": true, +// "rxjs-proper-imports": true } } From c6f55e424bec85b1ed92419cdd09ec049357e81d Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 19 Sep 2018 15:32:14 +0200 Subject: [PATCH 38/69] Finished patch support --- src/app/core/auth/auth.interceptor.spec.ts | 10 ++--- src/app/core/cache/object-cache.service.ts | 11 ++++-- .../core/cache/server-sync-buffer.actions.ts | 6 +-- .../core/cache/server-sync-buffer.effects.ts | 12 +++--- .../core/cache/server-sync-buffer.reducer.ts | 2 +- src/app/core/core.effects.ts | 4 +- src/app/core/data/data.service.ts | 2 +- src/app/core/data/request.models.ts | 38 +++++-------------- src/app/core/data/request.service.ts | 5 ++- src/app/core/data/rest-request-method.ts | 18 +++++++++ .../dspace-rest-v2/dspace-rest-v2.service.ts | 2 +- src/app/core/index/index.effects.ts | 4 +- src/config/auto-sync-config.interface.ts | 3 +- src/config/cache-config.interface.ts | 2 +- 14 files changed, 61 insertions(+), 58 deletions(-) create mode 100644 src/app/core/data/rest-request-method.ts diff --git a/src/app/core/auth/auth.interceptor.spec.ts b/src/app/core/auth/auth.interceptor.spec.ts index 89c9ed1951..4505ca7896 100644 --- a/src/app/core/auth/auth.interceptor.spec.ts +++ b/src/app/core/auth/auth.interceptor.spec.ts @@ -9,10 +9,10 @@ import { of as observableOf } from 'rxjs'; import { AuthInterceptor } from './auth.interceptor'; import { AuthService } from './auth.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 { TruncatablesState } from '../../shared/truncatable/truncatable.reducer'; import { AuthServiceStub } from '../../shared/testing/auth-service-stub'; +import { RestRequestMethod } from '../data//rest-request-method'; describe(`AuthInterceptor`, () => { let service: DSpaceRESTv2Service; @@ -49,7 +49,7 @@ describe(`AuthInterceptor`, () => { describe('when has a valid token', () => { 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(); }); @@ -60,7 +60,7 @@ describe(`AuthInterceptor`, () => { }); 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(); }); @@ -85,11 +85,11 @@ describe(`AuthInterceptor`, () => { 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(); }); - 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'); }); diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index f6b295afb4..5db2d73d4e 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -1,6 +1,6 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { distinctUntilChanged, filter, first, map, mergeMap, take, tap } from 'rxjs/operators'; +import { distinctUntilChanged, filter, first, map, mergeMap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { MemoizedSelector, select, Store } from '@ngrx/store'; import { IndexName } from '../index/index.reducer'; @@ -8,7 +8,8 @@ import { IndexName } from '../index/index.reducer'; import { CacheableObject, ObjectCacheEntry } from './object-cache.reducer'; import { AddPatchObjectCacheAction, - AddToObjectCacheAction, ApplyPatchObjectCacheAction, + AddToObjectCacheAction, + ApplyPatchObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions'; import { hasNoValue, isNotEmpty } from '../../shared/empty.util'; @@ -18,8 +19,8 @@ import { pathSelector } from '../shared/selectors'; import { NormalizedObjectFactory } from './models/normalized-object-factory'; import { NormalizedObject } from './models/normalized-object.model'; import { applyPatch, Operation } from 'fast-json-patch'; -import { RestRequestMethod } from '../data/request.models'; import { AddToSSBAction } from './server-sync-buffer.actions'; +import { RestRequestMethod } from '../data//rest-request-method'; function selfLinkFromUuidSelector(uuid: string): MemoizedSelector { return pathSelector(coreSelector, 'index', IndexName.OBJECT, uuid); @@ -209,6 +210,8 @@ export class ObjectCacheService { } } + + /** * Add operations to the existing list of operations for an ObjectCacheEntry * Makes sure the ServerSyncBuffer for this ObjectCacheEntry is updated @@ -219,7 +222,7 @@ export class ObjectCacheService { */ public addPatch(uuid: string, patch: Operation[]) { this.store.dispatch(new AddPatchObjectCacheAction(uuid, patch)); - this.store.dispatch(new AddToSSBAction(uuid, RestRequestMethod.Patch)); + this.store.dispatch(new AddToSSBAction(uuid, RestRequestMethod.PATCH)); } /** diff --git a/src/app/core/cache/server-sync-buffer.actions.ts b/src/app/core/cache/server-sync-buffer.actions.ts index 9189af4471..b164a41fd9 100644 --- a/src/app/core/cache/server-sync-buffer.actions.ts +++ b/src/app/core/cache/server-sync-buffer.actions.ts @@ -1,9 +1,7 @@ import { Action } from '@ngrx/store'; import { type } from '../../shared/ngrx/type'; -import { CacheableObject } from './object-cache.reducer'; -import { Operation } from 'fast-json-patch'; -import { RestRequest, RestRequestMethod } from '../data/request.models'; +import { RestRequestMethod } from '../data//rest-request-method'; /** * The list of ServerSyncBufferAction type definitions @@ -33,7 +31,7 @@ export class AddToSSBAction implements Action { * the unique href of the cached object entry that should be updated */ constructor(href: string, method: RestRequestMethod) { - this.payload = { href, method }; + this.payload = { href, method: undefined }; } } diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts index d6c1941e86..e5ee2cf28e 100644 --- a/src/app/core/cache/server-sync-buffer.effects.ts +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -14,22 +14,24 @@ import { Action, select, Store } from '@ngrx/store'; import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer'; import { of as observableOf, combineLatest as observableCombineLatest } from 'rxjs'; import { RequestService } from '../data/request.service'; -import { PutRequest, RestRequestMethod } from '../data/request.models'; +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 } from '../../shared/empty.util'; import { Observable } from 'rxjs/internal/Observable'; +import { RestRequestMethod } from '../data//rest-request-method'; @Injectable() -export class ObjectCacheEffects { +export class ServerSyncBufferEffects { @Effect() setTimeoutForServerSync = this.actions$ .pipe( ofType(ServerSyncBufferActionTypes.ADD), exhaustMap((action: AddToSSBAction) => { - const autoSyncConfig = this.EnvConfig.cache.autoSync; - const timeoutInSeconds = autoSyncConfig.timePerMethod[action.type] || autoSyncConfig.defaultTime; + // const autoSyncConfig = this.EnvConfig.cache.autoSync; + // const timeoutInSeconds = autoSyncConfig.timePerMethod[action.type] || autoSyncConfig.defaultTime; + const timeoutInSeconds = 0; return observableOf(new CommitSSBAction(action.payload.method)).pipe(delay(timeoutInSeconds * 1000)) }) ); @@ -51,7 +53,7 @@ export class ObjectCacheEffects { return true; }) .map((entry: ServerSyncBufferEntry) => { - if (entry.method === RestRequestMethod.Patch) { + if (entry.method === RestRequestMethod.PATCH) { return this.applyPatch(entry.href); } else { /* TODO other request stuff */ diff --git a/src/app/core/cache/server-sync-buffer.reducer.ts b/src/app/core/cache/server-sync-buffer.reducer.ts index 1fabe5a50a..3354afcd42 100644 --- a/src/app/core/cache/server-sync-buffer.reducer.ts +++ b/src/app/core/cache/server-sync-buffer.reducer.ts @@ -1,4 +1,3 @@ -import { RestRequestMethod } from '../data/request.models'; import { hasNoValue, hasValue } from '../../shared/empty.util'; import { AddToSSBAction, @@ -6,6 +5,7 @@ import { ServerSyncBufferAction, ServerSyncBufferActionTypes } from './server-sync-buffer.actions'; +import { RestRequestMethod } from '../data//rest-request-method'; /** * An entry in the ServerSyncBufferState diff --git a/src/app/core/core.effects.ts b/src/app/core/core.effects.ts index bc534a36b0..881f01aed0 100644 --- a/src/app/core/core.effects.ts +++ b/src/app/core/core.effects.ts @@ -4,11 +4,13 @@ import { ResponseCacheEffects } from './cache/response-cache.effects'; import { UUIDIndexEffects } from './index/index.effects'; import { RequestEffects } from './data/request.effects'; import { AuthEffects } from './auth/auth.effects'; +import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects'; export const coreEffects = [ ResponseCacheEffects, RequestEffects, ObjectCacheEffects, UUIDIndexEffects, - AuthEffects + AuthEffects, + ServerSyncBufferEffects ]; diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index f58868d2ce..5af4b7d09a 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -121,7 +121,7 @@ export abstract class DataService // .filter((href: string) => hasValue(href)) // .take(1) // .subscribe((href: string) => { - // const request = new RestRequest(this.requestService.generateRequestId(), href, RestRequestMethod.Post, dso); + // const request = new RestRequest(this.requestService.generateRequestId(), href, RestRequestMethod.POST, dso); // this.requestService.configure(request); // }); // diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index ce45d5b41c..42c4dd4eff 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -1,7 +1,5 @@ import { SortOptions } from '../cache/models/sort-options.model'; import { GenericConstructor } from '../shared/generic-constructor'; -import { GlobalConfig } from '../../../config/global-config.interface'; -import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service'; import { DSOResponseParsingService } from './dso-response-parsing.service'; import { ResponseParsingService } from './parsing.service'; @@ -10,35 +8,17 @@ import { BrowseResponseParsingService } from './browse-response-parsing.service' import { ConfigResponseParsingService } from './config-response-parsing.service'; import { AuthResponseParsingService } from '../auth/auth-response-parsing.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; -import { HttpHeaders } from '@angular/common/http'; import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service'; +import { RestRequestMethod } from './/rest-request-method'; /* tslint:disable:max-classes-per-file */ -/** - * Represents a Request Method. - * - * I didn't reuse the RequestMethod enum in @angular/http because - * it uses numbers. The string values here are more clear when - * debugging. - * - * The ones commented out are still unsupported in the rest of the codebase - */ -export enum RestRequestMethod { - Get = 'GET', - Post = 'POST', - Put = 'PUT', - Delete = 'DELETE', - Options = 'OPTIONS', - Head = 'HEAD', - Patch = 'PATCH' -} export abstract class RestRequest { constructor( public uuid: string, public href: string, - public method: RestRequestMethod = RestRequestMethod.Get, + public method: RestRequestMethod = RestRequestMethod.GET, public body?: any, public options?: HttpOptions ) { @@ -56,7 +36,7 @@ export class GetRequest extends RestRequest { public body?: any, public options?: HttpOptions ) { - super(uuid, href, RestRequestMethod.Get, body) + super(uuid, href, RestRequestMethod.GET, body) } } @@ -67,7 +47,7 @@ export class PostRequest extends RestRequest { public body?: any, public options?: HttpOptions ) { - super(uuid, href, RestRequestMethod.Post, body) + super(uuid, href, RestRequestMethod.POST, body) } } @@ -78,7 +58,7 @@ export class PutRequest extends RestRequest { public body?: any, public options?: HttpOptions ) { - super(uuid, href, RestRequestMethod.Put, body) + super(uuid, href, RestRequestMethod.PUT, body) } } @@ -89,7 +69,7 @@ export class DeleteRequest extends RestRequest { public body?: any, public options?: HttpOptions ) { - super(uuid, href, RestRequestMethod.Delete, body) + super(uuid, href, RestRequestMethod.DELETE, body) } } @@ -100,7 +80,7 @@ export class OptionsRequest extends RestRequest { public body?: any, public options?: HttpOptions ) { - super(uuid, href, RestRequestMethod.Options, body) + super(uuid, href, RestRequestMethod.OPTIONS, body) } } @@ -111,7 +91,7 @@ export class HeadRequest extends RestRequest { public body?: any, public options?: HttpOptions ) { - super(uuid, href, RestRequestMethod.Head, body) + super(uuid, href, RestRequestMethod.HEAD, body) } } @@ -122,7 +102,7 @@ export class PatchRequest extends RestRequest { public body?: any, public options?: HttpOptions ) { - super(uuid, href, RestRequestMethod.Patch, body) + super(uuid, href, RestRequestMethod.PATCH, body) } } diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index e944bcb19f..8728c223ea 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -14,10 +14,11 @@ import { IndexName } from '../index/index.reducer'; import { pathSelector } from '../shared/selectors'; import { UUIDService } from '../shared/uuid.service'; import { RequestConfigureAction, RequestExecuteAction } from './request.actions'; -import { GetRequest, RestRequest, RestRequestMethod } from './request.models'; +import { GetRequest, RestRequest } from './request.models'; import { RequestEntry } from './request.reducer'; import { CommitSSBAction } from '../cache/server-sync-buffer.actions'; +import { RestRequestMethod } from './/rest-request-method'; @Injectable() export class RequestService { @@ -71,7 +72,7 @@ export class RequestService { // TODO to review "overrideRequest" param when https://github.com/DSpace/dspace-angular/issues/217 will be fixed configure(request: RestRequest, forceBypassCache: boolean = false): void { - const isGetRequest = request.method === RestRequestMethod.Get; + const isGetRequest = request.method === RestRequestMethod.GET; if (!isGetRequest || !this.isCachedOrPending(request) || forceBypassCache) { this.dispatchRequest(request); if (isGetRequest && !forceBypassCache) { diff --git a/src/app/core/data/rest-request-method.ts b/src/app/core/data/rest-request-method.ts new file mode 100644 index 0000000000..03ae7ad0c4 --- /dev/null +++ b/src/app/core/data/rest-request-method.ts @@ -0,0 +1,18 @@ +/** + * Represents a Request Method. + * + * I didn't reuse the RequestMethod enum in @angular/http because + * it uses numbers. The string values here are more clear when + * debugging. + * + * The ones commented out are still unsupported in the rest of the codebase + */ +export enum RestRequestMethod { + GET = 'GET', + POST = 'POST', + PUT = 'PUT', + DELETE = 'DELETE', + OPTIONS = 'OPTIONS', + HEAD = 'HEAD', + PATCH = 'PATCH' +} diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts index 1570613c17..c8ce76a554 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts @@ -3,10 +3,10 @@ import {catchError, map} from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { Request } from '@angular/http'; import { HttpClient, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http' -import { RestRequestMethod } from '../data/request.models'; import { DSpaceRESTV2Response } from './dspace-rest-v2-response.model'; import { HttpObserve } from '@angular/common/http/src/client'; +import { RestRequestMethod } from '../data//rest-request-method'; export interface HttpOptions { body?: any; diff --git a/src/app/core/index/index.effects.ts b/src/app/core/index/index.effects.ts index de1ba681a2..0d783767a6 100644 --- a/src/app/core/index/index.effects.ts +++ b/src/app/core/index/index.effects.ts @@ -7,10 +7,10 @@ import { RemoveFromObjectCacheAction } from '../cache/object-cache.actions'; import { RequestActionTypes, RequestConfigureAction } from '../data/request.actions'; -import { RestRequestMethod } from '../data/request.models'; import { AddToIndexAction, RemoveFromIndexByValueAction } from './index.actions'; import { hasValue } from '../../shared/empty.util'; import { IndexName } from './index.reducer'; +import { RestRequestMethod } from '../data//rest-request-method'; @Injectable() export class UUIDIndexEffects { @@ -42,7 +42,7 @@ export class UUIDIndexEffects { @Effect() addRequest$ = this.actions$ .pipe( ofType(RequestActionTypes.CONFIGURE), - filter((action: RequestConfigureAction) => action.payload.method === RestRequestMethod.Get), + filter((action: RequestConfigureAction) => action.payload.method === RestRequestMethod.GET), map((action: RequestConfigureAction) => { return new AddToIndexAction( IndexName.REQUEST, diff --git a/src/config/auto-sync-config.interface.ts b/src/config/auto-sync-config.interface.ts index 4ad74005bf..d7c5ce83c5 100644 --- a/src/config/auto-sync-config.interface.ts +++ b/src/config/auto-sync-config.interface.ts @@ -1,6 +1,5 @@ -import { RestRequestMethod } from '../app/core/data/request.models'; +import { RestRequestMethod } from '../app/core/data//rest-request-method'; -/* enum indices */ type TimePerMethod = { [method in RestRequestMethod]: number; }; diff --git a/src/config/cache-config.interface.ts b/src/config/cache-config.interface.ts index 3e9334e300..b8651ed107 100644 --- a/src/config/cache-config.interface.ts +++ b/src/config/cache-config.interface.ts @@ -4,5 +4,5 @@ import { AutoSyncConfig } from './auto-sync-config.interface'; export interface CacheConfig extends Config { msToLive: number, control: string, - autoSync: AutoSyncConfig + // autoSync: AutoSyncConfig } From bae69aef9761ee957fc81d3191125b90aa4e9b11 Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 19 Sep 2018 15:34:14 +0200 Subject: [PATCH 39/69] Small fix --- src/app/core/auth/auth.interceptor.spec.ts | 2 +- src/app/core/cache/object-cache.service.ts | 2 +- src/app/core/cache/server-sync-buffer.actions.ts | 2 +- src/app/core/cache/server-sync-buffer.effects.ts | 2 +- src/app/core/cache/server-sync-buffer.reducer.ts | 2 +- src/app/core/data/request.models.ts | 2 +- src/app/core/data/request.service.ts | 2 +- src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts | 3 +-- src/app/core/index/index.effects.ts | 2 +- src/config/auto-sync-config.interface.ts | 2 +- 10 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/app/core/auth/auth.interceptor.spec.ts b/src/app/core/auth/auth.interceptor.spec.ts index 4505ca7896..72b0cc2616 100644 --- a/src/app/core/auth/auth.interceptor.spec.ts +++ b/src/app/core/auth/auth.interceptor.spec.ts @@ -12,7 +12,7 @@ import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service'; import { RouterStub } from '../../shared/testing/router-stub'; import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer'; import { AuthServiceStub } from '../../shared/testing/auth-service-stub'; -import { RestRequestMethod } from '../data//rest-request-method'; +import { RestRequestMethod } from '../data/rest-request-method'; describe(`AuthInterceptor`, () => { let service: DSpaceRESTv2Service; diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 5db2d73d4e..5b0d818dd9 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -20,7 +20,7 @@ import { NormalizedObjectFactory } from './models/normalized-object-factory'; 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'; +import { RestRequestMethod } from '../data/rest-request-method'; function selfLinkFromUuidSelector(uuid: string): MemoizedSelector { return pathSelector(coreSelector, 'index', IndexName.OBJECT, uuid); diff --git a/src/app/core/cache/server-sync-buffer.actions.ts b/src/app/core/cache/server-sync-buffer.actions.ts index b164a41fd9..2399db7c4d 100644 --- a/src/app/core/cache/server-sync-buffer.actions.ts +++ b/src/app/core/cache/server-sync-buffer.actions.ts @@ -1,7 +1,7 @@ import { Action } from '@ngrx/store'; import { type } from '../../shared/ngrx/type'; -import { RestRequestMethod } from '../data//rest-request-method'; +import { RestRequestMethod } from '../data/rest-request-method'; /** * The list of ServerSyncBufferAction type definitions diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts index e5ee2cf28e..8847b1d655 100644 --- a/src/app/core/cache/server-sync-buffer.effects.ts +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -21,7 +21,7 @@ import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.seriali import { GenericConstructor } from '../shared/generic-constructor'; import { hasValue } from '../../shared/empty.util'; import { Observable } from 'rxjs/internal/Observable'; -import { RestRequestMethod } from '../data//rest-request-method'; +import { RestRequestMethod } from '../data/rest-request-method'; @Injectable() export class ServerSyncBufferEffects { diff --git a/src/app/core/cache/server-sync-buffer.reducer.ts b/src/app/core/cache/server-sync-buffer.reducer.ts index 3354afcd42..3e3715d186 100644 --- a/src/app/core/cache/server-sync-buffer.reducer.ts +++ b/src/app/core/cache/server-sync-buffer.reducer.ts @@ -5,7 +5,7 @@ import { ServerSyncBufferAction, ServerSyncBufferActionTypes } from './server-sync-buffer.actions'; -import { RestRequestMethod } from '../data//rest-request-method'; +import { RestRequestMethod } from '../data/rest-request-method'; /** * An entry in the ServerSyncBufferState diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index 42c4dd4eff..75f1aac581 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -9,7 +9,7 @@ import { ConfigResponseParsingService } from './config-response-parsing.service' import { AuthResponseParsingService } from '../auth/auth-response-parsing.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service'; -import { RestRequestMethod } from './/rest-request-method'; +import { RestRequestMethod } from './rest-request-method'; /* tslint:disable:max-classes-per-file */ diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 8728c223ea..71d0189816 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -18,7 +18,7 @@ import { GetRequest, RestRequest } from './request.models'; import { RequestEntry } from './request.reducer'; import { CommitSSBAction } from '../cache/server-sync-buffer.actions'; -import { RestRequestMethod } from './/rest-request-method'; +import { RestRequestMethod } from './rest-request-method'; @Injectable() export class RequestService { diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts index c8ce76a554..7173e5ba0d 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts @@ -1,12 +1,11 @@ import {throwError as observableThrowError, Observable } from 'rxjs'; import {catchError, map} from 'rxjs/operators'; import { Injectable } from '@angular/core'; -import { Request } from '@angular/http'; import { HttpClient, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http' import { DSpaceRESTV2Response } from './dspace-rest-v2-response.model'; import { HttpObserve } from '@angular/common/http/src/client'; -import { RestRequestMethod } from '../data//rest-request-method'; +import { RestRequestMethod } from '../data/rest-request-method'; export interface HttpOptions { body?: any; diff --git a/src/app/core/index/index.effects.ts b/src/app/core/index/index.effects.ts index 0d783767a6..b152f8488d 100644 --- a/src/app/core/index/index.effects.ts +++ b/src/app/core/index/index.effects.ts @@ -10,7 +10,7 @@ import { RequestActionTypes, RequestConfigureAction } from '../data/request.acti import { AddToIndexAction, RemoveFromIndexByValueAction } from './index.actions'; import { hasValue } from '../../shared/empty.util'; import { IndexName } from './index.reducer'; -import { RestRequestMethod } from '../data//rest-request-method'; +import { RestRequestMethod } from '../data/rest-request-method'; @Injectable() export class UUIDIndexEffects { diff --git a/src/config/auto-sync-config.interface.ts b/src/config/auto-sync-config.interface.ts index d7c5ce83c5..5285916b12 100644 --- a/src/config/auto-sync-config.interface.ts +++ b/src/config/auto-sync-config.interface.ts @@ -1,4 +1,4 @@ -import { RestRequestMethod } from '../app/core/data//rest-request-method'; +import { RestRequestMethod } from '../app/core/data/rest-request-method'; type TimePerMethod = { [method in RestRequestMethod]: number; From 8c05ed86ef8e8a8d1914eeb137197463dedf13b1 Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 26 Sep 2018 15:31:26 +0200 Subject: [PATCH 40/69] intermediate bug commit --- config/environment.default.js | 2 +- .../community-page.component.html | 20 ++++++++- .../community-page.component.ts | 15 ++++++- .../search-text-filter.component.ts | 4 +- src/app/core/cache/object-cache.reducer.ts | 8 ++-- src/app/core/cache/object-cache.service.ts | 2 +- .../core/cache/server-sync-buffer.actions.ts | 2 +- .../core/cache/server-sync-buffer.effects.ts | 41 ++++++++++++------- src/config/cache-config.interface.ts | 2 +- 9 files changed, 69 insertions(+), 27 deletions(-) diff --git a/config/environment.default.js b/config/environment.default.js index 32edd1391d..42c3fc2452 100644 --- a/config/environment.default.js +++ b/config/environment.default.js @@ -24,7 +24,7 @@ module.exports = { autoSync: { defaultTime: 0, maxBufferSize: 100, - timePerMethod: {'Patch': 30} //time in seconds + timePerMethod: {'PATCH': 3} //time in seconds } }, // Form settings diff --git a/src/app/+community-page/community-page.component.html b/src/app/+community-page/community-page.component.html index 637e37af0c..ce86171370 100644 --- a/src/app/+community-page/community-page.component.html +++ b/src/app/+community-page/community-page.component.html @@ -24,9 +24,25 @@ [content]="communityPayload.copyrightText" [hasInnerHtml]="true"> - +
+
+
+ +
+ +
+ +
+
+
+
+
+
- + diff --git a/src/app/+community-page/community-page.component.ts b/src/app/+community-page/community-page.component.ts index a6e1d5376c..5a36209c28 100644 --- a/src/app/+community-page/community-page.component.ts +++ b/src/app/+community-page/community-page.component.ts @@ -1,4 +1,4 @@ -import {mergeMap, filter, map} from 'rxjs/operators'; +import { mergeMap, filter, map, first, tap } from 'rxjs/operators'; import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; @@ -24,6 +24,9 @@ import { hasValue } from '../shared/empty.util'; export class CommunityPageComponent implements OnInit, OnDestroy { communityRD$: Observable>; logoRD$: Observable>; + href: string; + newname: string; + private subs: Subscription[] = []; constructor( @@ -40,10 +43,20 @@ export class CommunityPageComponent implements OnInit, OnDestroy { map((rd: RemoteData) => rd.payload), filter((community: Community) => hasValue(community)), mergeMap((community: Community) => community.logo)); + + this.communityRD$.pipe(first()).subscribe((crd) => { + this.href = crd.payload.self; + this.newname = crd.payload.name; + }); } ngOnDestroy(): void { this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); } + patchIt(): void { + console.log('patching it!', this.href, this.newname); + this.communityDataService.patch(this.href, [{ op: 'replace', path: '/name', value: this.newname }]); + } + } diff --git a/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.ts index bb396a6692..fd14d6d3de 100644 --- a/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.ts @@ -1,6 +1,4 @@ -import { animate, state, style, transition, trigger } from '@angular/animations'; -import { Component, HostBinding, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; +import { Component, OnInit } from '@angular/core'; import { FilterType } from '../../../search-service/filter-type.model'; import { facetLoad, diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index adea46bc57..f856b57892 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -182,8 +182,10 @@ function addPatchObjectCache(state: ObjectCacheState, action: AddPatchObjectCach const newState = Object.assign({}, state); if (hasValue(newState[uuid])) { const patches = newState[uuid].patches; - newState[uuid].patches = [...patches, {operations} as Patch]; - newState[uuid].isDirty = true; + newState[uuid] = Object.assign({}, newState[uuid], { + patches: [...patches, { operations } as Patch], + isDirty: true + }); } return newState; } @@ -203,7 +205,7 @@ function applyPatchObjectCache(state: ObjectCacheState, action: ApplyPatchObject const newState = Object.assign({}, state); if (hasValue(newState[uuid])) { // flatten two dimensional array - const flatPatch: Operation[] = [].concat(...newState[uuid].patches); + const flatPatch: Operation[] = [].concat(...newState[uuid].patches.map((patch) => patch.operations)); const newData = applyPatch( newState[uuid].data, flatPatch); newState[uuid].data = newData.newDocument; newState[uuid].patches = []; diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 5b0d818dd9..f60b2de684 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -91,7 +91,7 @@ export class ObjectCacheService { getBySelfLink(selfLink: string): Observable { return this.getEntry(selfLink).pipe( map((entry: ObjectCacheEntry) => { - const flatPatch: Operation[] = [].concat(...entry.patches); + const flatPatch: Operation[] = [].concat(...entry.patches.map((patch) => patch.operations)); const patchedData = applyPatch(entry.data, flatPatch).newDocument; return Object.assign({}, entry, { data: patchedData }); } diff --git a/src/app/core/cache/server-sync-buffer.actions.ts b/src/app/core/cache/server-sync-buffer.actions.ts index 2399db7c4d..69eb81b02f 100644 --- a/src/app/core/cache/server-sync-buffer.actions.ts +++ b/src/app/core/cache/server-sync-buffer.actions.ts @@ -31,7 +31,7 @@ export class AddToSSBAction implements Action { * the unique href of the cached object entry that should be updated */ constructor(href: string, method: RestRequestMethod) { - this.payload = { href, method: undefined }; + this.payload = { href, method: method }; } } diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts index 8847b1d655..1211740ffe 100644 --- a/src/app/core/cache/server-sync-buffer.effects.ts +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -1,4 +1,4 @@ -import { delay, exhaustMap, first, map, switchMap, tap } from 'rxjs/operators'; +import { delay, exhaustMap, first, map, startWith, switchMap, tap } from 'rxjs/operators'; import { Inject, Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { @@ -9,17 +9,17 @@ import { } from './server-sync-buffer.actions'; import { GLOBAL_CONFIG } from '../../../config'; import { GlobalConfig } from '../../../config/global-config.interface'; -import { CoreState } from '../core.reducers'; -import { Action, select, Store } from '@ngrx/store'; +import { coreSelector, CoreState } from '../core.reducers'; +import { Action, createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer'; -import { of as observableOf, combineLatest as observableCombineLatest } from 'rxjs'; +import { of as observableOf, combineLatest as observableCombineLatest, empty as observableEmpty } 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 } from '../../shared/empty.util'; +import { hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; import { Observable } from 'rxjs/internal/Observable'; import { RestRequestMethod } from '../data/rest-request-method'; @@ -29,9 +29,8 @@ export class ServerSyncBufferEffects { .pipe( ofType(ServerSyncBufferActionTypes.ADD), exhaustMap((action: AddToSSBAction) => { - // const autoSyncConfig = this.EnvConfig.cache.autoSync; - // const timeoutInSeconds = autoSyncConfig.timePerMethod[action.type] || autoSyncConfig.defaultTime; - const timeoutInSeconds = 0; + 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)) }) ); @@ -41,7 +40,7 @@ export class ServerSyncBufferEffects { ofType(ServerSyncBufferActionTypes.COMMIT), switchMap((action: CommitSSBAction) => { return this.store.pipe( - select(serverSyncBufferSelector), + select(serverSyncBufferSelector()), map((bufferState: ServerSyncBufferState) => { const actions: Array> = bufferState.buffer .filter((entry: ServerSyncBufferEntry) => { @@ -54,29 +53,41 @@ export class ServerSyncBufferEffects { }) .map((entry: ServerSyncBufferEntry) => { if (entry.method === RestRequestMethod.PATCH) { + console.log(this.applyPatch(entry.href)); return this.applyPatch(entry.href); } else { /* TODO other request stuff */ } }); + console.log(actions); /* Add extra action to array, to make sure the ServerSyncBuffer is emptied afterwards */ - return observableCombineLatest(actions).pipe( - map((array) => array.push(new EmptySSBAction(action.payload))) - ); + if (isNotEmpty(actions) && isNotUndefined(actions[0])) { + return observableCombineLatest(...actions).pipe( + map((array) => { + console.log(array); + return array.push(new EmptySSBAction(action.payload)); + }) + ); + } else { + return { type:'NO_ACTION' }; + } }) ) }) ); + private applyPatch(href: string): Observable { const patchObject = this.objectCache.getBySelfLink(href); - return patchObject.pipe( + const test = patchObject.pipe( map((object) => { const serializedObject = new DSpaceRESTv2Serializer(object.constructor as GenericConstructor<{}>).serialize(object); this.requestService.configure(new PutRequest(this.requestService.generateRequestId(), href, serializedObject)); + console.log(new ApplyPatchObjectCacheAction(href)); return new ApplyPatchObjectCacheAction(href) }) ) + return test; } constructor(private actions$: Actions, @@ -88,4 +99,6 @@ export class ServerSyncBufferEffects { } } -export const serverSyncBufferSelector = (state: CoreState) => state['cache/syncbuffer']; +export function serverSyncBufferSelector(): MemoizedSelector { + return createSelector(coreSelector, (state: CoreState) => state['cache/syncbuffer']); +} diff --git a/src/config/cache-config.interface.ts b/src/config/cache-config.interface.ts index b8651ed107..3e9334e300 100644 --- a/src/config/cache-config.interface.ts +++ b/src/config/cache-config.interface.ts @@ -4,5 +4,5 @@ import { AutoSyncConfig } from './auto-sync-config.interface'; export interface CacheConfig extends Config { msToLive: number, control: string, - // autoSync: AutoSyncConfig + autoSync: AutoSyncConfig } From 1680f67744832dbb7ee9bbcbf2cdf96ea14048dd Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 27 Sep 2018 16:56:27 +0200 Subject: [PATCH 41/69] finalized put/patch requests --- .../collection-page.component.html | 14 ++++++++++ .../collection-page.component.ts | 13 ++++++++- .../community-page.component.html | 14 ---------- .../community-page.component.ts | 13 ++------- .../auth-response-parsing.service.spec.ts | 4 +-- src/app/core/auth/auth.service.ts | 1 - src/app/core/auth/models/auth-status.model.ts | 2 +- src/app/core/cache/object-cache.reducer.ts | 8 +++--- src/app/core/cache/object-cache.service.ts | 10 +++---- .../core/cache/server-sync-buffer.effects.ts | 28 ++++++++----------- .../mocks/mock-remote-data-build.service.ts | 7 ++--- src/app/shared/mocks/mock-store.ts | 4 --- src/app/shared/services/route.service.ts | 2 +- .../testing/auth-request-service-stub.ts | 4 +-- 14 files changed, 59 insertions(+), 65 deletions(-) diff --git a/src/app/+collection-page/collection-page.component.html b/src/app/+collection-page/collection-page.component.html index a233163070..90ae0cdf4c 100644 --- a/src/app/+collection-page/collection-page.component.html +++ b/src/app/+collection-page/collection-page.component.html @@ -33,6 +33,20 @@ [content]="collection.license" [title]="'collection.page.license'"> +
+
+
+ +
+ +
+ +
+
+
+
+
diff --git a/src/app/+collection-page/collection-page.component.ts b/src/app/+collection-page/collection-page.component.ts index 59cf83777f..95aa8ddc4a 100644 --- a/src/app/+collection-page/collection-page.component.ts +++ b/src/app/+collection-page/collection-page.component.ts @@ -15,7 +15,7 @@ import { Item } from '../core/shared/item.model'; import { fadeIn, fadeInOut } from '../shared/animations/fade'; import { hasValue, isNotEmpty } from '../shared/empty.util'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; -import { filter, flatMap, map } from 'rxjs/operators'; +import { filter, first, flatMap, map } from 'rxjs/operators'; import { SearchService } from '../+search-page/search-service/search.service'; import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model'; import { toDSpaceObjectListRD } from '../core/shared/operators'; @@ -39,6 +39,8 @@ export class CollectionPageComponent implements OnInit, OnDestroy { sortConfig: SortOptions; private subs: Subscription[] = []; private collectionId: string; + href: string; + newname: string; constructor( private collectionDataService: CollectionDataService, @@ -77,6 +79,10 @@ export class CollectionPageComponent implements OnInit, OnDestroy { }); }) ); + this.collectionRD$.pipe(first()).subscribe((crd) => { + this.href = crd.payload.self; + this.newname = crd.payload.name; + }); } updatePage(searchOptions) { @@ -109,4 +115,9 @@ export class CollectionPageComponent implements OnInit, OnDestroy { } }) } + + patchIt(): void { + console.log('patching it!', this.href, this.newname); + this.collectionDataService.patch(this.href, [{ op: 'replace', path: '/name', value: this.newname }]); + } } diff --git a/src/app/+community-page/community-page.component.html b/src/app/+community-page/community-page.component.html index ce86171370..1bf322a688 100644 --- a/src/app/+community-page/community-page.component.html +++ b/src/app/+community-page/community-page.component.html @@ -24,20 +24,6 @@ [content]="communityPayload.copyrightText" [hasInnerHtml]="true"> -
-
-
- -
- -
- -
-
-
-
-
diff --git a/src/app/+community-page/community-page.component.ts b/src/app/+community-page/community-page.component.ts index 5a36209c28..ce260aefc0 100644 --- a/src/app/+community-page/community-page.component.ts +++ b/src/app/+community-page/community-page.component.ts @@ -24,8 +24,7 @@ import { hasValue } from '../shared/empty.util'; export class CommunityPageComponent implements OnInit, OnDestroy { communityRD$: Observable>; logoRD$: Observable>; - href: string; - newname: string; + private subs: Subscription[] = []; @@ -44,19 +43,13 @@ export class CommunityPageComponent implements OnInit, OnDestroy { filter((community: Community) => hasValue(community)), mergeMap((community: Community) => community.logo)); - this.communityRD$.pipe(first()).subscribe((crd) => { - this.href = crd.payload.self; - this.newname = crd.payload.name; - }); + } ngOnDestroy(): void { this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); } - patchIt(): void { - console.log('patching it!', this.href, this.newname); - this.communityDataService.patch(this.href, [{ op: 'replace', path: '/name', value: this.newname }]); - } + } diff --git a/src/app/core/auth/auth-response-parsing.service.spec.ts b/src/app/core/auth/auth-response-parsing.service.spec.ts index f6dd87e99a..4622d885c1 100644 --- a/src/app/core/auth/auth-response-parsing.service.spec.ts +++ b/src/app/core/auth/auth-response-parsing.service.spec.ts @@ -8,13 +8,13 @@ import { CoreState } from '../core.reducers'; import { AuthStatus } from './models/auth-status.model'; import { AuthResponseParsingService } from './auth-response-parsing.service'; import { AuthGetRequest, AuthPostRequest } from '../data/request.models'; -import { getMockStore } from '../../shared/mocks/mock-store'; +// import { getMockStore } from '../../shared/mocks/mock-store'; describe('AuthResponseParsingService', () => { let service: AuthResponseParsingService; const EnvConfig = {cache: {msToLive: 1000}} as GlobalConfig; - const store = getMockStore() as Store; + const store = {} as Store; const objectCacheService = new ObjectCacheService(store); beforeEach(() => { diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index ec65e3a060..42178d721d 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -17,7 +17,6 @@ import { REQUEST } from '@nguniversal/express-engine/tokens'; import { RouterReducerState } from '@ngrx/router-store'; import { select, Store } from '@ngrx/store'; import { CookieAttributes } from 'js-cookie'; -import { Observable } from 'rxjs/Observable'; import { EPerson } from '../eperson/models/eperson.model'; import { AuthRequestService } from './auth-request.service'; diff --git a/src/app/core/auth/models/auth-status.model.ts b/src/app/core/auth/models/auth-status.model.ts index b8ccf9ed6d..4cb4bd530e 100644 --- a/src/app/core/auth/models/auth-status.model.ts +++ b/src/app/core/auth/models/auth-status.model.ts @@ -2,7 +2,7 @@ import { AuthError } from './auth-error.model'; import { AuthTokenInfo } from './auth-token-info.model'; import { EPerson } from '../../eperson/models/eperson.model'; import { RemoteData } from '../../data/remote-data'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs/internal/Observable'; export class AuthStatus { diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index f856b57892..4424bb2142 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -21,6 +21,7 @@ export interface Patch { uuid?: string; operations: Operation[]; } + /**conca * An interface to represent objects that can be cached * @@ -119,7 +120,7 @@ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheActio timeAdded: action.payload.timeAdded, msToLive: action.payload.msToLive, requestHref: action.payload.requestHref, - isDirty: false, + isDirty: (hasValue(existing) ? isNotEmpty(existing.patches) : false), patches: (hasValue(existing) ? existing.patches : []) } }); @@ -206,9 +207,8 @@ function applyPatchObjectCache(state: ObjectCacheState, action: ApplyPatchObject 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); - newState[uuid].data = newData.newDocument; - newState[uuid].patches = []; + const newData = applyPatch(newState[uuid].data, flatPatch, undefined, false); + newState[uuid] = Object.assign({}, newState[uuid], { data: newData.newDocument, patches: [] }); } return newState; } diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index f60b2de684..909a160068 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -1,6 +1,6 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { distinctUntilChanged, filter, first, map, mergeMap } from 'rxjs/operators'; +import { distinctUntilChanged, filter, first, map, mergeMap, } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { MemoizedSelector, select, Store } from '@ngrx/store'; import { IndexName } from '../index/index.reducer'; @@ -18,9 +18,10 @@ import { coreSelector, CoreState } from '../core.reducers'; import { pathSelector } from '../shared/selectors'; import { NormalizedObjectFactory } from './models/normalized-object-factory'; import { NormalizedObject } from './models/normalized-object.model'; -import { applyPatch, Operation } from 'fast-json-patch'; +import { applyPatch, applyReducer, Operation } from 'fast-json-patch'; import { AddToSSBAction } from './server-sync-buffer.actions'; import { RestRequestMethod } from '../data/rest-request-method'; +import { ReplaceOperation } from 'fast-json-patch/lib/core'; function selfLinkFromUuidSelector(uuid: string): MemoizedSelector { return pathSelector(coreSelector, 'index', IndexName.OBJECT, uuid); @@ -92,7 +93,7 @@ export class ObjectCacheService { return this.getEntry(selfLink).pipe( map((entry: ObjectCacheEntry) => { const flatPatch: Operation[] = [].concat(...entry.patches.map((patch) => patch.operations)); - const patchedData = applyPatch(entry.data, flatPatch).newDocument; + const patchedData = applyPatch(entry.data, flatPatch, undefined, false).newDocument; return Object.assign({}, entry, { data: patchedData }); } ), @@ -114,7 +115,7 @@ export class ObjectCacheService { getRequestHrefBySelfLink(selfLink: string): Observable { return this.getEntry(selfLink).pipe( map((entry: ObjectCacheEntry) => entry.requestHref), - distinctUntilChanged(),); + distinctUntilChanged()); } getRequestHrefByUUID(uuid: string): Observable { @@ -211,7 +212,6 @@ export class ObjectCacheService { } - /** * Add operations to the existing list of operations for an ObjectCacheEntry * Makes sure the ServerSyncBuffer for this ObjectCacheEntry is updated diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts index 1211740ffe..d5988cb839 100644 --- a/src/app/core/cache/server-sync-buffer.effects.ts +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -1,4 +1,4 @@ -import { delay, exhaustMap, first, map, startWith, switchMap, tap } from 'rxjs/operators'; +import { delay, exhaustMap, first, map, switchMap } from 'rxjs/operators'; import { Inject, Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { @@ -12,7 +12,7 @@ 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 { of as observableOf, combineLatest as observableCombineLatest, empty as observableEmpty } from 'rxjs'; +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'; @@ -41,7 +41,7 @@ export class ServerSyncBufferEffects { switchMap((action: CommitSSBAction) => { return this.store.pipe( select(serverSyncBufferSelector()), - map((bufferState: ServerSyncBufferState) => { + switchMap((bufferState: ServerSyncBufferState) => { const actions: Array> = bufferState.buffer .filter((entry: ServerSyncBufferEntry) => { /* If there's a request method, filter @@ -53,41 +53,37 @@ export class ServerSyncBufferEffects { }) .map((entry: ServerSyncBufferEntry) => { if (entry.method === RestRequestMethod.PATCH) { - console.log(this.applyPatch(entry.href)); return this.applyPatch(entry.href); } else { /* TODO other request stuff */ } }); - console.log(actions); + /* Add extra action to array, to make sure the ServerSyncBuffer is emptied afterwards */ if (isNotEmpty(actions) && isNotUndefined(actions[0])) { return observableCombineLatest(...actions).pipe( - map((array) => { - console.log(array); - return array.push(new EmptySSBAction(action.payload)); - }) - ); + switchMap((array) => [...array, new EmptySSBAction(action.payload)]) + ); } else { - return { type:'NO_ACTION' }; + return observableOf({ type: 'NO_ACTION' }); } }) ) }) ); - private applyPatch(href: string): Observable { - const patchObject = this.objectCache.getBySelfLink(href); - const test = patchObject.pipe( + const patchObject = this.objectCache.getBySelfLink(href).pipe(first()); + + 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)); - console.log(new ApplyPatchObjectCacheAction(href)); + return new ApplyPatchObjectCacheAction(href) }) ) - return test; } constructor(private actions$: Actions, diff --git a/src/app/shared/mocks/mock-remote-data-build.service.ts b/src/app/shared/mocks/mock-remote-data-build.service.ts index 7308051c85..33d273e1ef 100644 --- a/src/app/shared/mocks/mock-remote-data-build.service.ts +++ b/src/app/shared/mocks/mock-remote-data-build.service.ts @@ -1,11 +1,10 @@ -import { Observable } from 'rxjs'; -import { map, take } from 'rxjs/operators'; +import { Observable, of as observableOf } from 'rxjs'; +import { map } from 'rxjs/operators'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; import { RemoteData } from '../../core/data/remote-data'; import { RequestEntry } from '../../core/data/request.reducer'; import { hasValue } from '../empty.util'; -import { NormalizedObject } from '../../core/cache/models/normalized-object.model'; export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observable>): RemoteDataBuildService { return { @@ -19,7 +18,7 @@ export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observab } as RemoteData))) } }, - buildSingle: (href$: string | Observable) => Observable.of(new RemoteData(false, false, true, undefined, {})) + buildSingle: (href$: string | Observable) => observableOf(new RemoteData(false, false, true, undefined, {})) } as RemoteDataBuildService; } diff --git a/src/app/shared/mocks/mock-store.ts b/src/app/shared/mocks/mock-store.ts index 7a6ca772f2..e69de29bb2 100644 --- a/src/app/shared/mocks/mock-store.ts +++ b/src/app/shared/mocks/mock-store.ts @@ -1,4 +0,0 @@ -import { Store } from '@ngrx/store'; -import { of as observableOf } from 'rxjs'; - - diff --git a/src/app/shared/services/route.service.ts b/src/app/shared/services/route.service.ts index d72367c977..9dd9a0f164 100644 --- a/src/app/shared/services/route.service.ts +++ b/src/app/shared/services/route.service.ts @@ -52,6 +52,6 @@ export class RouteService { }); return params; }), - distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),); + distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b))); } } diff --git a/src/app/shared/testing/auth-request-service-stub.ts b/src/app/shared/testing/auth-request-service-stub.ts index a7ff7dcd53..ae58fd27fa 100644 --- a/src/app/shared/testing/auth-request-service-stub.ts +++ b/src/app/shared/testing/auth-request-service-stub.ts @@ -27,7 +27,7 @@ export class AuthRequestServiceStub { if (this.validateToken(token)) { authStatusStub.authenticated = true; authStatusStub.token = this.mockTokenInfo; - authStatusStub.eperson = Observable.of(new RemoteData(false, false, true, undefined, this.mockUser)); + authStatusStub.eperson = observableOf(new RemoteData(false, false, true, undefined, this.mockUser)); } else { authStatusStub.authenticated = false; } @@ -46,7 +46,7 @@ export class AuthRequestServiceStub { if (this.validateToken(token)) { authStatusStub.authenticated = true; authStatusStub.token = this.mockTokenInfo; - authStatusStub.eperson = Observable.of(new RemoteData(false, false, true, undefined, this.mockUser)); + authStatusStub.eperson = observableOf(new RemoteData(false, false, true, undefined, this.mockUser)); } else { authStatusStub.authenticated = false; } From b308df201d9b09d3516dd6eb913372acb34d8dc7 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Fri, 28 Sep 2018 16:11:22 +0200 Subject: [PATCH 42/69] add responseMsToLive to requests --- config/environment.default.js | 5 ++++- src/app/core/data/base-response-parsing.service.ts | 2 +- src/app/core/data/request.effects.ts | 2 +- src/app/core/data/request.models.ts | 8 +++++--- src/config/cache-config.interface.ts | 5 ++++- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/config/environment.default.js b/config/environment.default.js index a6ef738f41..75be454c88 100644 --- a/config/environment.default.js +++ b/config/environment.default.js @@ -18,7 +18,10 @@ module.exports = { // Caching settings cache: { // NOTE: how long should objects be cached for by default - msToLive: 15 * 60 * 1000, // 15 minutes + msToLive: { + default: 15 * 60 * 1000, // 15 minutes + exportToZip: 5 * 1000 // 5 seconds + }, // msToLive: 1000, // 15 minutes control: 'max-age=60' // revalidate browser }, diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index fdf5b4eb97..563b965cb1 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -126,7 +126,7 @@ export abstract class BaseResponseParsingService { if (hasNoValue(co) || hasNoValue(co.self)) { throw new Error('The server returned an invalid object'); } - this.objectCache.add(co, this.EnvConfig.cache.msToLive, requestHref); + this.objectCache.add(co, this.EnvConfig.cache.msToLive.default, requestHref); } processPageInfo(payload: any): PageInfo { diff --git a/src/app/core/data/request.effects.ts b/src/app/core/data/request.effects.ts index 5fadd316f4..b7cd685690 100644 --- a/src/app/core/data/request.effects.ts +++ b/src/app/core/data/request.effects.ts @@ -23,7 +23,7 @@ import { catchError, flatMap, map, take, tap } from 'rxjs/operators'; export const addToResponseCacheAndCompleteAction = (request: RestRequest, responseCache: ResponseCacheService, envConfig: GlobalConfig) => (source: Observable): Observable => source.pipe( - tap((response: RestResponse) => responseCache.add(request.href, response, envConfig.cache.msToLive)), + tap((response: RestResponse) => responseCache.add(request.href, response, request.responseMsToLive ? request.responseMsToLive : envConfig.cache.msToLive.default)), map((response: RestResponse) => new RequestCompleteAction(request.uuid)) ); diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index b87f9cefc8..da2668fbbd 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -41,7 +41,8 @@ export abstract class RestRequest { public href: string, public method: RestRequestMethod = RestRequestMethod.Get, public body?: any, - public options?: HttpOptions + public options?: HttpOptions, + public responseMsToLive?: number ) { } @@ -55,9 +56,10 @@ export class GetRequest extends RestRequest { public uuid: string, public href: string, public body?: any, - public options?: HttpOptions + public options?: HttpOptions, + public responseMsToLive?: number ) { - super(uuid, href, RestRequestMethod.Get, body) + super(uuid, href, RestRequestMethod.Get, body, options, responseMsToLive) } } diff --git a/src/config/cache-config.interface.ts b/src/config/cache-config.interface.ts index f8a2c88640..2ca54e1401 100644 --- a/src/config/cache-config.interface.ts +++ b/src/config/cache-config.interface.ts @@ -1,6 +1,9 @@ import { Config } from './config.interface'; export interface CacheConfig extends Config { - msToLive: number, + msToLive: { + default: number; + exportToZip: number; + }, control: string } From 8ffbd132420b5f3adbf974d602f218572bfede09 Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 12 Oct 2018 11:55:52 +0200 Subject: [PATCH 43/69] finished tests and docs --- src/app/app.module.ts | 1 - src/app/core/auth/server-auth.service.ts | 1 - .../core/cache/object-cache.reducer.spec.ts | 17 ++- .../core/cache/object-cache.service.spec.ts | 61 +++++++- src/app/core/cache/object-cache.service.ts | 24 +-- .../core/cache/server-sync-buffer.actions.ts | 1 + .../cache/server-sync-buffer.effects.spec.ts | 139 ++++++++++++++++++ .../core/cache/server-sync-buffer.effects.ts | 21 ++- .../cache/server-sync-buffer.reducer.spec.ts | 93 ++++++++++++ src/app/core/data/data.service.spec.ts | 63 +++++++- src/app/core/data/data.service.ts | 19 ++- src/app/core/shared/dspace-object.model.ts | 2 +- .../auth-nav-menu/auth-nav-menu.component.ts | 5 +- src/app/shared/testing/utils.ts | 12 ++ 14 files changed, 426 insertions(+), 33 deletions(-) create mode 100644 src/app/core/cache/server-sync-buffer.effects.spec.ts create mode 100644 src/app/core/cache/server-sync-buffer.reducer.spec.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index a02997abdd..8469911406 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -46,7 +46,6 @@ export function getMetaReducers(config: GlobalConfig): Array { 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 perform the APPLY_PATCH action without affecting the previous state', () => { + const action = new ApplyPatchObjectCacheAction(selfLink1); + // testState has already been frozen above + objectCacheReducer(testState, action); + }); + }); diff --git a/src/app/core/cache/object-cache.service.spec.ts b/src/app/core/cache/object-cache.service.spec.ts index 38d51f09b3..5a1a3c5ac6 100644 --- a/src/app/core/cache/object-cache.service.spec.ts +++ b/src/app/core/cache/object-cache.service.spec.ts @@ -2,12 +2,20 @@ import { Store } from '@ngrx/store'; import { of as observableOf } from 'rxjs'; 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 { ResourceType } from '../shared/resource-type'; import { NormalizedItem } from './models/normalized-item.model'; import { first } from 'rxjs/operators'; 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', () => { let service: ObjectCacheService; @@ -16,18 +24,29 @@ describe('ObjectCacheService', () => { const selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; const timestamp = new Date().getTime(); const msToLive = 900000; - const objectToCache = { + let objectToCache = { self: selfLink, type: ResourceType.Item }; - const cacheEntry = { - data: objectToCache, - timeAdded: timestamp, - msToLive: msToLive - }; - const invalidCacheEntry = Object.assign({}, cacheEntry, { msToLive: -1 }); + let cacheEntry; + let invalidCacheEntry; + const operations = [{ op: 'replace', path: '/name', value: 'random string' } as Operation]; + + function init() { + objectToCache = { + self: selfLink, + type: ResourceType.Item + }; + cacheEntry = { + data: objectToCache, + timeAdded: timestamp, + msToLive: msToLive + }; + invalidCacheEntry = Object.assign({}, cacheEntry, { msToLive: -1 }) + } beforeEach(() => { + init(); store = new Store(undefined, undefined, undefined); spyOn(store, 'dispatch'); service = new ObjectCacheService(store); @@ -127,4 +146,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)); + }); + }); }); diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 909a160068..3ac644a045 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -18,10 +18,9 @@ import { coreSelector, CoreState } from '../core.reducers'; import { pathSelector } from '../shared/selectors'; import { NormalizedObjectFactory } from './models/normalized-object-factory'; import { NormalizedObject } from './models/normalized-object.model'; -import { applyPatch, applyReducer, Operation } from 'fast-json-patch'; +import { applyPatch, Operation } from 'fast-json-patch'; import { AddToSSBAction } from './server-sync-buffer.actions'; import { RestRequestMethod } from '../data/rest-request-method'; -import { ReplaceOperation } from 'fast-json-patch/lib/core'; function selfLinkFromUuidSelector(uuid: string): MemoizedSelector { return pathSelector(coreSelector, 'index', IndexName.OBJECT, uuid); @@ -92,9 +91,13 @@ export class ObjectCacheService { getBySelfLink(selfLink: string): Observable { return this.getEntry(selfLink).pipe( map((entry: ObjectCacheEntry) => { - 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 }); + 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) => { @@ -211,7 +214,6 @@ export class ObjectCacheService { } } - /** * Add operations to the existing list of operations for an ObjectCacheEntry * Makes sure the ServerSyncBuffer for this ObjectCacheEntry is updated @@ -220,9 +222,9 @@ export class ObjectCacheService { * @param {Operation[]} patch * list of operations to perform */ - public addPatch(uuid: string, patch: Operation[]) { - this.store.dispatch(new AddPatchObjectCacheAction(uuid, patch)); - this.store.dispatch(new AddToSSBAction(uuid, RestRequestMethod.PATCH)); + public addPatch(selfLink: string, patch: Operation[]) { + this.store.dispatch(new AddPatchObjectCacheAction(selfLink, patch)); + this.store.dispatch(new AddToSSBAction(selfLink, RestRequestMethod.PATCH)); } /** @@ -243,8 +245,8 @@ export class ObjectCacheService { * @param {string} uuid * the uuid of the ObjectCacheEntry */ - private applyPatchesToCachedObject(uuid: string) { - this.store.dispatch(new ApplyPatchObjectCacheAction(uuid)); + private applyPatchesToCachedObject(selfLink: string) { + this.store.dispatch(new ApplyPatchObjectCacheAction(selfLink)); } } diff --git a/src/app/core/cache/server-sync-buffer.actions.ts b/src/app/core/cache/server-sync-buffer.actions.ts index 69eb81b02f..638d837bea 100644 --- a/src/app/core/cache/server-sync-buffer.actions.ts +++ b/src/app/core/cache/server-sync-buffer.actions.ts @@ -64,6 +64,7 @@ export class EmptySSBAction implements Action { * * @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; diff --git a/src/app/core/cache/server-sync-buffer.effects.spec.ts b/src/app/core/cache/server-sync-buffer.effects.spec.ts new file mode 100644 index 0000000000..0a8d50107e --- /dev/null +++ b/src/app/core/cache/server-sync-buffer.effects.spec.ts @@ -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; + const testConfig = { + cache: + { + autoSync: + { + timePerMethod: {}, + defaultTime: 0 + } + } + }; + const selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; + let store; + + beforeEach(() => { + store = new MockStore({}); + 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); + }); + }); + }); +}); diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts index d5988cb839..db2263c52a 100644 --- a/src/app/core/cache/server-sync-buffer.effects.ts +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -25,6 +25,13 @@ 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), @@ -35,6 +42,12 @@ export class ServerSyncBufferEffects { }) ); + /** + * 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), @@ -55,7 +68,7 @@ export class ServerSyncBufferEffects { if (entry.method === RestRequestMethod.PATCH) { return this.applyPatch(entry.href); } else { - /* TODO other request stuff */ + /* TODO implement for other request method types */ } }); @@ -72,6 +85,12 @@ export class ServerSyncBufferEffects { }) ); + /** + * 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} ApplyPatchObjectCacheAction to be dispatched + */ private applyPatch(href: string): Observable { const patchObject = this.objectCache.getBySelfLink(href).pipe(first()); diff --git a/src/app/core/cache/server-sync-buffer.reducer.spec.ts b/src/app/core/cache/server-sync-buffer.reducer.spec.ts new file mode 100644 index 0000000000..666144104b --- /dev/null +++ b/src/app/core/cache/server-sync-buffer.reducer.spec.ts @@ -0,0 +1,93 @@ +import * as deepFreeze from 'deep-freeze'; + +import { objectCacheReducer } from './object-cache.reducer'; +import { + AddPatchObjectCacheAction, + AddToObjectCacheAction, ApplyPatchObjectCacheAction, + RemoveFromObjectCacheAction, + ResetObjectCacheTimestampsAction +} from './object-cache.actions'; +import { Operation } from '../../../../node_modules/fast-json-patch'; +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 + }) + ; + }) +}); diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index e0df5c871e..e60725c4c2 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -10,6 +10,9 @@ import { Observable } from 'rxjs'; import { FindAllOptions } from './request.models'; import { SortOptions, SortDirection } from '../cache/models/sort-options.model'; import { of as observableOf } from 'rxjs'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { Operation } from '../../../../node_modules/fast-json-patch'; +import { DSpaceObject } from '../shared/dspace-object.model'; const endpoint = 'https://rest.api/core'; @@ -42,7 +45,14 @@ describe('DataService', () => { const requestService = {} as RequestService; const halService = {} as HALEndpointService; const rdbService = {} as RemoteDataBuildService; - const objectCache = {} as ObjectCacheService; + const objectCache = { + addPatch: () => { + /* empty */ + }, + getBySelfLink: () => { + /* empty */ + } + } as any; const store = {} as Store; function initTestService(): TestService { @@ -52,7 +62,8 @@ describe('DataService', () => { rdbService, store, endpoint, - halService + halService, + objectCache ); } @@ -122,5 +133,53 @@ describe('DataService', () => { }); }) }); + describe('patch', () => { + let operations; + let selfLink; + beforeEach(() => { + operations = [{ op: 'replace', path: '/name', value: 'random string' } as Operation]; + selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; + spyOn(objectCache, 'addPatch'); + }); + + it('should call addPatch on the object cache with the right parameters', () => { + service.patch(selfLink, operations); + expect(objectCache.addPatch).toHaveBeenCalledWith(selfLink, operations); + }); + }); + + describe('update', () => { + let operations; + let selfLink; + let dso; + let dso2; + const name1 = 'random string'; + const name2 = 'another random string'; + beforeEach(() => { + operations = [{ op: 'replace', path: '/name', value: name2 } as Operation]; + selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; + + dso = new DSpaceObject(); + dso.self = selfLink; + dso.name = name1; + + dso2 = new DSpaceObject(); + dso2.self = selfLink; + dso2.name = name2; + + spyOn(objectCache, 'getBySelfLink').and.returnValue(dso); + spyOn(objectCache, 'addPatch'); + }); + + it('should call addPatch on the object cache with the right parameters when there are differences', () => { + service.update(dso2); + expect(objectCache.addPatch).toHaveBeenCalledWith(selfLink, operations); + }); + + it('should not call addPatch on the object cache with the right parameters when there are no differences', () => { + service.update(dso); + expect(objectCache.addPatch).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 127a8f8afd..d23d7e8064 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -1,6 +1,5 @@ -import { distinctUntilChanged, filter, take, first, map } from 'rxjs/operators'; -import { of as observableOf, Observable } from 'rxjs'; -import {mergeMap, first, take, distinctUntilChanged, map, filter} from 'rxjs/operators'; +import { distinctUntilChanged, filter, first, map, take } from 'rxjs/operators'; +import { Observable } from 'rxjs'; import { Store } from '@ngrx/store'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; @@ -95,14 +94,26 @@ export abstract class DataService return this.rdbService.buildSingle(href); } + /** + * Add a new patch to the object cache to a specified object + * @param {string} href The selflink of the object that will be patched + * @param {Operation[]} operations The patch operations to be performed + */ patch(href: string, operations: Operation[]) { this.objectCache.addPatch(href, operations); } + /** + * Add a new patch to the object cache + * The patch is derived from the differences between the given object and its version in the object cache + * @param {DSpaceObject} object The given object + */ update(object: DSpaceObject) { const oldVersion = this.objectCache.getBySelfLink(object.self); const operations = compare(oldVersion, object); - this.objectCache.addPatch(object.self, operations); + if (isNotEmpty(operations)) { + this.objectCache.addPatch(object.self, operations); + } } // TODO implement, after the structure of the REST server's POST response is finalized // create(dso: DSpaceObject): Observable> { diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 3a40d142aa..68338143ba 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -10,7 +10,7 @@ import { autoserialize } from 'cerialize'; /** * An abstract model class for a DSpaceObject. */ -export class DSpaceObject implements CacheableObject, ListableObject { +export class DSpaceObject implements CacheableObject, ListableObject { self: string; diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts index dd2e6202ce..fc85616de9 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts @@ -1,6 +1,6 @@ -import { of as observableOf, Observable , Subscription } from 'rxjs'; +import { Observable, of as observableOf, Subscription } from 'rxjs'; -import { map, filter } from 'rxjs/operators'; +import { filter, map } from 'rxjs/operators'; import { Component, OnInit } from '@angular/core'; import { RouterReducerState } from '@ngrx/router-store'; import { select, Store } from '@ngrx/store'; @@ -16,7 +16,6 @@ import { } from '../../core/auth/selectors'; import { EPerson } from '../../core/eperson/models/eperson.model'; import { AuthService, LOGIN_ROUTE, LOGOUT_ROUTE } from '../../core/auth/auth.service'; -import { Subscription } from 'rxjs'; @Component({ selector: 'ds-auth-nav-menu', diff --git a/src/app/shared/testing/utils.ts b/src/app/shared/testing/utils.ts index 9343ae50fd..8714358100 100644 --- a/src/app/shared/testing/utils.ts +++ b/src/app/shared/testing/utils.ts @@ -30,3 +30,15 @@ export const createTestComponent = (html: string, type: { new(...args: any[]) fixture.detectChanges(); return fixture as ComponentFixture; }; + +export function spyOnOperator(obj: any, prop: string): any { + const oldProp = obj[prop]; + Object.defineProperty(obj, prop, { + configurable: true, + enumerable: true, + value: oldProp, + writable: true + }); + + return spyOn(obj, prop); +} \ No newline at end of file From 12398d8658458aff8eb6014be426fade980bdd13 Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 12 Oct 2018 11:57:47 +0200 Subject: [PATCH 44/69] removed test form --- .../collection-page.component.html | 14 -------------- .../+collection-page/collection-page.component.ts | 15 ++------------- 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/src/app/+collection-page/collection-page.component.html b/src/app/+collection-page/collection-page.component.html index 90ae0cdf4c..a233163070 100644 --- a/src/app/+collection-page/collection-page.component.html +++ b/src/app/+collection-page/collection-page.component.html @@ -33,20 +33,6 @@ [content]="collection.license" [title]="'collection.page.license'"> -
-
-
- -
- -
- -
-
-
-
-
diff --git a/src/app/+collection-page/collection-page.component.ts b/src/app/+collection-page/collection-page.component.ts index 95aa8ddc4a..047613d6d1 100644 --- a/src/app/+collection-page/collection-page.component.ts +++ b/src/app/+collection-page/collection-page.component.ts @@ -39,8 +39,6 @@ export class CollectionPageComponent implements OnInit, OnDestroy { sortConfig: SortOptions; private subs: Subscription[] = []; private collectionId: string; - href: string; - newname: string; constructor( private collectionDataService: CollectionDataService, @@ -77,12 +75,8 @@ export class CollectionPageComponent implements OnInit, OnDestroy { pagination: pagination, sort: this.sortConfig }); - }) - ); - this.collectionRD$.pipe(first()).subscribe((crd) => { - this.href = crd.payload.self; - this.newname = crd.payload.name; - }); + })); + } updatePage(searchOptions) { @@ -115,9 +109,4 @@ export class CollectionPageComponent implements OnInit, OnDestroy { } }) } - - patchIt(): void { - console.log('patching it!', this.href, this.newname); - this.collectionDataService.patch(this.href, [{ op: 'replace', path: '/name', value: this.newname }]); - } } From 2330e96158dffe4dec6eafcb7703efc4ba3e4868 Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 17 Oct 2018 13:18:01 +0200 Subject: [PATCH 45/69] intermediate commit --- .../top-level-community-list.component.ts | 1 + .../search-service/search.service.spec.ts | 18 +-- .../search-service/search.service.ts | 54 ++++---- src/app/core/auth/auth-request.service.ts | 15 +-- .../auth-response-parsing.service.spec.ts | 2 +- .../auth/auth-response-parsing.service.ts | 2 +- src/app/core/browse/browse.service.spec.ts | 21 +-- src/app/core/browse/browse.service.ts | 35 ++--- .../builders/remote-data-build.service.ts | 62 ++++----- src/app/core/cache/response-cache.actions.ts | 72 ---------- .../core/cache/response-cache.effects.spec.ts | 38 ------ src/app/core/cache/response-cache.effects.ts | 27 ---- .../core/cache/response-cache.reducer.spec.ts | 124 ------------------ src/app/core/cache/response-cache.reducer.ts | 111 ---------------- .../core/cache/response-cache.service.spec.ts | 100 -------------- src/app/core/cache/response-cache.service.ts | 100 -------------- ...nse-cache.models.ts => response.models.ts} | 2 +- src/app/core/config/config.service.spec.ts | 12 -- src/app/core/config/config.service.ts | 14 +- .../submission-definitions-config.service.ts | 2 - .../config/submission-forms-config.service.ts | 2 - .../submission-sections-config.service.ts | 2 - src/app/core/core.effects.ts | 2 - src/app/core/core.module.ts | 2 - src/app/core/core.reducers.ts | 3 - ...e-entries-response-parsing.service.spec.ts | 2 +- ...browse-entries-response-parsing.service.ts | 2 +- ...wse-items-response-parsing-service.spec.ts | 2 +- .../browse-items-response-parsing-service.ts | 2 +- .../browse-response-parsing.service.spec.ts | 2 +- .../data/browse-response-parsing.service.ts | 2 +- src/app/core/data/collection-data.service.ts | 3 - src/app/core/data/comcol-data.service.spec.ts | 15 --- src/app/core/data/comcol-data.service.ts | 23 ++-- src/app/core/data/community-data.service.ts | 11 +- .../config-response-parsing.service.spec.ts | 2 +- .../data/config-response-parsing.service.ts | 2 +- src/app/core/data/data.service.spec.ts | 6 +- src/app/core/data/data.service.ts | 19 ++- .../data/debug-response-parsing.service.ts | 2 +- .../core/data/dso-response-parsing.service.ts | 8 +- .../core/data/dspace-object-data.service.ts | 8 +- .../endpoint-map-response-parsing.service.ts | 2 +- .../facet-config-response-parsing.service.ts | 2 +- ...acet-value-map-response-parsing.service.ts | 2 +- .../facet-value-response-parsing.service.ts | 2 +- src/app/core/data/item-data.service.spec.ts | 3 - src/app/core/data/item-data.service.ts | 6 +- .../data/metadataschema-parsing.service.ts | 2 +- src/app/core/data/parsing.service.ts | 2 +- ...tstreamformats-response-parsing.service.ts | 2 +- ...metadatafields-response-parsing.service.ts | 2 +- ...etadataschemas-response-parsing.service.ts | 2 +- src/app/core/data/request.actions.ts | 38 +++++- src/app/core/data/request.effects.ts | 43 ++++-- src/app/core/data/request.models.ts | 13 +- src/app/core/data/request.reducer.ts | 30 ++++- src/app/core/data/request.service.spec.ts | 5 - src/app/core/data/request.service.ts | 87 ++++++++---- .../data/search-response-parsing.service.ts | 2 +- src/app/core/integration/authority.service.ts | 2 - ...tegration-response-parsing.service.spec.ts | 2 +- .../integration-response-parsing.service.ts | 2 +- .../integration/integration.service.spec.ts | 15 +-- .../core/integration/integration.service.ts | 13 +- .../core/metadata/metadata.service.spec.ts | 8 +- .../core/registry/registry.service.spec.ts | 6 +- src/app/core/registry/registry.service.ts | 66 ++++------ .../core/shared/hal-endpoint.service.spec.ts | 18 --- src/app/core/shared/hal-endpoint.service.ts | 44 +++++-- src/app/core/shared/operators.spec.ts | 50 +------ src/app/core/shared/operators.ts | 29 ++-- .../mocks/mock-remote-data-build.service.ts | 3 +- .../mocks/mock-response-cache.service.ts | 16 --- src/server.ts | 1 + 75 files changed, 387 insertions(+), 1067 deletions(-) delete mode 100644 src/app/core/cache/response-cache.actions.ts delete mode 100644 src/app/core/cache/response-cache.effects.spec.ts delete mode 100644 src/app/core/cache/response-cache.effects.ts delete mode 100644 src/app/core/cache/response-cache.reducer.spec.ts delete mode 100644 src/app/core/cache/response-cache.reducer.ts delete mode 100644 src/app/core/cache/response-cache.service.spec.ts delete mode 100644 src/app/core/cache/response-cache.service.ts rename src/app/core/cache/{response-cache.models.ts => response.models.ts} (99%) diff --git a/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts b/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts index 8e8c83ce5b..3fdb7e48a2 100644 --- a/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts +++ b/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts @@ -17,6 +17,7 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c changeDetection: ChangeDetectionStrategy.OnPush, animations: [fadeInOut] }) + export class TopLevelCommunityListComponent { communitiesRDObs: Observable>>; config: PaginationComponentOptions; diff --git a/src/app/+search-page/search-service/search.service.spec.ts b/src/app/+search-page/search-service/search.service.spec.ts index 6bfc9200ec..028a3fa446 100644 --- a/src/app/+search-page/search-service/search.service.spec.ts +++ b/src/app/+search-page/search-service/search.service.spec.ts @@ -8,21 +8,18 @@ import { SearchService } from './search.service'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { ActivatedRoute, Router, UrlTree } from '@angular/router'; import { RequestService } from '../../core/data/request.service'; -import { ResponseCacheService } from '../../core/cache/response-cache.service'; import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; import { RouterStub } from '../../shared/testing/router-stub'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { Observable, combineLatest as observableCombineLatest } from 'rxjs'; import { PaginatedSearchOptions } from '../paginated-search-options.model'; import { RemoteData } from '../../core/data/remote-data'; -import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; import { RequestEntry } from '../../core/data/request.reducer'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; -import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service'; import { FacetConfigSuccessResponse, SearchSuccessResponse -} from '../../core/cache/response-cache.models'; +} from '../../core/cache/response.models'; import { SearchQueryResponse } from './search-query-response.model'; import { SearchFilterConfig } from './search-filter-config.model'; import { CommunityDataService } from '../../core/data/community-data.service'; @@ -54,7 +51,6 @@ describe('SearchService', () => { providers: [ { provide: Router, useValue: router }, { provide: ActivatedRoute, useValue: route }, - { provide: ResponseCacheService, useValue: getMockResponseCacheService() }, { provide: RequestService, useValue: getMockRequestService() }, { provide: RemoteDataBuildService, useValue: {} }, { provide: HALEndpointService, useValue: {} }, @@ -86,9 +82,8 @@ describe('SearchService', () => { }; const remoteDataBuildService = { - toRemoteDataObservable: (requestEntryObs: Observable, responseCacheObs: Observable, payloadObs: Observable) => { - return observableCombineLatest(requestEntryObs, - responseCacheObs, payloadObs).pipe( + toRemoteDataObservable: (requestEntryObs: Observable, payloadObs: Observable) => { + return observableCombineLatest(requestEntryObs, payloadObs).pipe( map(([req, res, pay]) => { return { req, res, pay }; }) @@ -113,7 +108,6 @@ describe('SearchService', () => { providers: [ { provide: Router, useValue: router }, { provide: ActivatedRoute, useValue: route }, - { provide: ResponseCacheService, useValue: getMockResponseCacheService() }, { provide: RequestService, useValue: getMockRequestService() }, { provide: RemoteDataBuildService, useValue: remoteDataBuildService }, { provide: HALEndpointService, useValue: halService }, @@ -162,10 +156,8 @@ describe('SearchService', () => { const searchOptions = new PaginatedSearchOptions({}); const queryResponse = Object.assign(new SearchQueryResponse(), { objects: [] }); const response = new SearchSuccessResponse(queryResponse, '200'); - const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response }); beforeEach(() => { spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint)); - (searchService as any).responseCache.get.and.returnValue(observableOf(responseEntry)); /* tslint:disable:no-empty */ searchService.search(searchOptions).subscribe((t) => { }); // subscribe to make sure all methods are called @@ -192,10 +184,8 @@ describe('SearchService', () => { const endPoint = 'http://endpoint.com/test/config'; const filterConfig = [new SearchFilterConfig()]; const response = new FacetConfigSuccessResponse(filterConfig, '200'); - const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response }); beforeEach(() => { spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint)); - (searchService as any).responseCache.get.and.returnValue(observableOf(responseEntry)); /* tslint:disable:no-empty */ searchService.getConfig(null).subscribe((t) => { }); // subscribe to make sure all methods are called @@ -224,10 +214,8 @@ describe('SearchService', () => { const requestUrl = endPoint + '?scope=' + scope; const filterConfig = [new SearchFilterConfig()]; const response = new FacetConfigSuccessResponse(filterConfig, '200'); - const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response }); beforeEach(() => { spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint)); - (searchService as any).responseCache.get.and.returnValue(observableOf(responseEntry)); /* tslint:disable:no-empty */ searchService.getConfig(scope).subscribe((t) => { }); // subscribe to make sure all methods are called diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index 1503440eb0..c628fd350e 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -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 { ActivatedRoute, @@ -7,15 +7,13 @@ import { Router, UrlSegmentGroup } from '@angular/router'; -import { flatMap, map, switchMap } from 'rxjs/operators'; +import { filter, flatMap, map, switchMap } from 'rxjs/operators'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { FacetConfigSuccessResponse, FacetValueSuccessResponse, SearchSuccessResponse -} from '../../core/cache/response-cache.models'; -import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; -import { ResponseCacheService } from '../../core/cache/response-cache.service'; +} from '../../core/cache/response.models'; import { PaginatedList } from '../../core/data/paginated-list'; import { ResponseParsingService } from '../../core/data/parsing.service'; 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 { GenericConstructor } from '../../core/shared/generic-constructor'; 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 { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { NormalizedSearchResult } from '../normalized-search-result.model'; @@ -45,6 +47,7 @@ import { CommunityDataService } from '../../core/data/community-data.service'; import { ViewMode } from '../../core/shared/view-mode.model'; import { ResourceType } from '../../core/shared/resource-type'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; +import { RequestEntry } from '../../core/data/request.reducer'; /** * Service that performs all general actions that have to do with the search page @@ -68,7 +71,6 @@ export class SearchService implements OnDestroy { constructor(private router: Router, private route: ActivatedRoute, - protected responseCache: ResponseCacheService, protected requestService: RequestService, private rdb: RemoteDataBuildService, private halService: HALEndpointService, @@ -101,13 +103,9 @@ export class SearchService implements OnDestroy { flatMap((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 - const sqrObs: Observable = responseCacheObs.pipe( - map((entry: ResponseCacheEntry) => entry.response), + const sqrObs: Observable = requestEntryObs.pipe( + getResponseFromEntry(), map((response: SearchSuccessResponse) => response.results) ); @@ -139,8 +137,8 @@ export class SearchService implements OnDestroy { }) ); - const pageInfoObs: Observable = responseCacheObs.pipe( - map((entry: ResponseCacheEntry) => entry.response), + const pageInfoObs: Observable = requestEntryObs.pipe( + getResponseFromEntry(), 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); } /** @@ -185,18 +183,14 @@ export class SearchService implements OnDestroy { flatMap((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 - const facetConfigObs: Observable = responseCacheObs.pipe( - map((entry: ResponseCacheEntry) => entry.response), + const facetConfigObs: Observable = requestEntryObs.pipe( + getResponseFromEntry(), map((response: FacetConfigSuccessResponse) => response.results.map((result: any) => Object.assign(new SearchFilterConfig(), result))) ); - return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, facetConfigObs); + return this.rdb.toRemoteDataObservable(requestEntryObs, facetConfigObs); } /** @@ -232,18 +226,14 @@ export class SearchService implements OnDestroy { flatMap((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 - const facetValueObs: Observable = responseCacheObs.pipe( - map((entry: ResponseCacheEntry) => entry.response), + const facetValueObs: Observable = requestEntryObs.pipe( + getResponseFromEntry(), map((response: FacetValueSuccessResponse) => response.results) ); - const pageInfoObs: Observable = responseCacheObs.pipe( - map((entry: ResponseCacheEntry) => entry.response), + const pageInfoObs: Observable = requestEntryObs.pipe( + getResponseFromEntry(), 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); } /** diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index 7cb5fae7e4..0752149eae 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -2,15 +2,15 @@ import { Observable, of as observableOf, throwError as observableThrowError } fr import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators'; import { Inject, Injectable } from '@angular/core'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { RequestService } from '../data/request.service'; import { GLOBAL_CONFIG } from '../../../config'; import { GlobalConfig } from '../../../config/global-config.interface'; import { isNotEmpty } from '../../shared/empty.util'; import { AuthGetRequest, AuthPostRequest, PostRequest, RestRequest } from '../data/request.models'; -import { ResponseCacheEntry } from '../cache/response-cache.reducer'; -import { AuthStatusResponse, ErrorResponse } from '../cache/response-cache.models'; +import { AuthStatusResponse, ErrorResponse } from '../cache/response.models'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { RequestEntry } from '../data/request.reducer'; +import { getResponseFromEntry } from '../shared/operators'; @Injectable() export class AuthRequestService { @@ -19,18 +19,17 @@ export class AuthRequestService { constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, protected halService: HALEndpointService, - protected responseCache: ResponseCacheService, protected requestService: RequestService) { } protected fetchRequest(request: RestRequest): Observable { - return this.responseCache.get(request.href).pipe( - map((entry: ResponseCacheEntry) => entry.response), + return this.requestService.getByHref(request.href).pipe( + getResponseFromEntry(), // TODO to review when https://github.com/DSpace/dspace-angular/issues/217 will be fixed - tap(() => this.responseCache.remove(request.href)), + // tap(() => this.responseCache.remove(request.href)), mergeMap((response) => { if (response.isSuccessful && isNotEmpty(response)) { - return observableOf((response as AuthStatusResponse).response); + return observableOf((response as AuthStatusResponse).response); } else if (!response.isSuccessful) { return observableThrowError(new Error((response as ErrorResponse).errorMessage)); } diff --git a/src/app/core/auth/auth-response-parsing.service.spec.ts b/src/app/core/auth/auth-response-parsing.service.spec.ts index 138d0f1be3..e23e3ac56b 100644 --- a/src/app/core/auth/auth-response-parsing.service.spec.ts +++ b/src/app/core/auth/auth-response-parsing.service.spec.ts @@ -1,4 +1,4 @@ -import { AuthStatusResponse } from '../cache/response-cache.models'; +import { AuthStatusResponse } from '../cache/response.models'; import { ObjectCacheService } from '../cache/object-cache.service'; import { GlobalConfig } from '../../../config/global-config.interface'; diff --git a/src/app/core/auth/auth-response-parsing.service.ts b/src/app/core/auth/auth-response-parsing.service.ts index 8efa36f9e2..65d093de61 100644 --- a/src/app/core/auth/auth-response-parsing.service.ts +++ b/src/app/core/auth/auth-response-parsing.service.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@angular/core'; import { AuthObjectFactory } from './auth-object-factory'; 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 { GLOBAL_CONFIG } from '../../../config'; import { GlobalConfig } from '../../../config/global-config.interface'; diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index d43a26ed4b..4465eae1ee 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -2,10 +2,8 @@ import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.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 { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from '../data/request.models'; import { RequestService } from '../data/request.service'; import { BrowseDefinition } from '../shared/browse-definition.model'; @@ -14,7 +12,6 @@ import { BrowseService } from './browse.service'; describe('BrowseService', () => { let scheduler: TestScheduler; let service: BrowseService; - let responseCache: ResponseCacheService; let requestService: RequestService; let rdbService: RemoteDataBuildService; @@ -79,22 +76,10 @@ describe('BrowseService', () => { }) ]; - function initMockResponseCacheService(isSuccessful: boolean) { - const rcs = getMockResponseCacheService(); - (rcs.get as any).and.returnValue(cold('b-', { - b: { - response: { - isSuccessful, - payload: browseDefinitions, - } - } - })); - return rcs; - } + function initTestService() { return new BrowseService( - responseCache, requestService, halService, rdbService @@ -108,7 +93,6 @@ describe('BrowseService', () => { describe('getBrowseDefinitions', () => { beforeEach(() => { - responseCache = initMockResponseCacheService(true); requestService = getMockRequestService(); rdbService = getMockRemoteDataBuildService(); service = initTestService(); @@ -147,7 +131,6 @@ describe('BrowseService', () => { const mockAuthorName = 'Donald Smith'; beforeEach(() => { - responseCache = initMockResponseCacheService(true); requestService = getMockRequestService(); rdbService = getMockRemoteDataBuildService(); service = initTestService(); @@ -221,7 +204,6 @@ describe('BrowseService', () => { describe('if getBrowseDefinitions fires', () => { beforeEach(() => { - responseCache = initMockResponseCacheService(true); requestService = getMockRequestService(); rdbService = getMockRemoteDataBuildService(); service = initTestService(); @@ -277,7 +259,6 @@ describe('BrowseService', () => { describe('if getBrowseDefinitions doesn\'t fire', () => { it('should return undefined', () => { - responseCache = initMockResponseCacheService(true); requestService = getMockRequestService(); rdbService = getMockRemoteDataBuildService(); service = initTestService(); diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index ddce277e7e..c1c893ec27 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, map, startWith } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, startWith, tap } from 'rxjs/operators'; import { - ensureArrayHasValue, + ensureArrayHasValue, hasValue, hasValueOperator, isEmpty, isNotEmpty, @@ -11,16 +11,13 @@ import { import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { SortOptions } from '../cache/models/sort-options.model'; -import { GenericSuccessResponse } from '../cache/response-cache.models'; -import { ResponseCacheEntry } from '../cache/response-cache.reducer'; -import { ResponseCacheService } from '../cache/response-cache.service'; +import { GenericSuccessResponse } from '../cache/response.models'; import { PaginatedList } from '../data/paginated-list'; import { RemoteData } from '../data/remote-data'; import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest, - GetRequest, RestRequest } from '../data/request.models'; import { RequestService } from '../data/request.service'; @@ -29,14 +26,15 @@ import { BrowseEntry } from '../shared/browse-entry.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { configureRequest, - filterSuccessfulResponses, getBrowseDefinitionLinks, + filterSuccessfulResponses, + getBrowseDefinitionLinks, getRemoteDataPayload, - getRequestFromSelflink, - getResponseFromSelflink + getRequestFromSelflink } from '../shared/operators'; import { URLCombiner } from '../url-combiner/url-combiner'; import { Item } from '../shared/item.model'; import { DSpaceObject } from '../shared/dspace-object.model'; +import { RequestEntry } from '../data/request.reducer'; @Injectable() export class BrowseService { @@ -56,7 +54,6 @@ export class BrowseService { } constructor( - protected responseCache: ResponseCacheService, protected requestService: RequestService, protected halService: HALEndpointService, private rdb: RemoteDataBuildService, @@ -73,10 +70,8 @@ export class BrowseService { const href$ = request$.pipe(map((request: RestRequest) => request.href)); const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); - const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache)); - const payload$ = responseCache$.pipe( + const payload$ = requestEntry$.pipe( filterSuccessfulResponses(), - map((entry: ResponseCacheEntry) => entry.response), map((response: GenericSuccessResponse) => response.payload), ensureArrayHasValue(), map((definitions: BrowseDefinition[]) => definitions @@ -84,7 +79,7 @@ export class BrowseService { distinctUntilChanged() ); - return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$); + return this.rdb.toRemoteDataObservable(requestEntry$, payload$); } getBrowseEntriesFor(definitionID: string, options: { @@ -118,11 +113,9 @@ export class BrowseService { const href$ = request$.pipe(map((request: RestRequest) => request.href)); const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); - const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache)); - const payload$ = responseCache$.pipe( + const payload$ = requestEntry$.pipe( filterSuccessfulResponses(), - map((entry: ResponseCacheEntry) => entry.response), map((response: GenericSuccessResponse) => new PaginatedList(response.pageInfo, response.payload)), map((list: PaginatedList) => Object.assign(list, { page: list.page ? list.page.map((entry: BrowseEntry) => Object.assign(new BrowseEntry(), entry)) : list.page @@ -130,7 +123,7 @@ export class BrowseService { distinctUntilChanged() ); - return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$); + return this.rdb.toRemoteDataObservable(requestEntry$, payload$); } /** @@ -175,11 +168,9 @@ export class BrowseService { const href$ = request$.pipe(map((request: RestRequest) => request.href)); const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); - const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache)); - const payload$ = responseCache$.pipe( + const payload$ = requestEntry$.pipe( filterSuccessfulResponses(), - map((entry: ResponseCacheEntry) => entry.response), map((response: GenericSuccessResponse) => new PaginatedList(response.pageInfo, response.payload)), map((list: PaginatedList) => Object.assign(list, { page: list.page ? list.page.map((item: DSpaceObject) => Object.assign(new Item(), item)) : list.page @@ -187,7 +178,7 @@ export class BrowseService { distinctUntilChanged() ); - return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$); + return this.rdb.toRemoteDataObservable(requestEntry$, payload$); } getBrowseURLFor(metadatumKey: string, linkPath: string): Observable { diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index fe7f56220f..27b5ddf50d 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -1,11 +1,11 @@ import { combineLatest as observableCombineLatest, - of as observableOf, Observable, + of as observableOf, race as observableRace } from 'rxjs'; import { Injectable } from '@angular/core'; -import { distinctUntilChanged, flatMap, map, startWith } from 'rxjs/operators'; +import { distinctUntilChanged, flatMap, map, startWith, switchMap, tap } from 'rxjs/operators'; import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util'; import { PaginatedList } from '../../data/paginated-list'; import { RemoteData } from '../../data/remote-data'; @@ -16,22 +16,18 @@ import { RequestService } from '../../data/request.service'; import { NormalizedObject } from '../models/normalized-object.model'; import { ObjectCacheService } from '../object-cache.service'; -import { DSOSuccessResponse, ErrorResponse } from '../response-cache.models'; -import { ResponseCacheEntry } from '../response-cache.reducer'; -import { ResponseCacheService } from '../response-cache.service'; +import { DSOSuccessResponse, ErrorResponse } from '../response.models'; import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators'; import { PageInfo } from '../../shared/page-info.model'; import { + filterSuccessfulResponses, getRequestFromSelflink, - getResourceLinksFromResponse, - getResponseFromSelflink, - filterSuccessfulResponses + getResourceLinksFromResponse } from '../../shared/operators'; @Injectable() export class RemoteDataBuildService { constructor(protected objectCache: ObjectCacheService, - protected responseCache: ResponseCacheService, protected requestService: RequestService) { } @@ -39,19 +35,16 @@ export class RemoteDataBuildService { if (typeof href$ === 'string') { href$ = observableOf(href$); } - const requestHref$ = href$.pipe(flatMap((href: string) => - this.objectCache.getRequestHrefBySelfLink(href))); + const requestHref$ = href$.pipe( + switchMap((href: string) => + this.objectCache.getRequestHrefBySelfLink(href)), + ); const requestEntry$ = observableRace( href$.pipe(getRequestFromSelflink(this.requestService)), requestHref$.pipe(getRequestFromSelflink(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. const payload$ = observableCombineLatest( @@ -59,7 +52,7 @@ export class RemoteDataBuildService { flatMap((href: string) => this.objectCache.getBySelfLink(href)), startWith(undefined) ), - responseCache$.pipe( + requestEntry$.pipe( getResourceLinksFromResponse(), flatMap((resourceSelfLinks: string[]) => { if (isNotEmpty(resourceSelfLinks)) { @@ -86,21 +79,21 @@ export class RemoteDataBuildService { startWith(undefined), distinctUntilChanged() ); - return this.toRemoteDataObservable(requestEntry$, responseCache$, payload$); + return this.toRemoteDataObservable(requestEntry$, payload$); } - toRemoteDataObservable(requestEntry$: Observable, responseCache$: Observable, payload$: Observable) { - return observableCombineLatest(requestEntry$, responseCache$.pipe(startWith(undefined)), payload$).pipe( - map(([reqEntry, resEntry, payload]) => { + toRemoteDataObservable(requestEntry$: Observable, payload$: Observable) { + return observableCombineLatest(requestEntry$, requestEntry$.pipe(startWith(undefined)), payload$).pipe( + map(([reqEntry, payload]) => { const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true; const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false; let isSuccessful: boolean; let error: RemoteDataError; - if (hasValue(resEntry) && hasValue(resEntry.response)) { - isSuccessful = resEntry.response.isSuccessful; - const errorMessage = isSuccessful === false ? (resEntry.response as ErrorResponse).errorMessage : undefined; + if (hasValue(reqEntry) && hasValue(reqEntry.response)) { + isSuccessful = reqEntry.response.isSuccessful; + const errorMessage = isSuccessful === false ? (reqEntry.response as ErrorResponse).errorMessage : undefined; if (hasValue(errorMessage)) { - error = new RemoteDataError(resEntry.response.statusCode, errorMessage); + error = new RemoteDataError(reqEntry.response.statusCode, errorMessage); } } return new RemoteData( @@ -120,9 +113,7 @@ export class RemoteDataBuildService { } const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); - const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache)); - - const tDomainList$ = responseCache$.pipe( + const tDomainList$ = requestEntry$.pipe( getResourceLinksFromResponse(), flatMap((resourceUUIDs: string[]) => { return this.objectCache.getList(resourceUUIDs).pipe( @@ -135,12 +126,12 @@ export class RemoteDataBuildService { startWith([]), distinctUntilChanged() ); - - const pageInfo$ = responseCache$.pipe( + // tDomainList$.subscribe((t) => {console.log('domainlist', t)}); + const pageInfo$ = requestEntry$.pipe( filterSuccessfulResponses(), - map((entry: ResponseCacheEntry) => { - if (hasValue((entry.response as DSOSuccessResponse).pageInfo)) { - const resPageInfo = (entry.response as DSOSuccessResponse).pageInfo; + map((response: DSOSuccessResponse) => { + if (hasValue((response as DSOSuccessResponse).pageInfo)) { + const resPageInfo = (response as DSOSuccessResponse).pageInfo; if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) { return Object.assign({}, resPageInfo, { currentPage: resPageInfo.currentPage + 1 }); } else { @@ -156,7 +147,7 @@ export class RemoteDataBuildService { }) ); - return this.toRemoteDataObservable(requestEntry$, responseCache$, payload$); + return this.toRemoteDataObservable(requestEntry$, payload$); } build(normalized: TNormalized): TDomain { @@ -204,8 +195,9 @@ export class RemoteDataBuildService { } } }); - const domainModel = getMapsTo(normalized.constructor); + // console.log('domain model', normalized); + return Object.assign(new domainModel(), normalized, links); } diff --git a/src/app/core/cache/response-cache.actions.ts b/src/app/core/cache/response-cache.actions.ts deleted file mode 100644 index 0389067690..0000000000 --- a/src/app/core/cache/response-cache.actions.ts +++ /dev/null @@ -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; diff --git a/src/app/core/cache/response-cache.effects.spec.ts b/src/app/core/cache/response-cache.effects.spec.ts deleted file mode 100644 index 950049bfca..0000000000 --- a/src/app/core/cache/response-cache.effects.spec.ts +++ /dev/null @@ -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; - 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); - }); - }); -}); diff --git a/src/app/core/cache/response-cache.effects.ts b/src/app/core/cache/response-cache.effects.ts deleted file mode 100644 index 5a1e53e20c..0000000000 --- a/src/app/core/cache/response-cache.effects.ts +++ /dev/null @@ -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,) { - } - -} diff --git a/src/app/core/cache/response-cache.reducer.spec.ts b/src/app/core/cache/response-cache.reducer.spec.ts deleted file mode 100644 index 9037b20030..0000000000 --- a/src/app/core/cache/response-cache.reducer.spec.ts +++ /dev/null @@ -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); - }); - }); - - }); -}); diff --git a/src/app/core/cache/response-cache.reducer.ts b/src/app/core/cache/response-cache.reducer.ts deleted file mode 100644 index 73c680c1f5..0000000000 --- a/src/app/core/cache/response-cache.reducer.ts +++ /dev/null @@ -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; -} diff --git a/src/app/core/cache/response-cache.service.spec.ts b/src/app/core/cache/response-cache.service.spec.ts deleted file mode 100644 index 4fcd926343..0000000000 --- a/src/app/core/cache/response-cache.service.spec.ts +++ /dev/null @@ -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; - - 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(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); - }); - }); -}); diff --git a/src/app/core/cache/response-cache.service.ts b/src/app/core/cache/response-cache.service.ts deleted file mode 100644 index 973d3620ff..0000000000 --- a/src/app/core/cache/response-cache.service.ts +++ /dev/null @@ -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 { - return pathSelector(coreSelector, 'cache/response', key); -} - -/** - * A service to interact with the response cache - */ -@Injectable() -export class ResponseCacheService { - constructor( - private store: Store - ) { - } - - add(key: string, response: RestResponse, msToLive: number): Observable { - 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 - * an observable of the ResponseCacheEntry with the specified key - */ - get(key: string): Observable { - 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; - } - } - -} diff --git a/src/app/core/cache/response-cache.models.ts b/src/app/core/cache/response.models.ts similarity index 99% rename from src/app/core/cache/response-cache.models.ts rename to src/app/core/cache/response.models.ts index 9566dcdc3c..fcec635655 100644 --- a/src/app/core/cache/response-cache.models.ts +++ b/src/app/core/cache/response.models.ts @@ -13,7 +13,7 @@ import { AuthStatus } from '../auth/models/auth-status.model'; /* tslint:disable:max-classes-per-file */ export class RestResponse { - public toCache = true; + public timeAdded: number; constructor( public isSuccessful: boolean, diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts index 46c8fd1859..8e9f7db27a 100644 --- a/src/app/core/config/config.service.spec.ts +++ b/src/app/core/config/config.service.spec.ts @@ -1,7 +1,6 @@ import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { ConfigService } from './config.service'; import { RequestService } from '../data/request.service'; import { ConfigRequest, FindAllOptions } from '../data/request.models'; @@ -16,7 +15,6 @@ class TestService extends ConfigService { protected browseEndpoint = BROWSE; constructor( - protected responseCache: ResponseCacheService, protected requestService: RequestService, protected halService: HALEndpointService) { super(); @@ -26,7 +24,6 @@ class TestService extends ConfigService { describe('ConfigService', () => { let scheduler: TestScheduler; let service: TestService; - let responseCache: ResponseCacheService; let requestService: RequestService; let halService: any; @@ -39,17 +36,9 @@ describe('ConfigService', () => { const scopedEndpoint = `${serviceEndpoint}/${scopeName}`; const searchEndpoint = `${serviceEndpoint}/${BROWSE}?uuid=${scopeID}`; - function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService { - return jasmine.createSpyObj('responseCache', { - get: cold('c-', { - c: { response: { isSuccessful } } - }) - }); - } function initTestService(): TestService { return new TestService( - responseCache, requestService, halService ); @@ -57,7 +46,6 @@ describe('ConfigService', () => { beforeEach(() => { scheduler = getTestScheduler(); - responseCache = initMockResponseCacheService(true); requestService = getMockRequestService(); halService = new HALEndpointServiceStub(configEndpoint); service = initTestService(); diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index 872bc57c2b..c6c2e2e7d2 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -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 { RequestService } from '../data/request.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; -import { ConfigSuccessResponse } from '../cache/response-cache.models'; +import { ConfigSuccessResponse } from '../cache/response.models'; import { ConfigRequest, FindAllOptions, RestRequest } from '../data/request.models'; -import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ConfigData } from './config-data'; +import { RequestEntry } from '../data/request.reducer'; +import { getResponseFromEntry } from '../shared/operators'; export abstract class ConfigService { protected request: ConfigRequest; - protected abstract responseCache: ResponseCacheService; protected abstract requestService: RequestService; protected abstract linkPath: string; protected abstract browseEndpoint: string; protected abstract halService: HALEndpointService; protected getConfig(request: RestRequest): Observable { - 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( filter((response) => !response.isSuccessful), mergeMap(() => observableThrowError(new Error(`Couldn't retrieve the config`))) @@ -94,7 +95,6 @@ export abstract class ConfigService { } public getConfigBySearch(options: FindAllOptions = {}): Observable { - console.log(this.halService.getEndpoint(this.linkPath)); return this.halService.getEndpoint(this.linkPath).pipe( map((endpoint: string) => this.getConfigSearchHref(endpoint, options)), filter((href: string) => isNotEmpty(href)), diff --git a/src/app/core/config/submission-definitions-config.service.ts b/src/app/core/config/submission-definitions-config.service.ts index 6cbe0c55b5..b7b0873c21 100644 --- a/src/app/core/config/submission-definitions-config.service.ts +++ b/src/app/core/config/submission-definitions-config.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; import { ConfigService } from './config.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; @@ -11,7 +10,6 @@ export class SubmissionDefinitionsConfigService extends ConfigService { protected browseEndpoint = 'search/findByCollection'; constructor( - protected responseCache: ResponseCacheService, protected requestService: RequestService, protected halService: HALEndpointService) { super(); diff --git a/src/app/core/config/submission-forms-config.service.ts b/src/app/core/config/submission-forms-config.service.ts index 27eac78218..b688859ec9 100644 --- a/src/app/core/config/submission-forms-config.service.ts +++ b/src/app/core/config/submission-forms-config.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; import { ConfigService } from './config.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; @@ -11,7 +10,6 @@ export class SubmissionFormsConfigService extends ConfigService { protected browseEndpoint = ''; constructor( - protected responseCache: ResponseCacheService, protected requestService: RequestService, protected halService: HALEndpointService) { super(); diff --git a/src/app/core/config/submission-sections-config.service.ts b/src/app/core/config/submission-sections-config.service.ts index 6d4d2ca825..c8bbc0dd97 100644 --- a/src/app/core/config/submission-sections-config.service.ts +++ b/src/app/core/config/submission-sections-config.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; import { ConfigService } from './config.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; @@ -11,7 +10,6 @@ export class SubmissionSectionsConfigService extends ConfigService { protected browseEndpoint = ''; constructor( - protected responseCache: ResponseCacheService, protected requestService: RequestService, protected halService: HALEndpointService) { super(); diff --git a/src/app/core/core.effects.ts b/src/app/core/core.effects.ts index 881f01aed0..c9a352c545 100644 --- a/src/app/core/core.effects.ts +++ b/src/app/core/core.effects.ts @@ -1,13 +1,11 @@ import { ObjectCacheEffects } from './cache/object-cache.effects'; -import { ResponseCacheEffects } from './cache/response-cache.effects'; import { UUIDIndexEffects } from './index/index.effects'; import { RequestEffects } from './data/request.effects'; import { AuthEffects } from './auth/auth.effects'; import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects'; export const coreEffects = [ - ResponseCacheEffects, RequestEffects, ObjectCacheEffects, UUIDIndexEffects, diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 73e97c7933..dcbdbd0049 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -32,7 +32,6 @@ import { ObjectCacheService } from './cache/object-cache.service'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; import { RemoteDataBuildService } from './cache/builders/remote-data-build.service'; import { RequestService } from './data/request.service'; -import { ResponseCacheService } from './cache/response-cache.service'; import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service'; import { ServerResponseService } from '../shared/services/server-response.service'; import { NativeWindowFactory, NativeWindowService } from '../shared/services/window.service'; @@ -102,7 +101,6 @@ const PROVIDERS = [ RegistryService, RemoteDataBuildService, RequestService, - ResponseCacheService, EndpointMapResponseParsingService, FacetValueResponseParsingService, FacetValueMapResponseParsingService, diff --git a/src/app/core/core.reducers.ts b/src/app/core/core.reducers.ts index 6905eb1300..1843e10671 100644 --- a/src/app/core/core.reducers.ts +++ b/src/app/core/core.reducers.ts @@ -1,6 +1,5 @@ import { ActionReducerMap, createFeatureSelector } from '@ngrx/store'; -import { responseCacheReducer, ResponseCacheState } from './cache/response-cache.reducer'; import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer'; import { indexReducer, IndexState } from './index/index.reducer'; import { requestReducer, RequestState } from './data/request.reducer'; @@ -9,7 +8,6 @@ import { serverSyncBufferReducer, ServerSyncBufferState } from './cache/server-s export interface CoreState { 'cache/object': ObjectCacheState, - 'cache/response': ResponseCacheState, 'cache/syncbuffer': ServerSyncBufferState, 'data/request': RequestState, 'index': IndexState, @@ -18,7 +16,6 @@ export interface CoreState { export const coreReducers: ActionReducerMap = { 'cache/object': objectCacheReducer, - 'cache/response': responseCacheReducer, 'cache/syncbuffer': serverSyncBufferReducer, 'data/request': requestReducer, 'index': indexReducer, diff --git a/src/app/core/data/browse-entries-response-parsing.service.spec.ts b/src/app/core/data/browse-entries-response-parsing.service.spec.ts index dd04e4f2f5..a61da7aa95 100644 --- a/src/app/core/data/browse-entries-response-parsing.service.spec.ts +++ b/src/app/core/data/browse-entries-response-parsing.service.spec.ts @@ -1,5 +1,5 @@ import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service'; -import { ErrorResponse, GenericSuccessResponse } from '../cache/response-cache.models'; +import { ErrorResponse, GenericSuccessResponse } from '../cache/response.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service'; import { BrowseEntriesRequest } from './request.models'; diff --git a/src/app/core/data/browse-entries-response-parsing.service.ts b/src/app/core/data/browse-entries-response-parsing.service.ts index 171def60df..39600b637d 100644 --- a/src/app/core/data/browse-entries-response-parsing.service.ts +++ b/src/app/core/data/browse-entries-response-parsing.service.ts @@ -7,7 +7,7 @@ import { ErrorResponse, GenericSuccessResponse, RestResponse -} from '../cache/response-cache.models'; +} from '../cache/response.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { BrowseEntry } from '../shared/browse-entry.model'; diff --git a/src/app/core/data/browse-items-response-parsing-service.spec.ts b/src/app/core/data/browse-items-response-parsing-service.spec.ts index 6a141c01c4..99ea474dc6 100644 --- a/src/app/core/data/browse-items-response-parsing-service.spec.ts +++ b/src/app/core/data/browse-items-response-parsing-service.spec.ts @@ -1,5 +1,5 @@ import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service'; -import { ErrorResponse, GenericSuccessResponse } from '../cache/response-cache.models'; +import { ErrorResponse, GenericSuccessResponse } from '../cache/response.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service'; import { BrowseEntriesRequest, BrowseItemsRequest } from './request.models'; diff --git a/src/app/core/data/browse-items-response-parsing-service.ts b/src/app/core/data/browse-items-response-parsing-service.ts index e513ad0898..218c25bac6 100644 --- a/src/app/core/data/browse-items-response-parsing-service.ts +++ b/src/app/core/data/browse-items-response-parsing-service.ts @@ -7,7 +7,7 @@ import { ErrorResponse, GenericSuccessResponse, RestResponse -} from '../cache/response-cache.models'; +} from '../cache/response.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { BaseResponseParsingService } from './base-response-parsing.service'; diff --git a/src/app/core/data/browse-response-parsing.service.spec.ts b/src/app/core/data/browse-response-parsing.service.spec.ts index 2b1703e38f..bedf5f03a7 100644 --- a/src/app/core/data/browse-response-parsing.service.spec.ts +++ b/src/app/core/data/browse-response-parsing.service.spec.ts @@ -1,6 +1,6 @@ import { BrowseResponseParsingService } from './browse-response-parsing.service'; import { BrowseEndpointRequest } from './request.models'; -import { GenericSuccessResponse, ErrorResponse } from '../cache/response-cache.models'; +import { GenericSuccessResponse, ErrorResponse } from '../cache/response.models'; import { BrowseDefinition } from '../shared/browse-definition.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; diff --git a/src/app/core/data/browse-response-parsing.service.ts b/src/app/core/data/browse-response-parsing.service.ts index 8feb1bc82b..523fffd565 100644 --- a/src/app/core/data/browse-response-parsing.service.ts +++ b/src/app/core/data/browse-response-parsing.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { GenericSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models'; +import { GenericSuccessResponse, ErrorResponse, RestResponse } from '../cache/response.models'; import { isNotEmpty } from '../../shared/empty.util'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { BrowseDefinition } from '../shared/browse-definition.model'; diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 2642c4b5e6..74c73e37f3 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -1,10 +1,8 @@ import { Inject, Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; -import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NormalizedCollection } from '../cache/models/normalized-collection.model'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { CoreState } from '../core.reducers'; import { Collection } from '../shared/collection.model'; import { ComColDataService } from './comcol-data.service'; @@ -17,7 +15,6 @@ export class CollectionDataService extends ComColDataService, diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index bc9dc9114a..2125aff797 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -5,7 +5,6 @@ import { GlobalConfig } from '../../../config'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { CoreState } from '../core.reducers'; import { ComColDataService } from './comcol-data.service'; import { CommunityDataService } from './community-data.service'; @@ -23,7 +22,6 @@ class NormalizedTestObject extends NormalizedObject { class TestService extends ComColDataService { constructor( - protected responseCache: ResponseCacheService, protected requestService: RequestService, protected rdbService: RemoteDataBuildService, protected store: Store, @@ -41,7 +39,6 @@ class TestService extends ComColDataService { describe('ComColDataService', () => { let scheduler: TestScheduler; let service: TestService; - let responseCache: ResponseCacheService; let requestService: RequestService; let cds: CommunityDataService; let objectCache: ObjectCacheService; @@ -68,14 +65,6 @@ describe('ComColDataService', () => { }); } - function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService { - return jasmine.createSpyObj('responseCache', { - get: cold('c-', { - c: { response: { isSuccessful } } - }) - }); - } - function initMockObjectCacheService(): ObjectCacheService { return jasmine.createSpyObj('objectCache', { getByUUID: cold('d-', { @@ -90,7 +79,6 @@ describe('ComColDataService', () => { function initTestService(): TestService { return new TestService( - responseCache, requestService, rdbService, store, @@ -111,7 +99,6 @@ describe('ComColDataService', () => { cds = initMockCommunityDataService(); requestService = getMockRequestService(); objectCache = initMockObjectCacheService(); - responseCache = initMockResponseCacheService(true); service = initTestService(); const expected = new FindByIDRequest(requestService.generateRequestId(), communityEndpoint, scopeID); @@ -127,7 +114,6 @@ describe('ComColDataService', () => { cds = initMockCommunityDataService(); requestService = getMockRequestService(); objectCache = initMockObjectCacheService(); - responseCache = initMockResponseCacheService(true); service = initTestService(); }); @@ -150,7 +136,6 @@ describe('ComColDataService', () => { cds = initMockCommunityDataService(); requestService = getMockRequestService(); objectCache = initMockObjectCacheService(); - responseCache = initMockResponseCacheService(false); service = initTestService(); }); diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index c589c5bdc8..95a0015125 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -1,15 +1,16 @@ -import { distinctUntilChanged, filter, map, mergeMap, take, tap } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, mergeMap, share, take, tap } from 'rxjs/operators'; import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs'; -import { isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { NormalizedCommunity } from '../cache/models/normalized-community.model'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { CommunityDataService } from './community-data.service'; import { DataService } from './data.service'; import { FindAllOptions, FindByIDRequest } from './request.models'; import { NormalizedObject } from '../cache/models/normalized-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { RequestEntry } from './request.reducer'; +import { getResponseFromEntry } from '../shared/operators'; export abstract class ComColDataService extends DataService { protected abstract cds: CommunityDataService; @@ -26,9 +27,9 @@ export abstract class ComColDataService } * an Observable containing the scoped URL */ - public getBrowseEndpoint(options: FindAllOptions = {}): Observable { + public getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable { if (isEmpty(options.scopeID)) { - return this.halService.getEndpoint(this.linkPath); + return this.halService.getEndpoint(linkPath); } else { const scopeCommunityHrefObs = this.cds.getEndpoint().pipe( mergeMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, options.scopeID)), @@ -37,7 +38,7 @@ export abstract class ComColDataService { const request = new FindByIDRequest(this.requestService.generateRequestId(), href, options.scopeID); this.requestService.configure(request); - }),); + })); // return scopeCommunityHrefObs.pipe( // mergeMap((href: string) => this.responseCache.get(href)), @@ -46,7 +47,7 @@ export abstract class ComColDataService = this.objectCache.getByUUID(scopeID); // return community$.pipe( - // map((community) => community._links[this.linkPath]), + // map((community) => community._links[linkPath]), // filter((href) => isNotEmpty(href)), // distinctUntilChanged() // ); @@ -57,8 +58,8 @@ export abstract class ComColDataService this.responseCache.get(href)), - map((entry: ResponseCacheEntry) => entry.response)); + mergeMap((href: string) => this.requestService.getByHref(href)), + getResponseFromEntry()); const errorResponses = responses.pipe( filter((response) => !response.isSuccessful), mergeMap(() => observableThrowError(new Error(`The Community with scope ${options.scopeID} couldn't be retrieved`))) @@ -66,11 +67,11 @@ export abstract class ComColDataService response.isSuccessful), mergeMap(() => this.objectCache.getByUUID(options.scopeID)), - map((nc: NormalizedCommunity) => nc._links[this.linkPath]), + map((nc: NormalizedCommunity) => nc._links[linkPath]), filter((href) => isNotEmpty(href)) ); - return observableMerge(errorResponses, successResponses).pipe(distinctUntilChanged()); + return observableMerge(errorResponses, successResponses).pipe(distinctUntilChanged(), share()); } } } diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index df0e739490..a037936202 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -1,12 +1,10 @@ - -import {mergeMap, filter, take} from 'rxjs/operators'; +import { filter, mergeMap, take } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NormalizedCommunity } from '../cache/models/normalized-community.model'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { CoreState } from '../core.reducers'; import { Community } from '../shared/community.model'; import { ComColDataService } from './comcol-data.service'; @@ -25,7 +23,6 @@ export class CommunityDataService extends ComColDataService, @@ -40,12 +37,10 @@ export class CommunityDataService extends ComColDataService>> { - const hrefObs = this.halService.getEndpoint(this.topLinkPath).pipe(filter((href: string) => isNotEmpty(href)), - mergeMap((endpoint: string) => this.getFindAllHref(options)),); - + const hrefObs = this.getFindAllHref(options, this.topLinkPath); hrefObs.pipe( filter((href: string) => hasValue(href)), - take(1),) + take(1)) .subscribe((href: string) => { const request = new FindAllRequest(this.requestService.generateRequestId(), href, options); this.requestService.configure(request); diff --git a/src/app/core/data/config-response-parsing.service.spec.ts b/src/app/core/data/config-response-parsing.service.spec.ts index 654ee53651..a33c5cf5b5 100644 --- a/src/app/core/data/config-response-parsing.service.spec.ts +++ b/src/app/core/data/config-response-parsing.service.spec.ts @@ -1,4 +1,4 @@ -import { ConfigSuccessResponse, ErrorResponse } from '../cache/response-cache.models'; +import { ConfigSuccessResponse, ErrorResponse } from '../cache/response.models'; import { ConfigResponseParsingService } from './config-response-parsing.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { GlobalConfig } from '../../../config/global-config.interface'; diff --git a/src/app/core/data/config-response-parsing.service.ts b/src/app/core/data/config-response-parsing.service.ts index 2b1b923625..ddf884e02b 100644 --- a/src/app/core/data/config-response-parsing.service.ts +++ b/src/app/core/data/config-response-parsing.service.ts @@ -3,7 +3,7 @@ import { Inject, Injectable } from '@angular/core'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { ConfigSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models'; +import { ConfigSuccessResponse, ErrorResponse, RestResponse } from '../cache/response.models'; import { isNotEmpty } from '../../shared/empty.util'; import { ConfigObjectFactory } from '../shared/config/config-object-factory'; diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index e60725c4c2..7da709abd5 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -1,6 +1,5 @@ import { DataService } from './data.service'; import { NormalizedObject } from '../cache/models/normalized-object.model'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { RequestService } from './request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { CoreState } from '../core.reducers'; @@ -22,7 +21,6 @@ class NormalizedTestObject extends NormalizedObject { class TestService extends DataService { constructor( - protected responseCache: ResponseCacheService, protected requestService: RequestService, protected rdbService: RemoteDataBuildService, protected store: Store, @@ -33,7 +31,7 @@ class TestService extends DataService { super(); } - public getBrowseEndpoint(options: FindAllOptions): Observable { + public getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable { return observableOf(endpoint); } } @@ -41,7 +39,6 @@ class TestService extends DataService { describe('DataService', () => { let service: TestService; let options: FindAllOptions; - const responseCache = {} as ResponseCacheService; const requestService = {} as RequestService; const halService = {} as HALEndpointService; const rdbService = {} as RemoteDataBuildService; @@ -57,7 +54,6 @@ describe('DataService', () => { function initTestService(): TestService { return new TestService( - responseCache, requestService, rdbService, store, diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index d23d7e8064..4a993e4ac6 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -1,9 +1,8 @@ -import { distinctUntilChanged, filter, first, map, take } from 'rxjs/operators'; +import { delay, distinctUntilChanged, filter, first, map, take, tap } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { Store } from '@ngrx/store'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { CoreState } from '../core.reducers'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { URLCombiner } from '../url-combiner/url-combiner'; @@ -15,9 +14,9 @@ import { NormalizedObject } from '../cache/models/normalized-object.model'; import { compare, Operation } from 'fast-json-patch'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DSpaceObject } from '../shared/dspace-object.model'; +import { of } from 'rxjs/internal/observable/of'; export abstract class DataService { - protected abstract responseCache: ResponseCacheService; protected abstract requestService: RequestService; protected abstract rdbService: RemoteDataBuildService; protected abstract store: Store; @@ -25,34 +24,31 @@ export abstract class DataService protected abstract halService: HALEndpointService; protected abstract objectCache: ObjectCacheService; - public abstract getBrowseEndpoint(options: FindAllOptions): Observable + public abstract getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable - protected getFindAllHref(options: FindAllOptions = {}): Observable { + protected getFindAllHref(options: FindAllOptions = {}, linkPath?: string): Observable { let result: Observable; const args = []; - result = this.getBrowseEndpoint(options).pipe(distinctUntilChanged()); - + result = this.getBrowseEndpoint(options, linkPath); if (hasValue(options.currentPage) && typeof options.currentPage === 'number') { /* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */ args.push(`page=${options.currentPage - 1}`); } - if (hasValue(options.elementsPerPage)) { args.push(`size=${options.elementsPerPage}`); } - if (hasValue(options.sort)) { args.push(`sort=${options.sort.field},${options.sort.direction}`); } - if (hasValue(options.startsWith)) { args.push(`startsWith=${options.startsWith}`); } - if (isNotEmpty(args)) { return result.pipe(map((href: string) => new URLCombiner(href, `?${args.join('&')}`).toString())); } else { + result.subscribe((t) => console.log(t)); + return result; } } @@ -115,6 +111,7 @@ export abstract class DataService this.objectCache.addPatch(object.self, operations); } } + // TODO implement, after the structure of the REST server's POST response is finalized // create(dso: DSpaceObject): Observable> { // const postHrefObs = this.getEndpoint(); diff --git a/src/app/core/data/debug-response-parsing.service.ts b/src/app/core/data/debug-response-parsing.service.ts index d530948559..174abec897 100644 --- a/src/app/core/data/debug-response-parsing.service.ts +++ b/src/app/core/data/debug-response-parsing.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { RestResponse } from '../cache/response-cache.models'; +import { RestResponse } from '../cache/response.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; diff --git a/src/app/core/data/dso-response-parsing.service.ts b/src/app/core/data/dso-response-parsing.service.ts index aff450781f..568114be1a 100644 --- a/src/app/core/data/dso-response-parsing.service.ts +++ b/src/app/core/data/dso-response-parsing.service.ts @@ -7,7 +7,7 @@ import { NormalizedObject } from '../cache/models/normalized-object.model'; import { ResourceType } from '../shared/resource-type'; import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { RestResponse, DSOSuccessResponse } from '../cache/response-cache.models'; +import { RestResponse, DSOSuccessResponse } from '../cache/response.models'; import { RestRequest } from './request.models'; import { ResponseParsingService } from './parsing.service'; @@ -23,12 +23,14 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem constructor( @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, protected objectCache: ObjectCacheService, - ) { super(); + ) { + super(); } parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - const processRequestDTO = this.process(data.payload, request.href); + const processRequestDTO = this.process(data.payload, request.href); let objectList = processRequestDTO; + if (hasNoValue(processRequestDTO)) { return new DSOSuccessResponse([], data.statusCode, undefined) } diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index 03c602d2aa..324692c676 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -3,7 +3,6 @@ import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { CoreState } from '../core.reducers'; import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; @@ -18,7 +17,6 @@ class DataServiceImpl extends DataService protected linkPath = 'dso'; constructor( - protected responseCache: ResponseCacheService, protected requestService: RequestService, protected rdbService: RemoteDataBuildService, protected store: Store, @@ -27,8 +25,8 @@ class DataServiceImpl extends DataService super(); } - getBrowseEndpoint(options: FindAllOptions): Observable { - return this.halService.getEndpoint(this.linkPath); + getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable { + return this.halService.getEndpoint(linkPath); } getFindByIDHref(endpoint, resourceID): string { @@ -46,7 +44,7 @@ export class DSpaceObjectDataService { protected rdbService: RemoteDataBuildService, protected halService: HALEndpointService, protected objectCache: ObjectCacheService) { - this.dataService = new DataServiceImpl(null, requestService, rdbService, null, halService, objectCache); + this.dataService = new DataServiceImpl(requestService, rdbService, null, halService, objectCache); } findById(uuid: string): Observable> { diff --git a/src/app/core/data/endpoint-map-response-parsing.service.ts b/src/app/core/data/endpoint-map-response-parsing.service.ts index b850e13932..a145477953 100644 --- a/src/app/core/data/endpoint-map-response-parsing.service.ts +++ b/src/app/core/data/endpoint-map-response-parsing.service.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@angular/core'; import { GLOBAL_CONFIG } from '../../../config'; import { GlobalConfig } from '../../../config/global-config.interface'; -import { ErrorResponse, RestResponse, EndpointMapSuccessResponse } from '../cache/response-cache.models'; +import { ErrorResponse, RestResponse, EndpointMapSuccessResponse } from '../cache/response.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; diff --git a/src/app/core/data/facet-config-response-parsing.service.ts b/src/app/core/data/facet-config-response-parsing.service.ts index b0d89fb03e..02b12dfa10 100644 --- a/src/app/core/data/facet-config-response-parsing.service.ts +++ b/src/app/core/data/facet-config-response-parsing.service.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@angular/core'; import { FacetConfigSuccessResponse, RestResponse -} from '../cache/response-cache.models'; +} from '../cache/response.models'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; diff --git a/src/app/core/data/facet-value-map-response-parsing.service.ts b/src/app/core/data/facet-value-map-response-parsing.service.ts index 8588e4aa0b..0fc5917847 100644 --- a/src/app/core/data/facet-value-map-response-parsing.service.ts +++ b/src/app/core/data/facet-value-map-response-parsing.service.ts @@ -4,7 +4,7 @@ import { FacetValueMapSuccessResponse, FacetValueSuccessResponse, RestResponse -} from '../cache/response-cache.models'; +} from '../cache/response.models'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; diff --git a/src/app/core/data/facet-value-response-parsing.service.ts b/src/app/core/data/facet-value-response-parsing.service.ts index bc3f4e5368..585172c22e 100644 --- a/src/app/core/data/facet-value-response-parsing.service.ts +++ b/src/app/core/data/facet-value-response-parsing.service.ts @@ -4,7 +4,7 @@ import { FacetValueMapSuccessResponse, FacetValueSuccessResponse, RestResponse -} from '../cache/response-cache.models'; +} from '../cache/response.models'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index 050c888de2..bb67fc8412 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -3,7 +3,6 @@ import { cold, getTestScheduler } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; import { BrowseService } from '../browse/browse.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { CoreState } from '../core.reducers'; import { ItemDataService } from './item-data.service'; import { RequestService } from './request.service'; @@ -16,7 +15,6 @@ describe('ItemDataService', () => { let service: ItemDataService; let bs: BrowseService; const requestService = {} as RequestService; - const responseCache = {} as ResponseCacheService; const rdbService = {} as RemoteDataBuildService; const objectCache = {} as ObjectCacheService; const store = {} as Store; @@ -48,7 +46,6 @@ describe('ItemDataService', () => { function initTestService() { return new ItemDataService( - responseCache, requestService, rdbService, store, diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 22ca23a4e2..9c2c87119f 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -7,7 +7,6 @@ import { isNotEmpty } from '../../shared/empty.util'; import { BrowseService } from '../browse/browse.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NormalizedItem } from '../cache/models/normalized-item.model'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { CoreState } from '../core.reducers'; import { Item } from '../shared/item.model'; import { URLCombiner } from '../url-combiner/url-combiner'; @@ -23,7 +22,6 @@ export class ItemDataService extends DataService { protected linkPath = 'items'; constructor( - protected responseCache: ResponseCacheService, protected requestService: RequestService, protected rdbService: RemoteDataBuildService, protected store: Store, @@ -39,12 +37,12 @@ export class ItemDataService extends DataService { * @param {FindAllOptions} options * @returns {Observable} */ - public getBrowseEndpoint(options: FindAllOptions = {}): Observable { + public getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable { let field = 'dc.date.issued'; if (options.sort && options.sort.field) { field = options.sort.field; } - return this.bs.getBrowseURLFor(field, this.linkPath).pipe( + return this.bs.getBrowseURLFor(field, linkPath).pipe( filter((href: string) => isNotEmpty(href)), map((href: string) => new URLCombiner(href, `?scope=${options.scopeID}`).toString()), distinctUntilChanged(),); diff --git a/src/app/core/data/metadataschema-parsing.service.ts b/src/app/core/data/metadataschema-parsing.service.ts index cdd87c19d4..78a5257456 100644 --- a/src/app/core/data/metadataschema-parsing.service.ts +++ b/src/app/core/data/metadataschema-parsing.service.ts @@ -4,7 +4,7 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response. import { RestRequest } from './request.models'; import { ResponseParsingService } from './parsing.service'; import { Injectable } from '@angular/core'; -import { MetadataschemaSuccessResponse, RestResponse } from '../cache/response-cache.models'; +import { MetadataschemaSuccessResponse, RestResponse } from '../cache/response.models'; @Injectable() export class MetadataschemaParsingService implements ResponseParsingService { diff --git a/src/app/core/data/parsing.service.ts b/src/app/core/data/parsing.service.ts index a137b99079..ea8d1ea810 100644 --- a/src/app/core/data/parsing.service.ts +++ b/src/app/core/data/parsing.service.ts @@ -1,6 +1,6 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { RestRequest } from './request.models'; -import { RestResponse } from '../cache/response-cache.models'; +import { RestResponse } from '../cache/response.models'; export interface ResponseParsingService { parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse; diff --git a/src/app/core/data/registry-bitstreamformats-response-parsing.service.ts b/src/app/core/data/registry-bitstreamformats-response-parsing.service.ts index d981a12719..2ee3bbf75e 100644 --- a/src/app/core/data/registry-bitstreamformats-response-parsing.service.ts +++ b/src/app/core/data/registry-bitstreamformats-response-parsing.service.ts @@ -1,4 +1,4 @@ -import { RegistryBitstreamformatsSuccessResponse, RestResponse } from '../cache/response-cache.models'; +import { RegistryBitstreamformatsSuccessResponse, RestResponse } from '../cache/response.models'; import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; diff --git a/src/app/core/data/registry-metadatafields-response-parsing.service.ts b/src/app/core/data/registry-metadatafields-response-parsing.service.ts index 1fe8b1e15f..0b0982d048 100644 --- a/src/app/core/data/registry-metadatafields-response-parsing.service.ts +++ b/src/app/core/data/registry-metadatafields-response-parsing.service.ts @@ -1,7 +1,7 @@ import { RegistryMetadatafieldsSuccessResponse, RestResponse -} from '../cache/response-cache.models'; +} from '../cache/response.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { RestRequest } from './request.models'; import { ResponseParsingService } from './parsing.service'; diff --git a/src/app/core/data/registry-metadataschemas-response-parsing.service.ts b/src/app/core/data/registry-metadataschemas-response-parsing.service.ts index 2bb1302450..a70c985b15 100644 --- a/src/app/core/data/registry-metadataschemas-response-parsing.service.ts +++ b/src/app/core/data/registry-metadataschemas-response-parsing.service.ts @@ -1,4 +1,4 @@ -import { RegistryMetadataschemasSuccessResponse, RestResponse } from '../cache/response-cache.models'; +import { RegistryMetadataschemasSuccessResponse, RestResponse } from '../cache/response.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { RestRequest } from './request.models'; import { ResponseParsingService } from './parsing.service'; diff --git a/src/app/core/data/request.actions.ts b/src/app/core/data/request.actions.ts index 436c365caa..28149c2ead 100644 --- a/src/app/core/data/request.actions.ts +++ b/src/app/core/data/request.actions.ts @@ -1,6 +1,7 @@ import { Action } from '@ngrx/store'; import { type } from '../../shared/ngrx/type'; import { RestRequest } from './request.models'; +import { RestResponse } from '../cache/response.models'; /** * The list of RequestAction type definitions @@ -8,7 +9,8 @@ import { RestRequest } from './request.models'; export const RequestActionTypes = { CONFIGURE: type('dspace/core/data/request/CONFIGURE'), EXECUTE: type('dspace/core/data/request/EXECUTE'), - COMPLETE: type('dspace/core/data/request/COMPLETE') + COMPLETE: type('dspace/core/data/request/COMPLETE'), + RESET_TIMESTAMPS: type('dspace/core/data/request/RESET_TIMESTAMPS') }; /* tslint:disable:max-classes-per-file */ @@ -43,7 +45,10 @@ export class RequestExecuteAction implements Action { */ export class RequestCompleteAction implements Action { type = RequestActionTypes.COMPLETE; - payload: string; + payload: { + uuid: string, + response: RestResponse + }; /** * Create a new RequestCompleteAction @@ -51,10 +56,32 @@ export class RequestCompleteAction implements Action { * @param uuid * the request's uuid */ - constructor(uuid: string) { - this.payload = uuid; + constructor(uuid: string, response: RestResponse) { + this.payload = { + uuid, + response + }; } } + +/** + * An ngrx action to reset the timeAdded property of all responses in the cached objects + */ +export class ResetResponseTimestampsAction implements Action { + type = RequestActionTypes.RESET_TIMESTAMPS; + payload: number; + + /** + * Create a new ResetResponseTimestampsAction + * + * @param newTimestamp + * the new timeAdded all objects should get + */ + constructor(newTimestamp: number) { + this.payload = newTimestamp; + } +} + /* tslint:enable:max-classes-per-file */ /** @@ -63,4 +90,5 @@ export class RequestCompleteAction implements Action { export type RequestAction = RequestConfigureAction | RequestExecuteAction - | RequestCompleteAction; + | RequestCompleteAction + | ResetResponseTimestampsAction; diff --git a/src/app/core/data/request.effects.ts b/src/app/core/data/request.effects.ts index 6d42f792d1..bda91283bf 100644 --- a/src/app/core/data/request.effects.ts +++ b/src/app/core/data/request.effects.ts @@ -1,30 +1,33 @@ - -import {of as observableOf, Observable } from 'rxjs'; +import { Observable, of as observableOf } from 'rxjs'; import { Inject, Injectable, Injector } from '@angular/core'; -import { Request } from '@angular/http'; -import { RequestArgs } from '@angular/http/src/interfaces'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; import { isNotEmpty } from '../../shared/empty.util'; -import { ErrorResponse, RestResponse } from '../cache/response-cache.models'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service'; -import { RequestActionTypes, RequestCompleteAction, RequestExecuteAction } from './request.actions'; +import { + RequestActionTypes, + RequestCompleteAction, + RequestExecuteAction, + ResetResponseTimestampsAction +} from './request.actions'; import { RequestError, RestRequest } from './request.models'; import { RequestEntry } from './request.reducer'; import { RequestService } from './request.service'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; import { catchError, flatMap, map, take, tap } from 'rxjs/operators'; +import { ErrorResponse, RestResponse } from '../cache/response.models'; +import { StoreActionTypes } from '../../store.actions'; -export const addToResponseCacheAndCompleteAction = (request: RestRequest, responseCache: ResponseCacheService, envConfig: GlobalConfig) => - (source: Observable): Observable => +export const addToResponseCacheAndCompleteAction = (request: RestRequest, envConfig: GlobalConfig) => + (source: Observable): Observable => source.pipe( - tap((response: RestResponse) => responseCache.add(request.href, response, request.responseMsToLive ? request.responseMsToLive : envConfig.cache.msToLive.default)), - map((response: RestResponse) => new RequestCompleteAction(request.uuid)) + map((response: RestResponse) => { + return new RequestCompleteAction(request.uuid, response) + }) ); @Injectable() @@ -46,20 +49,32 @@ export class RequestEffects { } return this.restApi.request(request.method, request.href, body, request.options).pipe( map((data: DSpaceRESTV2Response) => this.injector.get(request.getResponseParser()).parse(request, data)), - addToResponseCacheAndCompleteAction(request, this.responseCache, this.EnvConfig), + addToResponseCacheAndCompleteAction(request, this.EnvConfig), catchError((error: RequestError) => observableOf(new ErrorResponse(error)).pipe( - addToResponseCacheAndCompleteAction(request, this.responseCache, this.EnvConfig) + addToResponseCacheAndCompleteAction(request, this.EnvConfig) )) ); }) ); + /** + * 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 ResetResponseTimestampsAction(new Date().getTime())) + ); + constructor( @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig, private actions$: Actions, private restApi: DSpaceRESTv2Service, private injector: Injector, - private responseCache: ResponseCacheService, protected requestService: RequestService ) { } diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index cd6ac1dbe2..a96d9716b2 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -14,32 +14,36 @@ import { BrowseItemsResponseParsingService } from './browse-items-response-parsi /* tslint:disable:max-classes-per-file */ - export abstract class RestRequest { + public responseMsToLive = 0; constructor( public uuid: string, public href: string, public method: RestRequestMethod = RestRequestMethod.GET, public body?: any, public options?: HttpOptions, - public responseMsToLive?: number ) { } getResponseParser(): GenericConstructor { return DSOResponseParsingService; } + + get toCache(): boolean { + return this.responseMsToLive > 0; + } } export class GetRequest extends RestRequest { + public responseMsToLive = 60 * 15 * 1000; + constructor( public uuid: string, public href: string, public body?: any, public options?: HttpOptions, - public responseMsToLive?: number ) { - super(uuid, href, RestRequestMethod.GET, body, options, responseMsToLive) + super(uuid, href, RestRequestMethod.GET, body, options) } } @@ -212,6 +216,7 @@ export class IntegrationRequest extends GetRequest { return IntegrationResponseParsingService; } } + export class RequestError extends Error { statusText: string; } diff --git a/src/app/core/data/request.reducer.ts b/src/app/core/data/request.reducer.ts index 3ac35d2741..b0875f37b3 100644 --- a/src/app/core/data/request.reducer.ts +++ b/src/app/core/data/request.reducer.ts @@ -1,14 +1,16 @@ import { RequestActionTypes, RequestAction, RequestConfigureAction, - RequestExecuteAction, RequestCompleteAction + RequestExecuteAction, RequestCompleteAction, ResetResponseTimestampsAction } from './request.actions'; import { RestRequest } from './request.models'; +import { RestResponse } from '../cache/response.models'; export class RequestEntry { request: RestRequest; requestPending: boolean; responsePending: boolean; completed: boolean; + response: RestResponse } export interface RequestState { @@ -32,6 +34,9 @@ export function requestReducer(state = initialState, action: RequestAction): Req case RequestActionTypes.COMPLETE: { return completeRequest(state, action as RequestCompleteAction); } + case RequestActionTypes.RESET_TIMESTAMPS: { + return resetResponseTimestamps(state, action as ResetResponseTimestampsAction); + } default: { return state; @@ -45,7 +50,7 @@ function configureRequest(state: RequestState, action: RequestConfigureAction): request: action.payload, requestPending: true, responsePending: false, - completed: false + completed: false, } }); } @@ -70,10 +75,25 @@ function executeRequest(state: RequestState, action: RequestExecuteAction): Requ * the new state, with the response added to the request */ function completeRequest(state: RequestState, action: RequestCompleteAction): RequestState { - return Object.assign({}, state, { - [action.payload]: Object.assign({}, state[action.payload], { + const time = new Date().getTime(); + + const ob = Object.assign({}, state, { + [action.payload.uuid]: Object.assign({}, state[action.payload.uuid], { responsePending: false, - completed: true + completed: true, + response: Object.assign({}, action.payload.response, { timeAdded: time }) }) }); + console.log(ob); + return ob; +} + +function resetResponseTimestamps(state: RequestState, action: ResetResponseTimestampsAction) { + const newState = Object.create(null); + Object.keys(state).forEach((key) => { + newState[key] = Object.assign({}, state[key], + { response: Object.assign({}, state[key].response, { timeAdded: action.payload }) } + ); + }); + return newState; } diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index 39f29a9beb..5953d43c9f 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -1,10 +1,8 @@ import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service'; -import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service'; import { defaultUUID, getMockUUIDService } from '../../shared/mocks/mock-uuid.service'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { CoreState } from '../core.reducers'; import { UUIDService } from '../shared/uuid.service'; import { RequestConfigureAction, RequestExecuteAction } from './request.actions'; @@ -29,7 +27,6 @@ describe('RequestService', () => { let service: RequestService; let serviceAsAny: any; let objectCache: ObjectCacheService; - let responseCache: ResponseCacheService; let uuidService: UUIDService; let store: Store; @@ -49,7 +46,6 @@ describe('RequestService', () => { objectCache = getMockObjectCacheService(); (objectCache.hasBySelfLink as any).and.returnValue(false); - responseCache = getMockResponseCacheService(); (responseCache.has as any).and.returnValue(false); (responseCache.get as any).and.returnValue(observableOf(undefined)); @@ -65,7 +61,6 @@ describe('RequestService', () => { service = new RequestService( objectCache, - responseCache, uuidService, store ); diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 71d0189816..cc8c9816f8 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -1,14 +1,23 @@ -import { Observable, merge as observableMerge } from 'rxjs'; -import { filter, first, map, mergeMap, partition, take } from 'rxjs/operators'; +import { merge as observableMerge, Observable, of as observableOf } from 'rxjs'; +import { + filter, + find, + first, + map, + mergeMap, + reduce, + startWith, + switchMap, + take, + tap +} from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { MemoizedSelector, select, Store } from '@ngrx/store'; -import { hasValue } from '../../shared/empty.util'; +import { hasNoValue, hasValue } from '../../shared/empty.util'; import { CacheableObject } from '../cache/object-cache.reducer'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { DSOSuccessResponse, RestResponse } from '../cache/response-cache.models'; -import { ResponseCacheEntry } from '../cache/response-cache.reducer'; -import { ResponseCacheService } from '../cache/response-cache.service'; +import { DSOSuccessResponse, RestResponse } from '../cache/response.models'; import { coreSelector, CoreState } from '../core.reducers'; import { IndexName } from '../index/index.reducer'; import { pathSelector } from '../shared/selectors'; @@ -19,13 +28,13 @@ import { GetRequest, RestRequest } from './request.models'; import { RequestEntry } from './request.reducer'; import { CommitSSBAction } from '../cache/server-sync-buffer.actions'; import { RestRequestMethod } from './rest-request-method'; +import { getResponseFromEntry } from '../shared/operators'; @Injectable() export class RequestService { private requestsOnTheirWayToTheStore: string[] = []; constructor(private objectCache: ObjectCacheService, - private responseCache: ResponseCacheService, private uuidService: UUIDService, private store: Store) { } @@ -83,23 +92,27 @@ export class RequestService { private isCachedOrPending(request: GetRequest) { let isCached = this.objectCache.hasBySelfLink(request.href); - if (!isCached && this.responseCache.has(request.href)) { - const responses = this.responseCache.get(request.href).pipe( - take(1), - map((entry: ResponseCacheEntry) => entry.response) - ); + const responses: Observable = this.isReusable(request.uuid).pipe( + filter((reusable: boolean) => !isCached && reusable), + switchMap(() => { + return this.getByHref(request.href).pipe( + getResponseFromEntry(), + take(1) + ); + } + )); - const errorResponses = responses.pipe(filter((response) => !response.isSuccessful), map(() => true)); // TODO add a configurable number of retries in case of an error. - const dsoSuccessResponses = responses.pipe( - filter((response) => response.isSuccessful && hasValue((response as DSOSuccessResponse).resourceSelfLinks)), - map((response: DSOSuccessResponse) => response.resourceSelfLinks), - map((resourceSelfLinks: string[]) => resourceSelfLinks - .every((selfLink) => this.objectCache.hasBySelfLink(selfLink)) - )); - const otherSuccessResponses = responses.pipe(filter((response) => response.isSuccessful && !hasValue((response as DSOSuccessResponse).resourceSelfLinks)), map(() => true)); + const errorResponses = responses.pipe(filter((response) => !response.isSuccessful), map(() => true)); // TODO add a configurable number of retries in case of an error. + const dsoSuccessResponses = responses.pipe( + filter((response) => response.isSuccessful && hasValue((response as DSOSuccessResponse).resourceSelfLinks)), + map((response: DSOSuccessResponse) => response.resourceSelfLinks), + map((resourceSelfLinks: string[]) => resourceSelfLinks + .every((selfLink) => this.objectCache.hasBySelfLink(selfLink)) + )); + const otherSuccessResponses = responses.pipe(filter((response) => response.isSuccessful && !hasValue((response as DSOSuccessResponse).resourceSelfLinks)), map(() => true)); + + observableMerge(errorResponses, otherSuccessResponses, dsoSuccessResponses).subscribe((c) => isCached = c); - observableMerge(errorResponses, otherSuccessResponses, dsoSuccessResponses).subscribe((c) => isCached = c); - } const isPending = this.isPending(request); return isCached || isPending; } @@ -129,4 +142,34 @@ export class RequestService { commit(method?: RestRequestMethod) { this.store.dispatch(new CommitSSBAction(method)) } + + /** + * 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 isReusable(uuid: string): Observable { + if (hasNoValue(uuid)) { + return observableOf(false); + } else { + const requestEntry$ = this.getByUUID(uuid); + return requestEntry$.pipe( + filter((entry: RequestEntry) => hasValue(entry) && hasValue(entry.response)), + map((entry: RequestEntry) => { + if (hasValue(entry) && entry.response.isSuccessful) { + const timeOutdated = entry.response.timeAdded + entry.request.responseMsToLive; + const isOutDated = new Date().getTime() > timeOutdated; + return !isOutDated; + } else { + return false; + } + }) + ); + return observableOf(false); + } + } } diff --git a/src/app/core/data/search-response-parsing.service.ts b/src/app/core/data/search-response-parsing.service.ts index 4039b8f761..7ee2b60f89 100644 --- a/src/app/core/data/search-response-parsing.service.ts +++ b/src/app/core/data/search-response-parsing.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { RestResponse, SearchSuccessResponse } from '../cache/response-cache.models'; +import { RestResponse, SearchSuccessResponse } from '../cache/response.models'; import { DSOResponseParsingService } from './dso-response-parsing.service'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; diff --git a/src/app/core/integration/authority.service.ts b/src/app/core/integration/authority.service.ts index cb2595adc4..a5fa3a8d09 100644 --- a/src/app/core/integration/authority.service.ts +++ b/src/app/core/integration/authority.service.ts @@ -1,6 +1,5 @@ import { Injectable } from '@angular/core'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { RequestService } from '../data/request.service'; import { IntegrationService } from './integration.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; @@ -11,7 +10,6 @@ export class AuthorityService extends IntegrationService { protected browseEndpoint = 'entries'; constructor( - protected responseCache: ResponseCacheService, protected requestService: RequestService, protected halService: HALEndpointService) { super(); diff --git a/src/app/core/integration/integration-response-parsing.service.spec.ts b/src/app/core/integration/integration-response-parsing.service.spec.ts index 9c3e5b0344..38741da4e2 100644 --- a/src/app/core/integration/integration-response-parsing.service.spec.ts +++ b/src/app/core/integration/integration-response-parsing.service.spec.ts @@ -1,4 +1,4 @@ -import { ErrorResponse, IntegrationSuccessResponse } from '../cache/response-cache.models'; +import { ErrorResponse, IntegrationSuccessResponse } from '../cache/response.models'; import { ObjectCacheService } from '../cache/object-cache.service'; import { GlobalConfig } from '../../../config/global-config.interface'; diff --git a/src/app/core/integration/integration-response-parsing.service.ts b/src/app/core/integration/integration-response-parsing.service.ts index 06c6b9620d..6eff7ab792 100644 --- a/src/app/core/integration/integration-response-parsing.service.ts +++ b/src/app/core/integration/integration-response-parsing.service.ts @@ -6,7 +6,7 @@ import { ErrorResponse, IntegrationSuccessResponse, RestResponse -} from '../cache/response-cache.models'; +} from '../cache/response.models'; import { isNotEmpty } from '../../shared/empty.util'; import { IntegrationObjectFactory } from './integration-object-factory'; diff --git a/src/app/core/integration/integration.service.spec.ts b/src/app/core/integration/integration.service.spec.ts index f7e3769620..158f4b0680 100644 --- a/src/app/core/integration/integration.service.spec.ts +++ b/src/app/core/integration/integration.service.spec.ts @@ -1,7 +1,6 @@ import { cold, getTestScheduler } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { RequestService } from '../data/request.service'; import { IntegrationRequest } from '../data/request.models'; @@ -18,7 +17,6 @@ class TestService extends IntegrationService { protected browseEndpoint = BROWSE; constructor( - protected responseCache: ResponseCacheService, protected requestService: RequestService, protected halService: HALEndpointService) { super(); @@ -28,7 +26,6 @@ class TestService extends IntegrationService { describe('IntegrationService', () => { let scheduler: TestScheduler; let service: TestService; - let responseCache: ResponseCacheService; let requestService: RequestService; let halService: any; let findOptions: IntegrationSearchOptions; @@ -43,24 +40,14 @@ describe('IntegrationService', () => { findOptions = new IntegrationSearchOptions(uuid, name, metadata); - function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService { - return jasmine.createSpyObj('responseCache', { - get: cold('c-', { - c: {response: {isSuccessful}} - }) - }); - } - - function initTestService(): TestService { + function initTestService(): TestService { return new TestService( - responseCache, requestService, halService ); } beforeEach(() => { - responseCache = initMockResponseCacheService(true); requestService = getMockRequestService(); scheduler = getTestScheduler(); halService = new HALEndpointServiceStub(integrationEndpoint); diff --git a/src/app/core/integration/integration.service.ts b/src/app/core/integration/integration.service.ts index 3c71ca5f3b..2ace710dc7 100644 --- a/src/app/core/integration/integration.service.ts +++ b/src/app/core/integration/integration.service.ts @@ -1,27 +1,26 @@ import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs'; import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators'; import { RequestService } from '../data/request.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; -import { IntegrationSuccessResponse } from '../cache/response-cache.models'; +import { IntegrationSuccessResponse } from '../cache/response.models'; import { GetRequest, IntegrationRequest } from '../data/request.models'; -import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { IntegrationData } from './integration-data'; import { IntegrationSearchOptions } from './models/integration-options.model'; +import { RequestEntry } from '../data/request.reducer'; +import { getResponseFromEntry } from '../shared/operators'; export abstract class IntegrationService { protected request: IntegrationRequest; - protected abstract responseCache: ResponseCacheService; protected abstract requestService: RequestService; protected abstract linkPath: string; protected abstract browseEndpoint: string; protected abstract halService: HALEndpointService; protected getData(request: GetRequest): Observable { - return this.responseCache.get(request.href).pipe( - map((entry: ResponseCacheEntry) => entry.response), - mergeMap((response) => { + return this.requestService.getByHref(request.href).pipe( + getResponseFromEntry(), + mergeMap((response) => { if (response.isSuccessful && isNotEmpty(response)) { const dataResponse = response as IntegrationSuccessResponse; return observableOf(new IntegrationData(dataResponse.pageInfo, dataResponse.dataDefinition)); diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 50ce4711ff..f8d6435a03 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -23,7 +23,6 @@ import { ItemDataService } from '../data/item-data.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RequestService } from '../data/request.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; @@ -62,7 +61,6 @@ describe('MetadataService', () => { let store: Store; let objectCacheService: ObjectCacheService; - let responseCacheService: ResponseCacheService; let requestService: RequestService; let uuidService: UUIDService; let remoteDataBuildService: RemoteDataBuildService; @@ -82,10 +80,9 @@ describe('MetadataService', () => { spyOn(store, 'dispatch'); objectCacheService = new ObjectCacheService(store); - responseCacheService = new ResponseCacheService(store); uuidService = new UUIDService(); - requestService = new RequestService(objectCacheService, responseCacheService, uuidService, store); - remoteDataBuildService = new RemoteDataBuildService(objectCacheService, responseCacheService, requestService); + requestService = new RequestService(objectCacheService, uuidService, store); + remoteDataBuildService = new RemoteDataBuildService(objectCacheService, requestService); TestBed.configureTestingModule({ imports: [ @@ -108,7 +105,6 @@ describe('MetadataService', () => { ], providers: [ { provide: ObjectCacheService, useValue: objectCacheService }, - { provide: ResponseCacheService, useValue: responseCacheService }, { provide: RequestService, useValue: requestService }, { provide: RemoteDataBuildService, useValue: remoteDataBuildService }, { provide: GLOBAL_CONFIG, useValue: ENV_CONFIG }, diff --git a/src/app/core/registry/registry.service.spec.ts b/src/app/core/registry/registry.service.spec.ts index d0ed1e5cb8..adb2ed8b05 100644 --- a/src/app/core/registry/registry.service.spec.ts +++ b/src/app/core/registry/registry.service.spec.ts @@ -1,24 +1,21 @@ import { TestBed } from '@angular/core/testing'; import { RegistryService } from './registry.service'; import { CommonModule } from '@angular/common'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { RequestService } from '../data/request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { Observable, of as observableOf, combineLatest as observableCombineLatest } from 'rxjs'; -import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { RequestEntry } from '../data/request.reducer'; import { RemoteData } from '../data/remote-data'; import { PageInfo } from '../shared/page-info.model'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; -import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service'; import { RegistryBitstreamformatsSuccessResponse, RegistryMetadatafieldsSuccessResponse, RegistryMetadataschemasSuccessResponse -} from '../cache/response-cache.models'; +} from '../cache/response.models'; import { Component } from '@angular/core'; import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model'; import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model'; @@ -146,7 +143,6 @@ describe('RegistryService', () => { DummyComponent ], providers: [ - { provide: ResponseCacheService, useValue: getMockResponseCacheService() }, { provide: RequestService, useValue: getMockRequestService() }, { provide: RemoteDataBuildService, useValue: rdbStub }, { provide: HALEndpointService, useValue: halServiceStub }, diff --git a/src/app/core/registry/registry.service.ts b/src/app/core/registry/registry.service.ts index 7e7c18f69e..ef92d42ce9 100644 --- a/src/app/core/registry/registry.service.ts +++ b/src/app/core/registry/registry.service.ts @@ -6,29 +6,29 @@ import { PageInfo } from '../shared/page-info.model'; import { MetadataSchema } from '../metadata/metadataschema.model'; import { MetadataField } from '../metadata/metadatafield.model'; import { BitstreamFormat } from './mock-bitstream-format.model'; -import { flatMap, map, tap } from 'rxjs/operators'; +import { filter, flatMap, map, tap } from 'rxjs/operators'; import { GetRequest, RestRequest } from '../data/request.models'; import { GenericConstructor } from '../shared/generic-constructor'; import { ResponseParsingService } from '../data/parsing.service'; import { RegistryMetadataschemasResponseParsingService } from '../data/registry-metadataschemas-response-parsing.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RequestService } from '../data/request.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model'; -import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { RegistryBitstreamformatsSuccessResponse, RegistryMetadatafieldsSuccessResponse, RegistryMetadataschemasSuccessResponse -} from '../cache/response-cache.models'; +} from '../cache/response.models'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RegistryMetadatafieldsResponseParsingService } from '../data/registry-metadatafields-response-parsing.service'; import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model'; -import { isNotEmpty } from '../../shared/empty.util'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { URLCombiner } from '../url-combiner/url-combiner'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { RegistryBitstreamformatsResponseParsingService } from '../data/registry-bitstreamformats-response-parsing.service'; import { RegistryBitstreamformatsResponse } from './registry-bitstreamformats-response.model'; +import { RequestEntry } from '../data/request.reducer'; +import { getResponseFromEntry } from '../shared/operators'; @Injectable() export class RegistryService { @@ -37,8 +37,7 @@ export class RegistryService { private metadataFieldsPath = 'metadatafields'; private bitstreamFormatsPath = 'bitstreamformats'; - constructor(protected responseCache: ResponseCacheService, - protected requestService: RequestService, + constructor(protected requestService: RequestService, private rdb: RemoteDataBuildService, private halService: HALEndpointService) { @@ -51,12 +50,8 @@ export class RegistryService { flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) ); - const responseCacheObs = requestObs.pipe( - flatMap((request: RestRequest) => this.responseCache.get(request.href)) - ); - - const rmrObs: Observable = responseCacheObs.pipe( - map((entry: ResponseCacheEntry) => entry.response), + const rmrObs: Observable = requestEntryObs.pipe( + getResponseFromEntry(), map((response: RegistryMetadataschemasSuccessResponse) => response.metadataschemasResponse) ); @@ -64,8 +59,8 @@ export class RegistryService { map((rmr: RegistryMetadataschemasResponse) => rmr.metadataschemas) ); - const pageInfoObs: Observable = responseCacheObs.pipe( - map((entry: ResponseCacheEntry) => entry.response), + const pageInfoObs: Observable = requestEntryObs.pipe( + getResponseFromEntry(), map((response: RegistryMetadataschemasSuccessResponse) => response.pageInfo) ); @@ -75,7 +70,7 @@ export class RegistryService { }) ); - return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); + return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs); } public getMetadataSchemaByName(schemaName: string): Observable> { @@ -90,12 +85,8 @@ export class RegistryService { flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) ); - const responseCacheObs = requestObs.pipe( - flatMap((request: RestRequest) => this.responseCache.get(request.href)) - ); - - const rmrObs: Observable = responseCacheObs.pipe( - map((entry: ResponseCacheEntry) => entry.response), + const rmrObs: Observable = requestEntryObs.pipe( + getResponseFromEntry(), map((response: RegistryMetadataschemasSuccessResponse) => response.metadataschemasResponse) ); @@ -104,7 +95,7 @@ export class RegistryService { map((metadataSchemas: MetadataSchema[]) => metadataSchemas.filter((value) => value.prefix === schemaName)[0]) ); - return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, metadataschemaObs); + return this.rdb.toRemoteDataObservable(requestEntryObs, metadataschemaObs); } public getMetadataFieldsBySchema(schema: MetadataSchema, pagination: PaginationComponentOptions): Observable>> { @@ -114,12 +105,8 @@ export class RegistryService { flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) ); - const responseCacheObs = requestObs.pipe( - flatMap((request: RestRequest) => this.responseCache.get(request.href)) - ); - - const rmrObs: Observable = responseCacheObs.pipe( - map((entry: ResponseCacheEntry) => entry.response), + const rmrObs: Observable = requestEntryObs.pipe( + getResponseFromEntry(), map((response: RegistryMetadatafieldsSuccessResponse) => response.metadatafieldsResponse) ); @@ -128,8 +115,9 @@ export class RegistryService { map((metadataFields: MetadataField[]) => metadataFields.filter((field) => field.schema.id === schema.id)) ); - const pageInfoObs: Observable = responseCacheObs.pipe( - map((entry: ResponseCacheEntry) => entry.response), + const pageInfoObs: Observable = requestEntryObs.pipe( + getResponseFromEntry(), + map((response: RegistryMetadatafieldsSuccessResponse) => response.pageInfo) ); @@ -139,7 +127,7 @@ export class RegistryService { }) ); - return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); + return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs); } public getBitstreamFormats(pagination: PaginationComponentOptions): Observable>> { @@ -149,12 +137,8 @@ export class RegistryService { flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) ); - const responseCacheObs = requestObs.pipe( - flatMap((request: RestRequest) => this.responseCache.get(request.href)) - ); - - const rbrObs: Observable = responseCacheObs.pipe( - map((entry: ResponseCacheEntry) => entry.response), + const rbrObs: Observable = requestEntryObs.pipe( + getResponseFromEntry(), map((response: RegistryBitstreamformatsSuccessResponse) => response.bitstreamformatsResponse) ); @@ -162,8 +146,8 @@ export class RegistryService { map((rbr: RegistryBitstreamformatsResponse) => rbr.bitstreamformats) ); - const pageInfoObs: Observable = responseCacheObs.pipe( - map((entry: ResponseCacheEntry) => entry.response), + const pageInfoObs: Observable = requestEntryObs.pipe( + getResponseFromEntry(), map((response: RegistryBitstreamformatsSuccessResponse) => response.pageInfo) ); @@ -173,7 +157,7 @@ export class RegistryService { }) ); - return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); + return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs); } private getMetadataSchemasRequestObs(pagination: PaginationComponentOptions): Observable { diff --git a/src/app/core/shared/hal-endpoint.service.spec.ts b/src/app/core/shared/hal-endpoint.service.spec.ts index 0c2afe938b..d36da207ca 100644 --- a/src/app/core/shared/hal-endpoint.service.spec.ts +++ b/src/app/core/shared/hal-endpoint.service.spec.ts @@ -1,14 +1,12 @@ import { cold, hot } from 'jasmine-marbles'; import { GlobalConfig } from '../../../config/global-config.interface'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from './hal-endpoint.service'; import { EndpointMapRequest } from '../data/request.models'; describe('HALEndpointService', () => { let service: HALEndpointService; - let responseCache: ResponseCacheService; let requestService: RequestService; let envConfig: GlobalConfig; @@ -19,14 +17,6 @@ describe('HALEndpointService', () => { describe('getRootEndpointMap', () => { beforeEach(() => { - responseCache = jasmine.createSpyObj('responseCache', { - get: hot('a-', { - a: { - response: { endpointMap: endpointMap } - } - }) - }); - requestService = getMockRequestService(); envConfig = { @@ -34,7 +24,6 @@ describe('HALEndpointService', () => { } as any; service = new HALEndpointService( - responseCache, requestService, envConfig ); @@ -60,12 +49,6 @@ describe('HALEndpointService', () => { envConfig = { rest: { baseUrl: 'https://rest.api/' } } as any; - - service = new HALEndpointService( - responseCache, - requestService, - envConfig - ); }); it('should return the endpoint URL for the service\'s linkPath', () => { @@ -89,7 +72,6 @@ describe('HALEndpointService', () => { describe('isEnabledOnRestApi', () => { beforeEach(() => { service = new HALEndpointService( - responseCache, requestService, envConfig ); diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index d72b9e9a9f..8ef65c4dd1 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -1,21 +1,27 @@ -import {of as observableOf, Observable } from 'rxjs'; -import {filter, distinctUntilChanged, map, flatMap, startWith, tap } from 'rxjs/operators'; +import { Observable, of as observableOf } from 'rxjs'; +import { + distinctUntilChanged, + filter, + flatMap, + map, + startWith, + switchMap, + tap +} from 'rxjs/operators'; import { RequestService } from '../data/request.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; import { GlobalConfig } from '../../../config/global-config.interface'; -import { EndpointMap, EndpointMapSuccessResponse } from '../cache/response-cache.models'; import { EndpointMapRequest } from '../data/request.models'; -import { ResponseCacheEntry } from '../cache/response-cache.reducer'; -import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; import { Inject, Injectable } from '@angular/core'; import { GLOBAL_CONFIG } from '../../../config'; +import { EndpointMap, EndpointMapSuccessResponse } from '../cache/response.models'; +import { getResponseFromEntry } from './operators'; @Injectable() export class HALEndpointService { - constructor(private responseCache: ResponseCacheService, - private requestService: RequestService, + constructor(private requestService: RequestService, @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) { } @@ -29,12 +35,22 @@ export class HALEndpointService { private getEndpointMapAt(href): Observable { const request = new EndpointMapRequest(this.requestService.generateRequestId(), href); - this.requestService.configure(request); - return this.responseCache.get(request.href).pipe( - map((entry: ResponseCacheEntry) => entry.response), - filter((response: EndpointMapSuccessResponse) => isNotEmpty(response)), + + this.requestService.getByUUID(request.uuid).pipe( + getResponseFromEntry(), map((response: EndpointMapSuccessResponse) => response.endpointMap), - distinctUntilChanged(),); + distinctUntilChanged()).subscribe((t) => console.log('uuid', t)); + this.requestService.getByHref(request.href).pipe( + getResponseFromEntry(), + map((response: EndpointMapSuccessResponse) => response.endpointMap), + distinctUntilChanged()).subscribe((t) => console.log('href', t)); + + this.requestService.configure(request); + return this.requestService.getByHref(request.href).pipe( /*<-- changing this to UUID breaks it */ + getResponseFromEntry(), + map((response: EndpointMapSuccessResponse) => response.endpointMap), + distinctUntilChanged()); + } public getEndpoint(linkPath: string): Observable { @@ -48,7 +64,7 @@ export class HALEndpointService { let currentPath; const pipeArguments = path .map((subPath: string, index: number) => [ - flatMap((href: string) => this.getEndpointMapAt(href)), + switchMap((href: string) => this.getEndpointMapAt(href)), map((endpointMap: EndpointMap) => { if (hasValue(endpointMap) && hasValue(endpointMap[subPath])) { currentPath = endpointMap[subPath]; diff --git a/src/app/core/shared/operators.spec.ts b/src/app/core/shared/operators.spec.ts index 16bf633705..0684308fe9 100644 --- a/src/app/core/shared/operators.spec.ts +++ b/src/app/core/shared/operators.spec.ts @@ -1,17 +1,15 @@ import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; -import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service'; -import { ResponseCacheEntry } from '../cache/response-cache.reducer'; -import { ResponseCacheService } from '../cache/response-cache.service'; -import { GetRequest, RestRequest } from '../data/request.models'; +import { GetRequest } from '../data/request.models'; import { RequestEntry } from '../data/request.reducer'; import { RequestService } from '../data/request.service'; import { configureRequest, - filterSuccessfulResponses, getRemoteDataPayload, - getRequestFromSelflink, getResourceLinksFromResponse, - getResponseFromSelflink + filterSuccessfulResponses, + getRemoteDataPayload, + getRequestFromSelflink, + getResourceLinksFromResponse, } from './operators'; describe('Core Module - RxJS Operators', () => { @@ -64,44 +62,6 @@ describe('Core Module - RxJS Operators', () => { }); }); - describe('getResponseFromSelflink', () => { - let responseCacheService: ResponseCacheService; - - beforeEach(() => { - scheduler = getTestScheduler(); - }); - - it('should return the ResponseCacheEntry corresponding to the self link in the source', () => { - responseCacheService = getMockResponseCacheService(); - - const source = hot('a', { a: testSelfLink }); - const result = source.pipe(getResponseFromSelflink(responseCacheService)); - const expected = cold('a', { a: new ResponseCacheEntry()}); - - expect(result).toBeObservable(expected) - }); - - it('should use the responseCacheService to fetch the response by the request\'s link', () => { - responseCacheService = getMockResponseCacheService(); - - const source = hot('a', { a: testSelfLink }); - scheduler.schedule(() => source.pipe(getResponseFromSelflink(responseCacheService)).subscribe()); - scheduler.flush(); - - expect(responseCacheService.get).toHaveBeenCalledWith(testSelfLink) - }); - - it('shouldn\'t return anything if there is no response matching the request\'s link', () => { - responseCacheService = getMockResponseCacheService(undefined, cold('a', { a: undefined })); - - const source = hot('a', { a: testSelfLink }); - const result = source.pipe(getResponseFromSelflink(responseCacheService)); - const expected = cold('-'); - - expect(result).toBeObservable(expected) - }); - }); - describe('filterSuccessfulResponses', () => { it('should only return responses for which isSuccessful === true', () => { const source = hot('abcde', testRCEs); diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index 476119399b..e9ab8794ff 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -1,9 +1,7 @@ import { Observable } from 'rxjs'; import { filter, first, flatMap, map, tap } from 'rxjs/operators'; -import { hasValueOperator, isNotEmpty } from '../../shared/empty.util'; -import { DSOSuccessResponse } from '../cache/response-cache.models'; -import { ResponseCacheEntry } from '../cache/response-cache.reducer'; -import { ResponseCacheService } from '../cache/response-cache.service'; +import { hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util'; +import { DSOSuccessResponse, RestResponse } from '../cache/response.models'; import { RemoteData } from '../data/remote-data'; import { RestRequest } from '../data/request.models'; import { RequestEntry } from '../data/request.reducer'; @@ -24,22 +22,25 @@ export const getRequestFromSelflink = (requestService: RequestService) => hasValueOperator() ); -export const getResponseFromSelflink = (responseCache: ResponseCacheService) => - (source: Observable): Observable => +export const filterSuccessfulResponses = () => + (source: Observable): Observable => source.pipe( - flatMap((href: string) => responseCache.get(href)), - hasValueOperator() + getResponseFromEntry(), + filter((response: RestResponse) => response.isSuccessful === true), ); -export const filterSuccessfulResponses = () => - (source: Observable): Observable => - source.pipe(filter((entry: ResponseCacheEntry) => entry.response.isSuccessful === true)); +export const getResponseFromEntry = () => + (source: Observable): Observable => + source.pipe( + filter((entry: RequestEntry) => hasValue(entry) && hasValue(entry.response)), + map((entry: RequestEntry) => entry.response) + ); export const getResourceLinksFromResponse = () => - (source: Observable): Observable => + (source: Observable): Observable => source.pipe( filterSuccessfulResponses(), - map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks), + map((response: DSOSuccessResponse) => response.resourceSelfLinks), ); export const configureRequest = (requestService: RequestService) => @@ -60,7 +61,7 @@ export const toDSpaceObjectListRD = () => map((rd: RemoteData>>) => { const dsoPage: T[] = rd.payload.page.map((searchResult: SearchResult) => searchResult.dspaceObject); const payload = Object.assign(rd.payload, { page: dsoPage }) as PaginatedList; - return Object.assign(rd, {payload: payload}); + return Object.assign(rd, { payload: payload }); }) ); diff --git a/src/app/shared/mocks/mock-remote-data-build.service.ts b/src/app/shared/mocks/mock-remote-data-build.service.ts index ea263239ea..675e539d90 100644 --- a/src/app/shared/mocks/mock-remote-data-build.service.ts +++ b/src/app/shared/mocks/mock-remote-data-build.service.ts @@ -1,14 +1,13 @@ import {of as observableOf, Observable } from 'rxjs'; import { map, take } from 'rxjs/operators'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; -import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; import { RemoteData } from '../../core/data/remote-data'; import { RequestEntry } from '../../core/data/request.reducer'; import { hasValue } from '../empty.util'; export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observable>): RemoteDataBuildService { return { - toRemoteDataObservable: (requestEntry$: Observable, responseCache$: Observable, payload$: Observable) => { + toRemoteDataObservable: (requestEntry$: Observable, payload$: Observable) => { if (hasValue(toRemoteDataObservable$)) { return toRemoteDataObservable$; diff --git a/src/app/shared/mocks/mock-response-cache.service.ts b/src/app/shared/mocks/mock-response-cache.service.ts index a5a999873d..e69de29bb2 100644 --- a/src/app/shared/mocks/mock-response-cache.service.ts +++ b/src/app/shared/mocks/mock-response-cache.service.ts @@ -1,16 +0,0 @@ -import {of as observableOf, Observable } from 'rxjs'; -import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; -import { ResponseCacheService } from '../../core/cache/response-cache.service'; - -export function getMockResponseCacheService( - add$: Observable = observableOf(new ResponseCacheEntry()), - get$: Observable = observableOf(new ResponseCacheEntry()), - has: boolean = false -): ResponseCacheService { - return jasmine.createSpyObj('ResponseCacheService', { - add: add$, - get: get$, - has, - }); - -} diff --git a/src/server.ts b/src/server.ts index 0e68e4f139..13d0b2fd89 100644 --- a/src/server.ts +++ b/src/server.ts @@ -53,6 +53,7 @@ export function startServer(bootstrap: Type<{}> | NgModuleFactory<{}>) { function ngApp(req, res) { function onHandleError(parentZoneDelegate, currentZone, targetZone, error) { + console.error('Error:', error); console.warn('Error in SSR, serving for direct CSR'); res.sendFile('index.csr.html', { root: './src' }); } From ec5f977dd2de4139a0916c62ab1b4d290e0db379 Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 18 Oct 2018 15:59:41 +0200 Subject: [PATCH 46/69] finished up response cache refactoring --- .../collection-page.component.ts | 4 +- .../search-service/search.service.spec.ts | 13 +--- .../search-service/search.service.ts | 16 ++--- .../auth-response-parsing.service.spec.ts | 3 +- src/app/core/browse/browse.service.spec.ts | 16 +++-- src/app/core/browse/browse.service.ts | 5 +- .../builders/remote-data-build.service.ts | 23 +++---- src/app/core/data/comcol-data.service.spec.ts | 18 +++-- src/app/core/data/request.reducer.spec.ts | 38 +++++++++-- src/app/core/data/request.reducer.ts | 8 +-- src/app/core/data/request.service.spec.ts | 20 +++--- src/app/core/data/request.service.ts | 5 +- .../core/registry/registry.service.spec.ts | 38 ++++------- .../core/shared/hal-endpoint.service.spec.ts | 41 +++++++----- src/app/core/shared/hal-endpoint.service.ts | 65 +++++++++---------- src/app/core/shared/operators.spec.ts | 10 ++- src/app/core/shared/operators.ts | 1 + 17 files changed, 174 insertions(+), 150 deletions(-) diff --git a/src/app/+collection-page/collection-page.component.ts b/src/app/+collection-page/collection-page.component.ts index 047613d6d1..b76c0a7520 100644 --- a/src/app/+collection-page/collection-page.component.ts +++ b/src/app/+collection-page/collection-page.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; 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 { CollectionDataService } from '../core/data/collection-data.service'; 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 { hasValue, isNotEmpty } from '../shared/empty.util'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; -import { filter, first, flatMap, map } from 'rxjs/operators'; +import { filter, flatMap, map, tap } from 'rxjs/operators'; import { SearchService } from '../+search-page/search-service/search.service'; import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model'; import { toDSpaceObjectListRD } from '../core/shared/operators'; diff --git a/src/app/+search-page/search-service/search.service.spec.ts b/src/app/+search-page/search-service/search.service.spec.ts index 028a3fa446..4af0ffcb2e 100644 --- a/src/app/+search-page/search-service/search.service.spec.ts +++ b/src/app/+search-page/search-service/search.service.spec.ts @@ -84,8 +84,8 @@ describe('SearchService', () => { const remoteDataBuildService = { toRemoteDataObservable: (requestEntryObs: Observable, payloadObs: Observable) => { return observableCombineLatest(requestEntryObs, payloadObs).pipe( - map(([req, res, pay]) => { - return { req, res, pay }; + map(([req, pay]) => { + return { req, pay }; }) ); }, @@ -175,9 +175,6 @@ describe('SearchService', () => { it('should call getByHref on the request service with the correct request url', () => { 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', () => { @@ -203,9 +200,6 @@ describe('SearchService', () => { it('should call getByHref on the request service with the correct request url', () => { 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', () => { @@ -233,9 +227,6 @@ describe('SearchService', () => { it('should call getByHref on the request service with the correct request url', () => { 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); - }); }); }); }); diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index c628fd350e..275b0b3340 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -7,7 +7,7 @@ import { Router, UrlSegmentGroup } from '@angular/router'; -import { filter, flatMap, map, switchMap } from 'rxjs/operators'; +import { map, switchMap, tap } from 'rxjs/operators'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { FacetConfigSuccessResponse, @@ -47,7 +47,6 @@ import { CommunityDataService } from '../../core/data/community-data.service'; import { ViewMode } from '../../core/shared/view-mode.model'; import { ResourceType } from '../../core/shared/resource-type'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; -import { RequestEntry } from '../../core/data/request.reducer'; /** * Service that performs all general actions that have to do with the search page @@ -100,7 +99,7 @@ export class SearchService implements OnDestroy { configureRequest(this.requestService) ); const requestEntryObs = requestObs.pipe( - flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) + switchMap((request: RestRequest) => this.requestService.getByHref(request.href)) ); // get search results from response cache @@ -113,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 const dsoObs: Observable> = sqrObs.pipe( map((sqr: SearchQueryResponse) => { - return sqr.objects.map((nsr: NormalizedSearchResult) => - this.rdb.buildSingle(nsr.dspaceObject)); + return sqr.objects.map((nsr: NormalizedSearchResult) => { + return this.rdb.buildSingle(nsr.dspaceObject); + }) }), - flatMap((input: Array>>) => this.rdb.aggregate(input)) + switchMap((input: Array>>) => this.rdb.aggregate(input)), ); // Create search results again with the correct dso objects linked to each result @@ -180,7 +180,7 @@ export class SearchService implements OnDestroy { ); const requestEntryObs = requestObs.pipe( - flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) + switchMap((request: RestRequest) => this.requestService.getByHref(request.href)) ); // get search results from response cache @@ -223,7 +223,7 @@ export class SearchService implements OnDestroy { ); const requestEntryObs = requestObs.pipe( - flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) + switchMap((request: RestRequest) => this.requestService.getByHref(request.href)) ); // get search results from response cache diff --git a/src/app/core/auth/auth-response-parsing.service.spec.ts b/src/app/core/auth/auth-response-parsing.service.spec.ts index e23e3ac56b..a4131db489 100644 --- a/src/app/core/auth/auth-response-parsing.service.spec.ts +++ b/src/app/core/auth/auth-response-parsing.service.spec.ts @@ -1,7 +1,6 @@ import { AuthStatusResponse } from '../cache/response.models'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { AuthStatus } from './models/auth-status.model'; import { AuthResponseParsingService } from './auth-response-parsing.service'; import { AuthGetRequest, AuthPostRequest } from '../data/request.models'; @@ -11,7 +10,7 @@ import { ObjectCacheState } from '../cache/object-cache.reducer'; describe('AuthResponseParsingService', () => { let service: AuthResponseParsingService; - const EnvConfig = { cache: { msToLive: 1000 } } as GlobalConfig; + const EnvConfig = { cache: { msToLive: 1000 } } as any; const store = new MockStore({}); const objectCacheService = new ObjectCacheService(store as any); diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index 4465eae1ee..da75e1a877 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -8,6 +8,8 @@ import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from import { RequestService } from '../data/request.service'; import { BrowseDefinition } from '../shared/browse-definition.model'; import { BrowseService } from './browse.service'; +import { RequestEntry } from '../data/request.reducer'; +import { of as observableOf } from 'rxjs'; describe('BrowseService', () => { let scheduler: TestScheduler; @@ -76,7 +78,11 @@ describe('BrowseService', () => { }) ]; - + const getRequestEntry$ = (successful: boolean) => { + return observableOf({ + response: { isSuccessful: successful, payload: browseDefinitions } as any + } as RequestEntry) + }; function initTestService() { return new BrowseService( @@ -93,7 +99,7 @@ describe('BrowseService', () => { describe('getBrowseDefinitions', () => { beforeEach(() => { - requestService = getMockRequestService(); + requestService = getMockRequestService(getRequestEntry$(true)); rdbService = getMockRemoteDataBuildService(); service = initTestService(); spyOn(halService, 'getEndpoint').and @@ -131,7 +137,7 @@ describe('BrowseService', () => { const mockAuthorName = 'Donald Smith'; beforeEach(() => { - requestService = getMockRequestService(); + requestService = getMockRequestService(getRequestEntry$(true)); rdbService = getMockRemoteDataBuildService(); service = initTestService(); spyOn(service, 'getBrowseDefinitions').and @@ -204,7 +210,7 @@ describe('BrowseService', () => { describe('if getBrowseDefinitions fires', () => { beforeEach(() => { - requestService = getMockRequestService(); + requestService = getMockRequestService(getRequestEntry$(true)); rdbService = getMockRemoteDataBuildService(); service = initTestService(); spyOn(service, 'getBrowseDefinitions').and @@ -259,7 +265,7 @@ describe('BrowseService', () => { describe('if getBrowseDefinitions doesn\'t fire', () => { it('should return undefined', () => { - requestService = getMockRequestService(); + requestService = getMockRequestService(getRequestEntry$(true)); rdbService = getMockRemoteDataBuildService(); service = initTestService(); spyOn(service, 'getBrowseDefinitions').and diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index c1c893ec27..ca56c0d267 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, filter, map, startWith, tap } from 'rxjs/operators'; +import { distinctUntilChanged, map, startWith } from 'rxjs/operators'; import { - ensureArrayHasValue, hasValue, + ensureArrayHasValue, hasValueOperator, isEmpty, isNotEmpty, @@ -34,7 +34,6 @@ import { import { URLCombiner } from '../url-combiner/url-combiner'; import { Item } from '../shared/item.model'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { RequestEntry } from '../data/request.reducer'; @Injectable() export class BrowseService { diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 27b5ddf50d..ad5707bbf6 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -5,7 +5,7 @@ import { race as observableRace } from 'rxjs'; import { Injectable } from '@angular/core'; -import { distinctUntilChanged, flatMap, map, startWith, switchMap, tap } from 'rxjs/operators'; +import { distinctUntilChanged, first, flatMap, map, startWith, switchMap } from 'rxjs/operators'; import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util'; import { PaginatedList } from '../../data/paginated-list'; import { RemoteData } from '../../data/remote-data'; @@ -32,6 +32,8 @@ export class RemoteDataBuildService { } buildSingle(href$: string | Observable): Observable> { + console.log('call buildSingle', href$); + if (typeof href$ === 'string') { href$ = observableOf(href$); } @@ -42,19 +44,18 @@ export class RemoteDataBuildService { const requestEntry$ = observableRace( href$.pipe(getRequestFromSelflink(this.requestService)), - requestHref$.pipe(getRequestFromSelflink(this.requestService)) - ); + requestHref$.pipe(getRequestFromSelflink(this.requestService)), + ).pipe(first()); // always use self link if that is cached, only if it isn't, get it via the response. const payload$ = observableCombineLatest( href$.pipe( - flatMap((href: string) => this.objectCache.getBySelfLink(href)), - startWith(undefined) - ), + switchMap((href: string) => this.objectCache.getBySelfLink(href)), + startWith(undefined)), requestEntry$.pipe( getResourceLinksFromResponse(), - flatMap((resourceSelfLinks: string[]) => { + switchMap((resourceSelfLinks: string[]) => { if (isNotEmpty(resourceSelfLinks)) { return this.objectCache.getBySelfLink(resourceSelfLinks[0]); } else { @@ -83,7 +84,7 @@ export class RemoteDataBuildService { } toRemoteDataObservable(requestEntry$: Observable, payload$: Observable) { - return observableCombineLatest(requestEntry$, requestEntry$.pipe(startWith(undefined)), payload$).pipe( + return observableCombineLatest(requestEntry$, payload$).pipe( map(([reqEntry, payload]) => { const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true; const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false; @@ -124,9 +125,8 @@ export class RemoteDataBuildService { })); }), startWith([]), - distinctUntilChanged() + distinctUntilChanged(), ); - // tDomainList$.subscribe((t) => {console.log('domainlist', t)}); const pageInfo$ = requestEntry$.pipe( filterSuccessfulResponses(), map((response: DSOSuccessResponse) => { @@ -152,7 +152,6 @@ export class RemoteDataBuildService { build(normalized: TNormalized): TDomain { const links: any = {}; - const relationships = getRelationships(normalized.constructor) || []; relationships.forEach((relationship: string) => { @@ -196,8 +195,6 @@ export class RemoteDataBuildService { } }); const domainModel = getMapsTo(normalized.constructor); - // console.log('domain model', normalized); - return Object.assign(new domainModel(), normalized, links); } diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index 2125aff797..867d559c70 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -12,6 +12,8 @@ import { FindAllOptions, FindByIDRequest } from './request.models'; import { RequestService } from './request.service'; import { NormalizedObject } from '../cache/models/normalized-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { RequestEntry } from './request.reducer'; +import { of as observableOf } from 'rxjs'; const LINK_NAME = 'test'; @@ -34,6 +36,7 @@ class TestService extends ComColDataService { super(); } } + /* tslint:enable:max-classes-per-file */ describe('ComColDataService', () => { @@ -52,6 +55,11 @@ describe('ComColDataService', () => { const options = Object.assign(new FindAllOptions(), { scopeID: scopeID }); + const getRequestEntry$ = (successful: boolean) => { + return observableOf({ + response: { isSuccessful: successful } as any + } as RequestEntry) + }; const communitiesEndpoint = 'https://rest.api/core/communities'; const communityEndpoint = `${communitiesEndpoint}/${scopeID}`; @@ -97,7 +105,7 @@ describe('ComColDataService', () => { it('should configure a new FindByIDRequest for the scope Community', () => { cds = initMockCommunityDataService(); - requestService = getMockRequestService(); + requestService = getMockRequestService(getRequestEntry$(true)); objectCache = initMockObjectCacheService(); service = initTestService(); @@ -112,7 +120,7 @@ describe('ComColDataService', () => { describe('if the scope Community can be found', () => { beforeEach(() => { cds = initMockCommunityDataService(); - requestService = getMockRequestService(); + requestService = getMockRequestService(getRequestEntry$(true)); objectCache = initMockObjectCacheService(); service = initTestService(); }); @@ -125,16 +133,16 @@ describe('ComColDataService', () => { it('should return the endpoint to fetch resources within the given scope', () => { const result = service.getBrowseEndpoint(options); - const expected = cold('--e-', { e: scopedEndpoint }); + const expected = '--e-'; - expect(result).toBeObservable(expected); + scheduler.expectObservable(result).toBe(expected, { e: scopedEndpoint }); }); }); describe('if the scope Community can\'t be found', () => { beforeEach(() => { cds = initMockCommunityDataService(); - requestService = getMockRequestService(); + requestService = getMockRequestService(getRequestEntry$(false)); objectCache = initMockObjectCacheService(); service = initTestService(); }); diff --git a/src/app/core/data/request.reducer.spec.ts b/src/app/core/data/request.reducer.spec.ts index bd8fad5de7..57fbb01ce1 100644 --- a/src/app/core/data/request.reducer.spec.ts +++ b/src/app/core/data/request.reducer.spec.ts @@ -2,16 +2,20 @@ import * as deepFreeze from 'deep-freeze'; import { requestReducer, RequestState } from './request.reducer'; import { - RequestCompleteAction, RequestConfigureAction, RequestExecuteAction + RequestCompleteAction, + RequestConfigureAction, + RequestExecuteAction, ResetResponseTimestampsAction } from './request.actions'; -import { GetRequest, RestRequest } from './request.models'; +import { GetRequest } from './request.models'; +import { RestResponse } from '../cache/response.models'; +const response = new RestResponse(true, 'OK'); class NullAction extends RequestCompleteAction { type = null; payload = null; constructor() { - super(null); + super(null, null); } } @@ -25,7 +29,8 @@ describe('requestReducer', () => { request: new GetRequest(id1, link1), requestPending: false, responsePending: false, - completed: false + completed: false, + response: undefined } }; deepFreeze(testState); @@ -56,6 +61,7 @@ describe('requestReducer', () => { expect(newState[id2].requestPending).toEqual(true); expect(newState[id2].responsePending).toEqual(false); expect(newState[id2].completed).toEqual(false); + expect(newState[id2].response).toEqual(undefined); }); it('should set \'requestPending\' to false, \'responsePending\' to true and leave \'completed\' untouched for the given RestRequest in the state, in response to an EXECUTE action', () => { @@ -69,11 +75,13 @@ describe('requestReducer', () => { expect(newState[id1].requestPending).toEqual(false); expect(newState[id1].responsePending).toEqual(true); expect(newState[id1].completed).toEqual(state[id1].completed); + expect(newState[id1].response).toEqual(undefined) }); + it('should leave \'requestPending\' untouched, set \'responsePending\' to false and \'completed\' to true for the given RestRequest in the state, in response to a COMPLETE action', () => { const state = testState; - const action = new RequestCompleteAction(id1); + const action = new RequestCompleteAction(id1, response); const newState = requestReducer(state, action); expect(newState[id1].request.uuid).toEqual(id1); @@ -81,5 +89,25 @@ describe('requestReducer', () => { expect(newState[id1].requestPending).toEqual(state[id1].requestPending); expect(newState[id1].responsePending).toEqual(false); expect(newState[id1].completed).toEqual(true); + expect(newState[id1].response.isSuccessful).toEqual(response.isSuccessful) + expect(newState[id1].response.statusCode).toEqual(response.statusCode) + expect(newState[id1].response.timeAdded).toBeTruthy() + }); + + it('should leave \'requestPending\' untouched, should leave \'responsePending\' untouched and leave \'completed\' untouched, but update the response\'s timeAdded for the given RestRequest in the state, in response to a COMPLETE action', () => { + const update = Object.assign({}, testState[id1], {response}); + const state = Object.assign({}, testState, {[id1]: update}); + const timeStamp = 1000; + const action = new ResetResponseTimestampsAction(timeStamp); + const newState = requestReducer(state, action); + + expect(newState[id1].request.uuid).toEqual(state[id1].request.uuid); + expect(newState[id1].request.href).toEqual(state[id1].request.href); + expect(newState[id1].requestPending).toEqual(state[id1].requestPending); + expect(newState[id1].responsePending).toEqual(state[id1].responsePending); + expect(newState[id1].completed).toEqual(state[id1].completed); + expect(newState[id1].response.isSuccessful).toEqual(response.isSuccessful); + expect(newState[id1].response.statusCode).toEqual(response.statusCode); + expect(newState[id1].response.timeAdded).toBe(timeStamp); }); }); diff --git a/src/app/core/data/request.reducer.ts b/src/app/core/data/request.reducer.ts index b0875f37b3..a680de2d6b 100644 --- a/src/app/core/data/request.reducer.ts +++ b/src/app/core/data/request.reducer.ts @@ -56,12 +56,13 @@ function configureRequest(state: RequestState, action: RequestConfigureAction): } function executeRequest(state: RequestState, action: RequestExecuteAction): RequestState { - return Object.assign({}, state, { + const obs = Object.assign({}, state, { [action.payload]: Object.assign({}, state[action.payload], { requestPending: false, responsePending: true }) }); + return obs; } /** @@ -76,16 +77,13 @@ function executeRequest(state: RequestState, action: RequestExecuteAction): Requ */ function completeRequest(state: RequestState, action: RequestCompleteAction): RequestState { const time = new Date().getTime(); - - const ob = Object.assign({}, state, { + return Object.assign({}, state, { [action.payload.uuid]: Object.assign({}, state[action.payload.uuid], { responsePending: false, completed: true, response: Object.assign({}, action.payload.response, { timeAdded: time }) }) }); - console.log(ob); - return ob; } function resetResponseTimestamps(state: RequestState, action: ResetResponseTimestampsAction) { diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index 5953d43c9f..5232d7efab 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -46,13 +46,10 @@ describe('RequestService', () => { objectCache = getMockObjectCacheService(); (objectCache.hasBySelfLink as any).and.returnValue(false); - (responseCache.has as any).and.returnValue(false); - (responseCache.get as any).and.returnValue(observableOf(undefined)); - uuidService = getMockUUIDService(); store = new Store(new BehaviorSubject({}), new ActionsSubject(), null); - selectSpy = spyOnProperty(ngrx, 'select') + selectSpy = spyOnProperty(ngrx, 'select'); selectSpy.and.callFake(() => { return () => { return () => cold('a', { a: undefined }); @@ -322,7 +319,7 @@ describe('RequestService', () => { describe('when the request is cached', () => { describe('in the ObjectCache', () => { beforeEach(() => { - (objectCache.hasBySelfLink as any).and.returnValues(true); + (objectCache.hasBySelfLink as any).and.returnValue(true); }); it('should return true', () => { @@ -334,12 +331,13 @@ describe('RequestService', () => { }); describe('in the responseCache', () => { beforeEach(() => { - (responseCache.has as any).and.returnValues(true); + spyOn(serviceAsAny, 'isReusable').and.returnValue(observableOf(true)); + spyOn(serviceAsAny, 'getByHref').and.returnValue(observableOf(undefined)); }); describe('and it\'s a DSOSuccessResponse', () => { beforeEach(() => { - (responseCache.get as any).and.returnValues(observableOf({ + (serviceAsAny.getByHref as any).and.returnValue(observableOf({ response: { isSuccessful: true, resourceSelfLinks: [ @@ -361,6 +359,7 @@ describe('RequestService', () => { }); it('should return false if not all top level links in the response are cached in the object cache', () => { (objectCache.hasBySelfLink as any).and.returnValues(false, true, false); + spyOn(service, 'isPending').and.returnValue(false); const result = serviceAsAny.isCachedOrPending(testGetRequest); const expected = false; @@ -368,11 +367,12 @@ describe('RequestService', () => { expect(result).toEqual(expected); }); }); + describe('and it isn\'t a DSOSuccessResponse', () => { beforeEach(() => { - (objectCache.hasBySelfLink as any).and.returnValues(false); - (responseCache.has as any).and.returnValues(true); - (responseCache.get as any).and.returnValues(observableOf({ + (objectCache.hasBySelfLink as any).and.returnValue(false); + (service as any).isReusable.and.returnValue(observableOf(true)); + (serviceAsAny.getByHref as any).and.returnValue(observableOf({ response: { isSuccessful: true } diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index cc8c9816f8..564b19de75 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -1,5 +1,6 @@ import { merge as observableMerge, Observable, of as observableOf } from 'rxjs'; import { + distinctUntilChanged, filter, find, first, @@ -109,10 +110,10 @@ export class RequestService { map((resourceSelfLinks: string[]) => resourceSelfLinks .every((selfLink) => this.objectCache.hasBySelfLink(selfLink)) )); + const otherSuccessResponses = responses.pipe(filter((response) => response.isSuccessful && !hasValue((response as DSOSuccessResponse).resourceSelfLinks)), map(() => true)); observableMerge(errorResponses, otherSuccessResponses, dsoSuccessResponses).subscribe((c) => isCached = c); - const isPending = this.isPending(request); return isCached || isPending; } @@ -144,7 +145,7 @@ export class RequestService { } /** - * Check whether a ResponseCacheEntry should still be cached + * Check whether a Response should still be cached * * @param entry * the entry to check diff --git a/src/app/core/registry/registry.service.spec.ts b/src/app/core/registry/registry.service.spec.ts index adb2ed8b05..c87597cffc 100644 --- a/src/app/core/registry/registry.service.spec.ts +++ b/src/app/core/registry/registry.service.spec.ts @@ -124,10 +124,10 @@ describe('RegistryService', () => { }; const rdbStub = { - toRemoteDataObservable: (requestEntryObs: Observable, responseCacheObs: Observable, payloadObs: Observable) => { + toRemoteDataObservable: (requestEntryObs: Observable, payloadObs: Observable) => { return observableCombineLatest(requestEntryObs, - responseCacheObs, payloadObs).pipe(map(([req, res, pay]) => { - return { req, res, pay }; + payloadObs).pipe(map(([req, pay]) => { + return { req, pay }; }) ); }, @@ -160,10 +160,10 @@ describe('RegistryService', () => { page: pageInfo }); const response = new RegistryMetadataschemasSuccessResponse(queryResponse, '200', pageInfo); - const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response }); + const responseEntry = Object.assign(new RequestEntry(), { response: response }); beforeEach(() => { - (registryService as any).responseCache.get.and.returnValue(observableOf(responseEntry)); + (registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry)); /* tslint:disable:no-empty */ registryService.getMetadataSchemas(pagination).subscribe((value) => { }); @@ -181,10 +181,6 @@ describe('RegistryService', () => { it('should call getByHref on the request service with the correct request url', () => { expect((registryService as any).requestService.getByHref).toHaveBeenCalledWith(endpointWithParams); }); - - it('should call get on the request service with the correct request url', () => { - expect((registryService as any).responseCache.get).toHaveBeenCalledWith(endpointWithParams); - }); }); describe('when requesting metadataschema by name', () => { @@ -193,10 +189,10 @@ describe('RegistryService', () => { page: pageInfo }); const response = new RegistryMetadataschemasSuccessResponse(queryResponse, '200', pageInfo); - const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response }); + const responseEntry = Object.assign(new RequestEntry(), { response: response }); beforeEach(() => { - (registryService as any).responseCache.get.and.returnValue(observableOf(responseEntry)); + (registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry)); /* tslint:disable:no-empty */ registryService.getMetadataSchemaByName(mockSchemasList[0].prefix).subscribe((value) => { }); @@ -214,10 +210,6 @@ describe('RegistryService', () => { it('should call getByHref on the request service with the correct request url', () => { expect((registryService as any).requestService.getByHref.calls.argsFor(0)[0]).toContain(endpoint); }); - - it('should call get on the request service with the correct request url', () => { - expect((registryService as any).responseCache.get.calls.argsFor(0)[0]).toContain(endpoint); - }); }); describe('when requesting metadatafields', () => { @@ -226,10 +218,10 @@ describe('RegistryService', () => { page: pageInfo }); const response = new RegistryMetadatafieldsSuccessResponse(queryResponse, '200', pageInfo); - const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response }); + const responseEntry = Object.assign(new RequestEntry(), { response: response }); beforeEach(() => { - (registryService as any).responseCache.get.and.returnValue(observableOf(responseEntry)); + (registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry)); /* tslint:disable:no-empty */ registryService.getMetadataFieldsBySchema(mockSchemasList[0], pagination).subscribe((value) => { }); @@ -247,10 +239,6 @@ describe('RegistryService', () => { it('should call getByHref on the request service with the correct request url', () => { expect((registryService as any).requestService.getByHref).toHaveBeenCalledWith(endpointWithParams); }); - - it('should call get on the request service with the correct request url', () => { - expect((registryService as any).responseCache.get).toHaveBeenCalledWith(endpointWithParams); - }); }); describe('when requesting bitstreamformats', () => { @@ -259,10 +247,10 @@ describe('RegistryService', () => { page: pageInfo }); const response = new RegistryBitstreamformatsSuccessResponse(queryResponse, '200', pageInfo); - const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response }); + const responseEntry = Object.assign(new RequestEntry(), { response: response }); beforeEach(() => { - (registryService as any).responseCache.get.and.returnValue(observableOf(responseEntry)); + (registryService as any).requestService.getByHref.and.returnValue(observableOf(responseEntry)); /* tslint:disable:no-empty */ registryService.getBitstreamFormats(pagination).subscribe((value) => { }); @@ -280,9 +268,5 @@ describe('RegistryService', () => { it('should call getByHref on the request service with the correct request url', () => { expect((registryService as any).requestService.getByHref).toHaveBeenCalledWith(endpointWithParams); }); - - it('should call get on the request service with the correct request url', () => { - expect((registryService as any).responseCache.get).toHaveBeenCalledWith(endpointWithParams); - }); }); }); diff --git a/src/app/core/shared/hal-endpoint.service.spec.ts b/src/app/core/shared/hal-endpoint.service.spec.ts index d36da207ca..b65b3f905b 100644 --- a/src/app/core/shared/hal-endpoint.service.spec.ts +++ b/src/app/core/shared/hal-endpoint.service.spec.ts @@ -1,33 +1,44 @@ -import { cold, hot } from 'jasmine-marbles'; +import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { GlobalConfig } from '../../../config/global-config.interface'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from './hal-endpoint.service'; import { EndpointMapRequest } from '../data/request.models'; +import { RequestEntry } from '../data/request.reducer'; +import { of as observableOf } from 'rxjs'; describe('HALEndpointService', () => { let service: HALEndpointService; let requestService: RequestService; let envConfig: GlobalConfig; + let requestEntry; const endpointMap = { test: 'https://rest.api/test', }; const linkPath = 'test'; + beforeEach(() => { + requestEntry = { + request: { responseMsToLive: 1000 } as any, + requestPending: false, + responsePending: false, + completed: true, + response: { endpointMap: endpointMap } as any + } as RequestEntry; + requestService = getMockRequestService(observableOf(requestEntry)); + + envConfig = { + rest: { baseUrl: 'https://rest.api/' } + } as any; + + service = new HALEndpointService( + requestService, + envConfig + ); + }); + describe('getRootEndpointMap', () => { - beforeEach(() => { - requestService = getMockRequestService(); - - envConfig = { - rest: { baseUrl: 'https://rest.api/' } - } as any; - - service = new HALEndpointService( - requestService, - envConfig - ); - }); it('should configure a new EndpointMapRequest', () => { (service as any).getRootEndpointMap(); @@ -37,8 +48,8 @@ describe('HALEndpointService', () => { it('should return an Observable of the endpoint map', () => { const result = (service as any).getRootEndpointMap(); - const expected = cold('b-', { b: endpointMap }); - expect(result).toBeObservable(expected); + const expected = '(b|)'; + getTestScheduler().expectObservable(result).toBe(expected, { b: endpointMap }); }); }); diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index 8ef65c4dd1..3a5da84e13 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -1,9 +1,8 @@ -import { Observable, of as observableOf } from 'rxjs'; +import { Observable, of as observableOf, combineLatest as observableCombineLatest } from 'rxjs'; import { distinctUntilChanged, - filter, - flatMap, map, + mergeMap, startWith, switchMap, tap @@ -11,12 +10,13 @@ import { import { RequestService } from '../data/request.service'; import { GlobalConfig } from '../../../config/global-config.interface'; import { EndpointMapRequest } from '../data/request.models'; -import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; import { Inject, Injectable } from '@angular/core'; import { GLOBAL_CONFIG } from '../../../config'; import { EndpointMap, EndpointMapSuccessResponse } from '../cache/response.models'; import { getResponseFromEntry } from './operators'; +import { URLCombiner } from '../url-combiner/url-combiner'; @Injectable() export class HALEndpointService { @@ -36,48 +36,41 @@ export class HALEndpointService { private getEndpointMapAt(href): Observable { const request = new EndpointMapRequest(this.requestService.generateRequestId(), href); - this.requestService.getByUUID(request.uuid).pipe( - getResponseFromEntry(), - map((response: EndpointMapSuccessResponse) => response.endpointMap), - distinctUntilChanged()).subscribe((t) => console.log('uuid', t)); - this.requestService.getByHref(request.href).pipe( - getResponseFromEntry(), - map((response: EndpointMapSuccessResponse) => response.endpointMap), - distinctUntilChanged()).subscribe((t) => console.log('href', t)); - this.requestService.configure(request); - return this.requestService.getByHref(request.href).pipe( /*<-- changing this to UUID breaks it */ + return this.requestService.getByHref(request.href).pipe( getResponseFromEntry(), map((response: EndpointMapSuccessResponse) => response.endpointMap), distinctUntilChanged()); - } public getEndpoint(linkPath: string): Observable { - return this.getEndpointAt(...linkPath.split('/')); + return this.getEndpointAt(this.getRootHref(), ...linkPath.split('/')); } - private getEndpointAt(...path: string[]): Observable { - if (isEmpty(path)) { - path = ['/']; + private getEndpointAt(href: string, ...halNames: string[]): Observable { + if (isEmpty(halNames)) { + throw new Error('cant\'t fetch the URL without the HAL link names') + } + + const nextHref$ = this.getEndpointMapAt(href).pipe( + map((endpointMap: EndpointMap): string => { + /*TODO remove if/else block once the rest response contains _links for facets*/ + const nextName = halNames[0]; + if (hasValue(endpointMap) && hasValue(endpointMap[nextName])) { + return endpointMap[nextName]; + } else { + return new URLCombiner(href, nextName).toString(); + } + }) + ) as Observable; + + if (halNames.length === 1) { + return nextHref$; + } else { + return nextHref$.pipe( + switchMap((nextHref) => this.getEndpointAt(nextHref, ...halNames.slice(1))) + ); } - let currentPath; - const pipeArguments = path - .map((subPath: string, index: number) => [ - switchMap((href: string) => this.getEndpointMapAt(href)), - map((endpointMap: EndpointMap) => { - if (hasValue(endpointMap) && hasValue(endpointMap[subPath])) { - currentPath = endpointMap[subPath]; - return endpointMap[subPath]; - } else { - /*TODO remove if/else block once the rest response contains _links for facets*/ - currentPath += '/' + subPath; - return currentPath; - } - }), - ]) - .reduce((combined, thisElement) => [...combined, ...thisElement], []); - return observableOf(this.getRootHref()).pipe(...pipeArguments, distinctUntilChanged()); } public isEnabledOnRestApi(linkPath: string): Observable { diff --git a/src/app/core/shared/operators.spec.ts b/src/app/core/shared/operators.spec.ts index 0684308fe9..5f29de9e93 100644 --- a/src/app/core/shared/operators.spec.ts +++ b/src/app/core/shared/operators.spec.ts @@ -25,6 +25,14 @@ describe('Core Module - RxJS Operators', () => { e: { response: { isSuccessful: 1, resourceSelfLinks: [] } } }; + const testResponses = { + a: testRCEs.a.response, + b: testRCEs.b.response, + c: testRCEs.c.response, + d: testRCEs.d.response, + e: testRCEs.e.response + }; + beforeEach(() => { scheduler = getTestScheduler(); }); @@ -66,7 +74,7 @@ describe('Core Module - RxJS Operators', () => { it('should only return responses for which isSuccessful === true', () => { const source = hot('abcde', testRCEs); const result = source.pipe(filterSuccessfulResponses()); - const expected = cold('a--d-', testRCEs); + const expected = cold('a--d-', testResponses); expect(result).toBeObservable(expected) }); diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index e9ab8794ff..a6335ebb5d 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -58,6 +58,7 @@ export const getSucceededRemoteData = () => export const toDSpaceObjectListRD = () => (source: Observable>>>): Observable>> => source.pipe( + filter((rd: RemoteData>>) => rd.hasSucceeded), map((rd: RemoteData>>) => { const dsoPage: T[] = rd.payload.page.map((searchResult: SearchResult) => searchResult.dspaceObject); const payload = Object.assign(rd.payload, { page: dsoPage }) as PaginatedList; From 5c12e2d995711bdea06e75ae1a7800383e233766 Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 19 Oct 2018 16:07:07 +0200 Subject: [PATCH 47/69] added indexing for different UUIDs --- src/app/core/auth/auth-request.service.ts | 3 + src/app/core/auth/auth.service.ts | 1 + .../builders/remote-data-build.service.ts | 16 +++-- src/app/core/data/comcol-data.service.ts | 14 +++- src/app/core/data/data.service.ts | 2 - src/app/core/data/request.effects.ts | 6 +- src/app/core/data/request.reducer.ts | 8 +++ src/app/core/data/request.service.spec.ts | 8 +-- src/app/core/data/request.service.ts | 64 +++++++++++++------ src/app/core/index/index.reducer.spec.ts | 4 ++ src/app/core/index/index.reducer.ts | 14 ++-- src/app/core/shared/hal-endpoint.service.ts | 4 +- 12 files changed, 101 insertions(+), 43 deletions(-) diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index 0752149eae..284f588040 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -24,9 +24,12 @@ export class AuthRequestService { protected fetchRequest(request: RestRequest): Observable { return this.requestService.getByHref(request.href).pipe( + tap((t) => console.log(t)), + getResponseFromEntry(), // TODO to review when https://github.com/DSpace/dspace-angular/issues/217 will be fixed // tap(() => this.responseCache.remove(request.href)), + tap((t) => console.log(t)), mergeMap((response) => { if (response.isSuccessful && isNotEmpty(response)) { return observableOf((response as AuthStatusResponse).response); diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 229c44bcfa..4c520e8f30 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -116,6 +116,7 @@ export class AuthService { options.headers = headers; return this.authRequestService.postToEndpoint('login', body, options).pipe( map((status: AuthStatus) => { + console.log('yey response'); if (status.authenticated) { return status; } else { diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index ad5707bbf6..3877c19ff9 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -5,7 +5,15 @@ import { race as observableRace } from 'rxjs'; import { Injectable } from '@angular/core'; -import { distinctUntilChanged, first, flatMap, map, startWith, switchMap } from 'rxjs/operators'; +import { + distinctUntilChanged, + first, + flatMap, + map, + startWith, + switchMap, + takeUntil, tap +} from 'rxjs/operators'; import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util'; import { PaginatedList } from '../../data/paginated-list'; import { RemoteData } from '../../data/remote-data'; @@ -32,8 +40,6 @@ export class RemoteDataBuildService { } buildSingle(href$: string | Observable): Observable> { - console.log('call buildSingle', href$); - if (typeof href$ === 'string') { href$ = observableOf(href$); } @@ -45,7 +51,9 @@ export class RemoteDataBuildService { const requestEntry$ = observableRace( href$.pipe(getRequestFromSelflink(this.requestService)), requestHref$.pipe(getRequestFromSelflink(this.requestService)), - ).pipe(first()); + ).pipe( + first() + ); // always use self link if that is cached, only if it isn't, get it via the response. const payload$ = diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 95a0015125..63c11dd8cb 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -1,4 +1,13 @@ -import { distinctUntilChanged, filter, map, mergeMap, share, take, tap } from 'rxjs/operators'; +import { + distinctUntilChanged, + filter, + first, + map, + mergeMap, + share, + take, + tap +} from 'rxjs/operators'; import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs'; import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { NormalizedCommunity } from '../cache/models/normalized-community.model'; @@ -59,7 +68,8 @@ export abstract class ComColDataService this.requestService.getByHref(href)), - getResponseFromEntry()); + getResponseFromEntry() + ); const errorResponses = responses.pipe( filter((response) => !response.isSuccessful), mergeMap(() => observableThrowError(new Error(`The Community with scope ${options.scopeID} couldn't be retrieved`))) diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 4a993e4ac6..6a7916854b 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -47,8 +47,6 @@ export abstract class DataService if (isNotEmpty(args)) { return result.pipe(map((href: string) => new URLCombiner(href, `?${args.join('&')}`).toString())); } else { - result.subscribe((t) => console.log(t)); - return result; } } diff --git a/src/app/core/data/request.effects.ts b/src/app/core/data/request.effects.ts index bda91283bf..537a0b69b6 100644 --- a/src/app/core/data/request.effects.ts +++ b/src/app/core/data/request.effects.ts @@ -3,7 +3,7 @@ import { Inject, Injectable, Injector } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; -import { isNotEmpty } from '../../shared/empty.util'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service'; @@ -18,7 +18,7 @@ import { RequestEntry } from './request.reducer'; import { RequestService } from './request.service'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; -import { catchError, flatMap, map, take, tap } from 'rxjs/operators'; +import { catchError, filter, flatMap, map, take, tap } from 'rxjs/operators'; import { ErrorResponse, RestResponse } from '../cache/response.models'; import { StoreActionTypes } from '../../store.actions'; @@ -40,7 +40,9 @@ export class RequestEffects { take(1) ); }), + filter((entry: RequestEntry) => hasValue(entry)), map((entry: RequestEntry) => entry.request), + tap((entry: RequestEntry) => console.log(entry)), flatMap((request: RestRequest) => { let body; if (isNotEmpty(request.body)) { diff --git a/src/app/core/data/request.reducer.ts b/src/app/core/data/request.reducer.ts index a680de2d6b..e6a9097dce 100644 --- a/src/app/core/data/request.reducer.ts +++ b/src/app/core/data/request.reducer.ts @@ -53,6 +53,14 @@ function configureRequest(state: RequestState, action: RequestConfigureAction): completed: false, } }); + console.log(Object.assign({}, state, { + [action.payload.uuid]: { + request: action.payload, + requestPending: true, + responsePending: false, + completed: false, + } + });); } function executeRequest(state: RequestState, action: RequestExecuteAction): RequestState { diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index 5232d7efab..debddb748c 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -170,11 +170,11 @@ describe('RequestService', () => { it('should return an Observable of undefined', () => { const result = service.getByUUID(testUUID); - const expected = cold('b', { - b: undefined - }); + // const expected = cold('b', { + // b: undefined + // }); - expect(result).toBeObservable(expected); + scheduler.expectObservable(result).toBe('b', {b: undefined}); }); }); diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 564b19de75..4b7a7c9b49 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -12,10 +12,11 @@ import { take, tap } from 'rxjs/operators'; +import { race as observableRace } from 'rxjs'; import { Injectable } from '@angular/core'; import { MemoizedSelector, select, Store } from '@ngrx/store'; -import { hasNoValue, hasValue } from '../../shared/empty.util'; +import { hasNoValue, hasValue, isNotUndefined } from '../../shared/empty.util'; import { CacheableObject } from '../cache/object-cache.reducer'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DSOSuccessResponse, RestResponse } from '../cache/response.models'; @@ -30,6 +31,7 @@ import { RequestEntry } from './request.reducer'; import { CommitSSBAction } from '../cache/server-sync-buffer.actions'; import { RestRequestMethod } from './rest-request-method'; import { getResponseFromEntry } from '../shared/operators'; +import { AddToIndexAction } from '../index/index.actions'; @Injectable() export class RequestService { @@ -48,6 +50,10 @@ export class RequestService { return pathSelector(coreSelector, 'index', IndexName.REQUEST, href); } + private originalUUIDFromUUIDSelector(uuid: string): MemoizedSelector { + return pathSelector(coreSelector, 'index', IndexName.UUID_MAPPING, uuid); + } + generateRequestId(): string { return `client/${this.uuidService.generate()}`; } @@ -70,7 +76,15 @@ export class RequestService { } getByUUID(uuid: string): Observable { - return this.store.pipe(select(this.entryFromUUIDSelector(uuid))); + return observableRace( + this.store.pipe(select(this.entryFromUUIDSelector(uuid))), + this.store.pipe( + select(this.originalUUIDFromUUIDSelector(uuid)), + switchMap((originalUUID) => { + return this.store.pipe(select(this.entryFromUUIDSelector(originalUUID))) + }, + )) + ); } getByHref(href: string): Observable { @@ -88,32 +102,42 @@ export class RequestService { if (isGetRequest && !forceBypassCache) { this.trackRequestsOnTheirWayToTheStore(request); } + } else { + this.getByHref(request.href).pipe( + filter((entry) => hasValue(entry)), + take(1) + ).subscribe((entry) => { + return this.store.dispatch(new AddToIndexAction(IndexName.UUID_MAPPING, request.uuid, entry.request.uuid)) + } + ) } } private isCachedOrPending(request: GetRequest) { let isCached = this.objectCache.hasBySelfLink(request.href); - const responses: Observable = this.isReusable(request.uuid).pipe( - filter((reusable: boolean) => !isCached && reusable), - switchMap(() => { - return this.getByHref(request.href).pipe( - getResponseFromEntry(), - take(1) - ); - } - )); + if (isCached) { + const responses: Observable = this.isReusable(request.uuid).pipe( + filter((reusable: boolean) => reusable), + switchMap(() => { + return this.getByHref(request.href).pipe( + getResponseFromEntry(), + take(1) + ); + } + )); - const errorResponses = responses.pipe(filter((response) => !response.isSuccessful), map(() => true)); // TODO add a configurable number of retries in case of an error. - const dsoSuccessResponses = responses.pipe( - filter((response) => response.isSuccessful && hasValue((response as DSOSuccessResponse).resourceSelfLinks)), - map((response: DSOSuccessResponse) => response.resourceSelfLinks), - map((resourceSelfLinks: string[]) => resourceSelfLinks - .every((selfLink) => this.objectCache.hasBySelfLink(selfLink)) - )); + const errorResponses = responses.pipe(filter((response) => !response.isSuccessful), map(() => true)); // TODO add a configurable number of retries in case of an error. + const dsoSuccessResponses = responses.pipe( + filter((response) => response.isSuccessful && hasValue((response as DSOSuccessResponse).resourceSelfLinks)), + map((response: DSOSuccessResponse) => response.resourceSelfLinks), + map((resourceSelfLinks: string[]) => resourceSelfLinks + .every((selfLink) => this.objectCache.hasBySelfLink(selfLink)) + )); - const otherSuccessResponses = responses.pipe(filter((response) => response.isSuccessful && !hasValue((response as DSOSuccessResponse).resourceSelfLinks)), map(() => true)); + const otherSuccessResponses = responses.pipe(filter((response) => response.isSuccessful && !hasValue((response as DSOSuccessResponse).resourceSelfLinks)), map(() => true)); - observableMerge(errorResponses, otherSuccessResponses, dsoSuccessResponses).subscribe((c) => isCached = c); + observableMerge(errorResponses, otherSuccessResponses, dsoSuccessResponses).subscribe((c) => isCached = c); + } const isPending = this.isPending(request); return isCached || isPending; } diff --git a/src/app/core/index/index.reducer.spec.ts b/src/app/core/index/index.reducer.spec.ts index a1cf92aeb3..ffc2c9fadc 100644 --- a/src/app/core/index/index.reducer.spec.ts +++ b/src/app/core/index/index.reducer.spec.ts @@ -20,6 +20,10 @@ describe('requestReducer', () => { const testState: IndexState = { [IndexName.OBJECT]: { [key1]: val1 + },[IndexName.REQUEST]: { + [key1]: val1 + },[IndexName.UUID_MAPPING]: { + [key1]: val1 } }; deepFreeze(testState); diff --git a/src/app/core/index/index.reducer.ts b/src/app/core/index/index.reducer.ts index 869dee9e51..c179182509 100644 --- a/src/app/core/index/index.reducer.ts +++ b/src/app/core/index/index.reducer.ts @@ -7,13 +7,12 @@ import { export enum IndexName { OBJECT = 'object/uuid-to-self-link', - REQUEST = 'get-request/href-to-uuid' + REQUEST = 'get-request/href-to-uuid', + UUID_MAPPING = 'get-request/configured-to-cache-uuid' } -export interface IndexState { - // TODO this should be `[name in IndexName]: {` but that's currently broken, - // see https://github.com/Microsoft/TypeScript/issues/13042 - [name: string]: { +export type IndexState = { + [name in IndexName]: { [key: string]: string } } @@ -43,9 +42,10 @@ function addToIndex(state: IndexState, action: AddToIndexAction): IndexState { const newSubState = Object.assign({}, subState, { [action.payload.key]: action.payload.value }); - return Object.assign({}, state, { + const obs = Object.assign({}, state, { [action.payload.name]: newSubState - }) + }); + return obs; } function removeFromIndexByValue(state: IndexState, action: RemoveFromIndexByValueAction): IndexState { diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index 3a5da84e13..a0bf7aabd5 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -1,6 +1,6 @@ import { Observable, of as observableOf, combineLatest as observableCombineLatest } from 'rxjs'; import { - distinctUntilChanged, + distinctUntilChanged, first, map, mergeMap, startWith, @@ -40,7 +40,7 @@ export class HALEndpointService { return this.requestService.getByHref(request.href).pipe( getResponseFromEntry(), map((response: EndpointMapSuccessResponse) => response.endpointMap), - distinctUntilChanged()); + ); } public getEndpoint(linkPath: string): Observable { From 6de7104b7c0b9b625414ce32a31109dc2dbeb5c3 Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 19 Oct 2018 16:18:44 +0200 Subject: [PATCH 48/69] finished update --- src/app/core/auth/auth-request.service.ts | 5 +---- src/app/core/data/request.reducer.ts | 8 -------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index 284f588040..f957d807c1 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -23,13 +23,10 @@ export class AuthRequestService { } protected fetchRequest(request: RestRequest): Observable { - return this.requestService.getByHref(request.href).pipe( - tap((t) => console.log(t)), - + return this.requestService.getByUUID(request.uuid).pipe( getResponseFromEntry(), // TODO to review when https://github.com/DSpace/dspace-angular/issues/217 will be fixed // tap(() => this.responseCache.remove(request.href)), - tap((t) => console.log(t)), mergeMap((response) => { if (response.isSuccessful && isNotEmpty(response)) { return observableOf((response as AuthStatusResponse).response); diff --git a/src/app/core/data/request.reducer.ts b/src/app/core/data/request.reducer.ts index e6a9097dce..a680de2d6b 100644 --- a/src/app/core/data/request.reducer.ts +++ b/src/app/core/data/request.reducer.ts @@ -53,14 +53,6 @@ function configureRequest(state: RequestState, action: RequestConfigureAction): completed: false, } }); - console.log(Object.assign({}, state, { - [action.payload.uuid]: { - request: action.payload, - requestPending: true, - responsePending: false, - completed: false, - } - });); } function executeRequest(state: RequestState, action: RequestExecuteAction): RequestState { From 0cf65b3f8c1c5247f6a2d76bd2ceac0d67010231 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 29 Oct 2018 11:17:07 +0100 Subject: [PATCH 49/69] 54472: Remove console.log --- src/app/core/auth/auth.service.ts | 1 - src/app/core/data/request.effects.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 4c520e8f30..229c44bcfa 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -116,7 +116,6 @@ export class AuthService { options.headers = headers; return this.authRequestService.postToEndpoint('login', body, options).pipe( map((status: AuthStatus) => { - console.log('yey response'); if (status.authenticated) { return status; } else { diff --git a/src/app/core/data/request.effects.ts b/src/app/core/data/request.effects.ts index 537a0b69b6..5e7bec698b 100644 --- a/src/app/core/data/request.effects.ts +++ b/src/app/core/data/request.effects.ts @@ -42,7 +42,6 @@ export class RequestEffects { }), filter((entry: RequestEntry) => hasValue(entry)), map((entry: RequestEntry) => entry.request), - tap((entry: RequestEntry) => console.log(entry)), flatMap((request: RestRequest) => { let body; if (isNotEmpty(request.body)) { From a49f9bdb031dc866455712502033fff43dbfb135 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 29 Oct 2018 13:09:45 +0100 Subject: [PATCH 50/69] 54472: Intermediate commit --- src/app/core/data/data.service.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index d591b1a8d5..ed32021f34 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -1,4 +1,4 @@ -import { distinctUntilChanged, filter, first, map, take } from 'rxjs/operators'; +import { distinctUntilChanged, filter, first, map, switchMap, take } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { Store } from '@ngrx/store'; import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; @@ -13,7 +13,7 @@ import { FindAllOptions, FindAllRequest, FindByIDRequest, - GetRequest + GetRequest, RestRequest } from './request.models'; import { RequestService } from './request.service'; import { NormalizedObject } from '../cache/models/normalized-object.model'; @@ -143,6 +143,8 @@ export abstract class DataService ); const selfLink$ = request$.pipe( + map((restRequest: RestRequest) => restRequest.href), + switchMap((href: string) => this.requestService.getByHref(href)), getResponseFromEntry(), map((response: RestResponse) => { if (!response.isSuccessful && response instanceof ErrorResponse) { From 1fc9359133f66d7a05447d0076cfe9218c3ffa18 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 29 Oct 2018 16:04:18 +0100 Subject: [PATCH 51/69] 54472: Refactored create to work with new response cache --- src/app/core/data/data.service.ts | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index ed32021f34..6a82577aa5 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -25,7 +25,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { HttpClient } from '@angular/common/http'; import { configureRequest, - filterSuccessfulResponses, + filterSuccessfulResponses, getResourceLinksFromResponse, getResponseFromEntry } from '../shared/operators'; import { DSOSuccessResponse, ErrorResponse, RestResponse } from '../cache/response.models'; @@ -138,13 +138,15 @@ export abstract class DataService const request$ = endpoint$.pipe( take(1), - map((endpoint: string) => new CreateRequest(requestId, endpoint, dso)), - configureRequest(this.requestService) + map((endpoint: string) => new CreateRequest(requestId, endpoint, dso)) ); - const selfLink$ = request$.pipe( - map((restRequest: RestRequest) => restRequest.href), - switchMap((href: string) => this.requestService.getByHref(href)), + // Execute the post request + request$.pipe( + configureRequest(this.requestService) + ).subscribe(); + + const selfLink$ = this.requestService.getByUUID(requestId).pipe( getResponseFromEntry(), map((response: RestResponse) => { if (!response.isSuccessful && response instanceof ErrorResponse) { @@ -153,13 +155,17 @@ export abstract class DataService return response; } }), - filterSuccessfulResponses(), - map((response: DSOSuccessResponse) => { - return response.resourceSelfLinks[0]; + map((response: any) => { + if (isNotEmpty(response.resourceSelfLinks)) { + return response.resourceSelfLinks[0]; + } }), distinctUntilChanged() ) as Observable; - return this.rdbService.buildSingle(selfLink$) as Observable>; + + return selfLink$.pipe( + switchMap((selfLink: string) => this.findByHref(selfLink)), + ) } } From 80b11515c4048df601452b5545f9669885171966 Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 31 Oct 2018 11:59:09 +0100 Subject: [PATCH 52/69] fixed SSB commit delay --- .../auth-response-parsing.service.spec.ts | 2 +- .../core/cache/object-cache.reducer.spec.ts | 26 ++++++++++++++++--- .../core/cache/server-sync-buffer.effects.ts | 7 +++-- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/app/core/auth/auth-response-parsing.service.spec.ts b/src/app/core/auth/auth-response-parsing.service.spec.ts index 138d0f1be3..5b94d8e69a 100644 --- a/src/app/core/auth/auth-response-parsing.service.spec.ts +++ b/src/app/core/auth/auth-response-parsing.service.spec.ts @@ -11,7 +11,7 @@ import { ObjectCacheState } from '../cache/object-cache.reducer'; describe('AuthResponseParsingService', () => { let service: AuthResponseParsingService; - const EnvConfig = { cache: { msToLive: 1000 } } as GlobalConfig; + const EnvConfig = { cache: { msToLive: 1000 } } as any; const store = new MockStore({}); const objectCacheService = new ObjectCacheService(store as any); diff --git a/src/app/core/cache/object-cache.reducer.spec.ts b/src/app/core/cache/object-cache.reducer.spec.ts index 311f11c2ad..4e383a82e4 100644 --- a/src/app/core/cache/object-cache.reducer.spec.ts +++ b/src/app/core/cache/object-cache.reducer.spec.ts @@ -8,6 +8,7 @@ import { RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction } from './object-cache.actions'; +import { Operation } from 'fast-json-patch'; class NullAction extends RemoveFromObjectCacheAction { type = null; @@ -21,6 +22,7 @@ class NullAction extends RemoveFromObjectCacheAction { describe('objectCacheReducer', () => { 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 newName = 'new different name'; const testState = { [selfLink1]: { data: { @@ -140,15 +142,31 @@ describe('objectCacheReducer', () => { }); it('should perform the ADD_PATCH action without affecting the previous state', () => { - const action = new AddPatchObjectCacheAction(selfLink1, [{ op: 'replace', path: '/name', value: 'random string' }]); + const action = new AddPatchObjectCacheAction(selfLink1, [{ + op: 'replace', + path: '/name', + value: 'random string' + }]); // testState has already been frozen above objectCacheReducer(testState, action); }); - it('should perform the APPLY_PATCH action without affecting the previous state', () => { + 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); - // testState has already been frozen above - objectCacheReducer(testState, action); + const newState = objectCacheReducer(stateWithPatch, action); + expect(newState[selfLink1].patches).toEqual([]); + expect((newState[selfLink1].data as any).name).toEqual(newName); }); }); diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts index db2263c52a..5a0a5527d1 100644 --- a/src/app/core/cache/server-sync-buffer.effects.ts +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -1,4 +1,4 @@ -import { delay, exhaustMap, first, map, switchMap } from 'rxjs/operators'; +import { delay, exhaustMap, first, map, switchMap, tap } from 'rxjs/operators'; import { Inject, Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { @@ -38,7 +38,9 @@ export class ServerSyncBufferEffects { 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)) + return observableOf(new CommitSSBAction(action.payload.method)).pipe( + delay(timeoutInSeconds * 1000), + ) }) ); @@ -54,6 +56,7 @@ export class ServerSyncBufferEffects { switchMap((action: CommitSSBAction) => { return this.store.pipe( select(serverSyncBufferSelector()), + first(), /* necessary, otherwise delay will not have any effect after the first run */ switchMap((bufferState: ServerSyncBufferState) => { const actions: Array> = bufferState.buffer .filter((entry: ServerSyncBufferEntry) => { From dd24958f618980cdaaab1e8ff5250054944de2fd Mon Sep 17 00:00:00 2001 From: lotte Date: Mon, 5 Nov 2018 12:01:51 +0100 Subject: [PATCH 53/69] finished tests and docs --- .../auth/auth-response-parsing.service.ts | 2 +- src/app/core/auth/auth.service.ts | 1 - src/app/core/auth/server-auth.service.ts | 1 - src/app/core/browse/browse.service.ts | 9 +- .../remote-data-build.service.spec.ts | 2 +- .../builders/remote-data-build.service.ts | 24 ++-- src/app/core/cache/object-cache.actions.ts | 6 +- .../core/cache/object-cache.reducer.spec.ts | 20 ++-- src/app/core/cache/object-cache.reducer.ts | 4 +- .../core/cache/object-cache.service.spec.ts | 5 +- src/app/core/cache/object-cache.service.ts | 18 ++- .../core/cache/server-sync-buffer.actions.ts | 2 +- .../cache/server-sync-buffer.reducer.spec.ts | 10 +- .../data/base-response-parsing.service.ts | 28 ++--- .../data/config-response-parsing.service.ts | 2 +- .../core/data/dso-response-parsing.service.ts | 2 +- ...acet-value-map-response-parsing.service.ts | 2 - .../facet-value-response-parsing.service.ts | 9 +- src/app/core/data/request.effects.ts | 1 - src/app/core/data/request.service.spec.ts | 103 +++++++++++++++++- src/app/core/data/request.service.ts | 30 ++++- .../integration-response-parsing.service.ts | 2 +- src/app/core/metadata/metadata.service.ts | 6 +- .../core/shared/hal-endpoint.service.spec.ts | 66 +++++++++++ src/app/core/shared/hal-endpoint.service.ts | 6 + src/app/core/shared/operators.spec.ts | 81 +++++++++++--- src/app/core/shared/operators.ts | 9 +- src/app/shared/mocks/mock-request.service.ts | 5 +- 28 files changed, 341 insertions(+), 115 deletions(-) diff --git a/src/app/core/auth/auth-response-parsing.service.ts b/src/app/core/auth/auth-response-parsing.service.ts index 65d093de61..61559991ec 100644 --- a/src/app/core/auth/auth-response-parsing.service.ts +++ b/src/app/core/auth/auth-response-parsing.service.ts @@ -27,7 +27,7 @@ export class AuthResponseParsingService extends BaseResponseParsingService imple parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '200' || data.statusCode === 'OK')) { - const response = this.process(data.payload, request.href); + const response = this.process(data.payload, request.uuid); return new AuthStatusResponse(response, data.statusCode); } else { return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode); diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 4c520e8f30..229c44bcfa 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -116,7 +116,6 @@ export class AuthService { options.headers = headers; return this.authRequestService.postToEndpoint('login', body, options).pipe( map((status: AuthStatus) => { - console.log('yey response'); if (status.authenticated) { return status; } else { diff --git a/src/app/core/auth/server-auth.service.ts b/src/app/core/auth/server-auth.service.ts index 9ab2d84c20..c83410f6e3 100644 --- a/src/app/core/auth/server-auth.service.ts +++ b/src/app/core/auth/server-auth.service.ts @@ -41,7 +41,6 @@ export class ServerAuthService extends AuthService { // TODO this should be cleaned up, AuthStatus could be parsed by the RemoteDataService as a whole... const person$ = this.rdbService.buildSingle(status.eperson.toString()); - // person$.subscribe(() => console.log('test')); return person$.pipe( map((eperson) => eperson.payload) ); diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index ca56c0d267..b807a77e99 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -28,8 +28,7 @@ import { configureRequest, filterSuccessfulResponses, getBrowseDefinitionLinks, - getRemoteDataPayload, - getRequestFromSelflink + getRemoteDataPayload, getRequestFromRequestHref } from '../shared/operators'; import { URLCombiner } from '../url-combiner/url-combiner'; import { Item } from '../shared/item.model'; @@ -68,7 +67,7 @@ export class BrowseService { ); const href$ = request$.pipe(map((request: RestRequest) => request.href)); - const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); + const requestEntry$ = href$.pipe(getRequestFromRequestHref(this.requestService)); const payload$ = requestEntry$.pipe( filterSuccessfulResponses(), map((response: GenericSuccessResponse) => response.payload), @@ -111,7 +110,7 @@ export class BrowseService { const href$ = request$.pipe(map((request: RestRequest) => request.href)); - const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); + const requestEntry$ = href$.pipe(getRequestFromRequestHref(this.requestService)); const payload$ = requestEntry$.pipe( filterSuccessfulResponses(), @@ -166,7 +165,7 @@ export class BrowseService { const href$ = request$.pipe(map((request: RestRequest) => request.href)); - const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); + const requestEntry$ = href$.pipe(getRequestFromRequestHref(this.requestService)); const payload$ = requestEntry$.pipe( filterSuccessfulResponses(), diff --git a/src/app/core/cache/builders/remote-data-build.service.spec.ts b/src/app/core/cache/builders/remote-data-build.service.spec.ts index 8aea54102c..e4444ca803 100644 --- a/src/app/core/cache/builders/remote-data-build.service.spec.ts +++ b/src/app/core/cache/builders/remote-data-build.service.spec.ts @@ -32,7 +32,7 @@ describe('RemoteDataBuildService', () => { let service: RemoteDataBuildService; beforeEach(() => { - service = new RemoteDataBuildService(undefined, undefined, undefined); + service = new RemoteDataBuildService(undefined, undefined); }); describe('when toPaginatedList is called', () => { diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 121ad1aa8c..52ec4382ae 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -5,15 +5,7 @@ import { race as observableRace } from 'rxjs'; import { Injectable } from '@angular/core'; -import { - distinctUntilChanged, - first, - flatMap, - map, - startWith, - switchMap, - takeUntil, tap -} from 'rxjs/operators'; +import { distinctUntilChanged, first, flatMap, map, startWith, switchMap } from 'rxjs/operators'; import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util'; import { PaginatedList } from '../../data/paginated-list'; import { RemoteData } from '../../data/remote-data'; @@ -29,7 +21,7 @@ import { getMapsTo, getRelationMetadata, getRelationships } from './build-decora import { PageInfo } from '../../shared/page-info.model'; import { filterSuccessfulResponses, - getRequestFromSelflink, + getRequestFromRequestHref, getRequestFromRequestUUID, getResourceLinksFromResponse } from '../../shared/operators'; @@ -43,16 +35,16 @@ export class RemoteDataBuildService { if (typeof href$ === 'string') { href$ = observableOf(href$); } - const requestHref$ = href$.pipe( + const requestUUID$ = href$.pipe( switchMap((href: string) => - this.objectCache.getRequestHrefBySelfLink(href)), + this.objectCache.getRequestUUIDBySelfLink(href)), ); const requestEntry$ = observableRace( - href$.pipe(getRequestFromSelflink(this.requestService)), - requestHref$.pipe(getRequestFromSelflink(this.requestService)), + href$.pipe(getRequestFromRequestHref(this.requestService)), + requestUUID$.pipe(getRequestFromRequestUUID(this.requestService)), ).pipe( - first() + first() ); // always use self link if that is cached, only if it isn't, get it via the response. @@ -121,7 +113,7 @@ export class RemoteDataBuildService { href$ = observableOf(href$); } - const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); + const requestEntry$ = href$.pipe(getRequestFromRequestHref(this.requestService)); const tDomainList$ = requestEntry$.pipe( getResourceLinksFromResponse(), flatMap((resourceUUIDs: string[]) => { diff --git a/src/app/core/cache/object-cache.actions.ts b/src/app/core/cache/object-cache.actions.ts index 024a0e7061..8531677ffc 100644 --- a/src/app/core/cache/object-cache.actions.ts +++ b/src/app/core/cache/object-cache.actions.ts @@ -25,7 +25,7 @@ export class AddToObjectCacheAction implements Action { objectToCache: CacheableObject; timeAdded: number; msToLive: number; - requestHref: string; + requestUUID: string; }; /** @@ -42,8 +42,8 @@ export class AddToObjectCacheAction implements Action { * This isn't necessarily the same as the object's self * link, it could have been part of a list for example */ - constructor(objectToCache: CacheableObject, timeAdded: number, msToLive: number, requestHref: string) { - this.payload = { objectToCache, timeAdded, msToLive, requestHref }; + constructor(objectToCache: CacheableObject, timeAdded: number, msToLive: number, requestUUID: string) { + this.payload = { objectToCache, timeAdded, msToLive, requestUUID }; } } diff --git a/src/app/core/cache/object-cache.reducer.spec.ts b/src/app/core/cache/object-cache.reducer.spec.ts index 4e383a82e4..efa28d7249 100644 --- a/src/app/core/cache/object-cache.reducer.spec.ts +++ b/src/app/core/cache/object-cache.reducer.spec.ts @@ -20,6 +20,8 @@ class NullAction extends RemoveFromObjectCacheAction { } 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 selfLink2 = 'https://localhost:8080/api/core/items/28b04544-1766-4e82-9728-c4e93544ecd3'; const newName = 'new different name'; @@ -31,18 +33,18 @@ describe('objectCacheReducer', () => { }, timeAdded: new Date().getTime(), msToLive: 900000, - requestHref: selfLink1, + requestUUID: requestUUID1, patches: [], isDirty: false }, [selfLink2]: { data: { - self: selfLink2, + self: requestUUID2, foo: 'baz' }, timeAdded: new Date().getTime(), msToLive: 900000, - requestHref: selfLink2, + requestUUID: selfLink2, patches: [], isDirty: false } @@ -68,8 +70,8 @@ describe('objectCacheReducer', () => { const objectToCache = { self: selfLink1 }; const timeAdded = new Date().getTime(); const msToLive = 900000; - const requestHref = 'https://rest.api/endpoint/selfLink1'; - const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref); + const requestUUID = requestUUID1; + const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestUUID); const newState = objectCacheReducer(state, action); expect(newState[selfLink1].data).toEqual(objectToCache); @@ -81,8 +83,8 @@ describe('objectCacheReducer', () => { const objectToCache = { self: selfLink1, foo: 'baz', somethingElse: true }; const timeAdded = new Date().getTime(); const msToLive = 900000; - const requestHref = 'https://rest.api/endpoint/selfLink1'; - const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref); + const requestUUID = requestUUID1; + const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestUUID); const newState = objectCacheReducer(testState, action); /* tslint:disable:no-string-literal */ @@ -96,8 +98,8 @@ describe('objectCacheReducer', () => { const objectToCache = { self: selfLink1 }; const timeAdded = new Date().getTime(); const msToLive = 900000; - const requestHref = 'https://rest.api/endpoint/selfLink1'; - const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref); + const requestUUID = requestUUID1; + const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestUUID); deepFreeze(state); objectCacheReducer(state, action); diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index 4424bb2142..867f31e1bb 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -45,7 +45,7 @@ export class ObjectCacheEntry implements CacheEntry { data: CacheableObject; timeAdded: number; msToLive: number; - requestHref: string; + requestUUID: string; patches: Patch[] = []; isDirty: boolean; } @@ -119,7 +119,7 @@ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheActio data: action.payload.objectToCache, timeAdded: action.payload.timeAdded, msToLive: action.payload.msToLive, - requestHref: action.payload.requestHref, + requestUUID: action.payload.requestUUID, isDirty: (hasValue(existing) ? isNotEmpty(existing.patches) : false), patches: (hasValue(existing) ? existing.patches : []) } diff --git a/src/app/core/cache/object-cache.service.spec.ts b/src/app/core/cache/object-cache.service.spec.ts index 5a1a3c5ac6..af353a38c1 100644 --- a/src/app/core/cache/object-cache.service.spec.ts +++ b/src/app/core/cache/object-cache.service.spec.ts @@ -22,6 +22,7 @@ describe('ObjectCacheService', () => { let store: Store; const selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; + const requestUUID = '4d3a4ce8-a375-4b98-859b-39f0a014d736'; const timestamp = new Date().getTime(); const msToLive = 900000; let objectToCache = { @@ -58,8 +59,8 @@ describe('ObjectCacheService', () => { describe('add', () => { it('should dispatch an ADD action with the object to add, the time to live, and the current timestamp', () => { - service.add(objectToCache, msToLive, selfLink); - expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive, selfLink)); + service.add(objectToCache, msToLive, requestUUID); + expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive, requestUUID)); }); }); diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 3ac644a045..44297d6f61 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -45,13 +45,11 @@ export class ObjectCacheService { * The object to add * @param msToLive * The number of milliseconds it should be cached for - * @param requestHref - * The selfLink 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 + * @param requestUUID + * The UUID of the request that resulted in this object */ - add(objectToCache: CacheableObject, msToLive: number, requestHref: string): void { - this.store.dispatch(new AddToObjectCacheAction(objectToCache, new Date().getTime(), msToLive, requestHref)); + add(objectToCache: CacheableObject, msToLive: number, requestUUID: string): void { + this.store.dispatch(new AddToObjectCacheAction(objectToCache, new Date().getTime(), msToLive, requestUUID)); } /** @@ -115,16 +113,16 @@ export class ObjectCacheService { ); } - getRequestHrefBySelfLink(selfLink: string): Observable { + getRequestUUIDBySelfLink(selfLink: string): Observable { return this.getEntry(selfLink).pipe( - map((entry: ObjectCacheEntry) => entry.requestHref), + map((entry: ObjectCacheEntry) => entry.requestUUID), distinctUntilChanged()); } - getRequestHrefByUUID(uuid: string): Observable { + getRequestUUIDByObjectUUID(uuid: string): Observable { return this.store.pipe( select(selfLinkFromUuidSelector(uuid)), - mergeMap((selfLink: string) => this.getRequestHrefBySelfLink(selfLink)) + mergeMap((selfLink: string) => this.getRequestUUIDBySelfLink(selfLink)) ); } diff --git a/src/app/core/cache/server-sync-buffer.actions.ts b/src/app/core/cache/server-sync-buffer.actions.ts index 638d837bea..fd7e04ef8a 100644 --- a/src/app/core/cache/server-sync-buffer.actions.ts +++ b/src/app/core/cache/server-sync-buffer.actions.ts @@ -15,7 +15,7 @@ export const ServerSyncBufferActionTypes = { /* tslint:disable:max-classes-per-file */ /** - * An ngrx action to add a new cached object to the server's sync buffer + * An ngrx action to add a new cached object to the server sync buffer */ export class AddToSSBAction implements Action { type = ServerSyncBufferActionTypes.ADD; diff --git a/src/app/core/cache/server-sync-buffer.reducer.spec.ts b/src/app/core/cache/server-sync-buffer.reducer.spec.ts index 666144104b..8f1392c99d 100644 --- a/src/app/core/cache/server-sync-buffer.reducer.spec.ts +++ b/src/app/core/cache/server-sync-buffer.reducer.spec.ts @@ -1,13 +1,5 @@ import * as deepFreeze from 'deep-freeze'; - -import { objectCacheReducer } from './object-cache.reducer'; -import { - AddPatchObjectCacheAction, - AddToObjectCacheAction, ApplyPatchObjectCacheAction, - RemoveFromObjectCacheAction, - ResetObjectCacheTimestampsAction -} from './object-cache.actions'; -import { Operation } from '../../../../node_modules/fast-json-patch'; +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'; diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index ee87338899..eada156ce9 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -25,14 +25,14 @@ export abstract class BaseResponseParsingService { protected abstract objectFactory: any; protected abstract toCache: boolean; - protected process(data: any, requestHref: string): any { + protected process(data: any, requestUUID: string): any { if (isNotEmpty(data)) { if (hasNoValue(data) || (typeof data !== 'object')) { return data; } else if (isPaginatedResponse(data)) { - return this.processPaginatedList(data, requestHref); + return this.processPaginatedList(data, requestUUID); } else if (Array.isArray(data)) { - return this.processArray(data, requestHref); + return this.processArray(data, requestUUID); } else if (isObjectLevel(data)) { data = this.fixBadEPersonRestResponse(data); const object = this.deserialize(data); @@ -41,7 +41,7 @@ export abstract class BaseResponseParsingService { .keys(data._embedded) .filter((property) => data._embedded.hasOwnProperty(property)) .forEach((property) => { - const parsedObj = this.process(data._embedded[property], requestHref); + const parsedObj = this.process(data._embedded[property], requestUUID); if (isNotEmpty(parsedObj)) { if (isPaginatedResponse(data._embedded[property])) { object[property] = parsedObj; @@ -55,7 +55,7 @@ export abstract class BaseResponseParsingService { }); } - this.cache(object, requestHref); + this.cache(object, requestUUID); return object; } const result = {}; @@ -63,7 +63,7 @@ export abstract class BaseResponseParsingService { .filter((property) => data.hasOwnProperty(property)) .filter((property) => hasValue(data[property])) .forEach((property) => { - const obj = this.process(data[property], requestHref); + const obj = this.process(data[property], requestUUID); result[property] = obj; }); return result; @@ -71,7 +71,7 @@ export abstract class BaseResponseParsingService { } } - protected processPaginatedList(data: any, requestHref: string): PaginatedList { + protected processPaginatedList(data: any, requestUUID: string): PaginatedList { const pageInfo: PageInfo = this.processPageInfo(data); let list = data._embedded; @@ -79,14 +79,14 @@ export abstract class BaseResponseParsingService { if (!Array.isArray(list)) { list = this.flattenSingleKeyObject(list); } - const page: ObjectDomain[] = this.processArray(list, requestHref); + const page: ObjectDomain[] = this.processArray(list, requestUUID); return new PaginatedList(pageInfo, page); } - protected processArray(data: any, requestHref: string): ObjectDomain[] { + protected processArray(data: any, requestUUID: string): ObjectDomain[] { let array: ObjectDomain[] = []; data.forEach((datum) => { - array = [...array, this.process(datum, requestHref)]; + array = [...array, this.process(datum, requestUUID)]; } ); return array; @@ -114,17 +114,17 @@ export abstract class BaseResponseParsingService { } } - protected cache(obj, requestHref) { + protected cache(obj, requestUUID) { if (this.toCache) { - this.addToObjectCache(obj, requestHref); + this.addToObjectCache(obj, requestUUID); } } - protected addToObjectCache(co: CacheableObject, requestHref: string): void { + protected addToObjectCache(co: CacheableObject, requestUUID: string): void { if (hasNoValue(co) || hasNoValue(co.self)) { throw new Error('The server returned an invalid object'); } - this.objectCache.add(co, this.EnvConfig.cache.msToLive.default, requestHref); + this.objectCache.add(co, this.EnvConfig.cache.msToLive.default, requestUUID); } processPageInfo(payload: any): PageInfo { diff --git a/src/app/core/data/config-response-parsing.service.ts b/src/app/core/data/config-response-parsing.service.ts index ddf884e02b..50303d0a09 100644 --- a/src/app/core/data/config-response-parsing.service.ts +++ b/src/app/core/data/config-response-parsing.service.ts @@ -28,7 +28,7 @@ export class ConfigResponseParsingService extends BaseResponseParsingService imp parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '201' || data.statusCode === '200' || data.statusCode === 'OK')) { - const configDefinition = this.process(data.payload, request.href); + const configDefinition = this.process(data.payload, request.uuid); return new ConfigSuccessResponse(configDefinition, data.statusCode, this.processPageInfo(data.payload)); } else { return new ErrorResponse( diff --git a/src/app/core/data/dso-response-parsing.service.ts b/src/app/core/data/dso-response-parsing.service.ts index 568114be1a..1066d11a50 100644 --- a/src/app/core/data/dso-response-parsing.service.ts +++ b/src/app/core/data/dso-response-parsing.service.ts @@ -28,7 +28,7 @@ export class DSOResponseParsingService extends BaseResponseParsingService implem } parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - const processRequestDTO = this.process(data.payload, request.href); + const processRequestDTO = this.process(data.payload, request.uuid); let objectList = processRequestDTO; if (hasNoValue(processRequestDTO)) { diff --git a/src/app/core/data/facet-value-map-response-parsing.service.ts b/src/app/core/data/facet-value-map-response-parsing.service.ts index 0fc5917847..2f580ee952 100644 --- a/src/app/core/data/facet-value-map-response-parsing.service.ts +++ b/src/app/core/data/facet-value-map-response-parsing.service.ts @@ -9,8 +9,6 @@ import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { PageInfo } from '../shared/page-info.model'; -import { isNotEmpty } from '../../shared/empty.util'; import { FacetValue } from '../../+search-page/search-service/facet-value.model'; import { BaseResponseParsingService } from './base-response-parsing.service'; import { ObjectCacheService } from '../cache/object-cache.service'; diff --git a/src/app/core/data/facet-value-response-parsing.service.ts b/src/app/core/data/facet-value-response-parsing.service.ts index 585172c22e..54f36a0564 100644 --- a/src/app/core/data/facet-value-response-parsing.service.ts +++ b/src/app/core/data/facet-value-response-parsing.service.ts @@ -1,16 +1,9 @@ import { Inject, Injectable } from '@angular/core'; -import { - FacetValueMap, - FacetValueMapSuccessResponse, - FacetValueSuccessResponse, - RestResponse -} from '../cache/response.models'; +import { FacetValueSuccessResponse, RestResponse } from '../cache/response.models'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { PageInfo } from '../shared/page-info.model'; -import { isNotEmpty } from '../../shared/empty.util'; import { FacetValue } from '../../+search-page/search-service/facet-value.model'; import { BaseResponseParsingService } from './base-response-parsing.service'; import { ObjectCacheService } from '../cache/object-cache.service'; diff --git a/src/app/core/data/request.effects.ts b/src/app/core/data/request.effects.ts index 537a0b69b6..5e7bec698b 100644 --- a/src/app/core/data/request.effects.ts +++ b/src/app/core/data/request.effects.ts @@ -42,7 +42,6 @@ export class RequestEffects { }), filter((entry: RequestEntry) => hasValue(entry)), map((entry: RequestEntry) => entry.request), - tap((entry: RequestEntry) => console.log(entry)), flatMap((request: RestRequest) => { let body; if (isNotEmpty(request.body)) { diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index debddb748c..90d2edfc84 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -174,7 +174,7 @@ describe('RequestService', () => { // b: undefined // }); - scheduler.expectObservable(result).toBe('b', {b: undefined}); + scheduler.expectObservable(result).toBe('b', { b: undefined }); }); }); @@ -458,4 +458,105 @@ describe('RequestService', () => { }); }); }); + + describe('isReusable', () => { + describe('when the given UUID is has no value', () => { + let reusable; + beforeEach(() => { + const uuid = undefined; + reusable = serviceAsAny.isReusable(uuid); + }); + it('return an observable emitting false', () => { + reusable.subscribe((isReusable) => expect(isReusable).toBe(false)); + }) + }); + + describe('when the given UUID has a value, but no cached entry is found', () => { + let reusable; + beforeEach(() => { + spyOn(service, 'getByUUID').and.returnValue(observableOf(undefined)); + const uuid = 'a45bb291-1adb-40d9-b2fc-7ad9080607be'; + reusable = serviceAsAny.isReusable(uuid); + }); + it('return an observable emitting false', () => { + reusable.subscribe((isReusable) => expect(isReusable).toBe(false)); + }) + }); + + describe('when the given UUID has a value, a cached entry is found, but it has no response', () => { + let reusable; + beforeEach(() => { + spyOn(service, 'getByUUID').and.returnValue(observableOf({ response: undefined })); + const uuid = '53c9b814-ad8b-4567-9bc1-d9bb6cfba6c8'; + reusable = serviceAsAny.isReusable(uuid); + }); + it('return an observable emitting false', () => { + reusable.subscribe((isReusable) => expect(isReusable).toBe(false)); + }) + }); + + describe('when the given UUID has a value, a cached entry is found, but its response was not successful', () => { + let reusable; + beforeEach(() => { + spyOn(service, 'getByUUID').and.returnValue(observableOf({ response: { isSuccessful: false } })); + const uuid = '694c9b32-7b2e-4788-835b-ef3fc2252e6c'; + reusable = serviceAsAny.isReusable(uuid); + }); + it('return an observable emitting false', () => { + reusable.subscribe((isReusable) => expect(isReusable).toBe(false)); + }) + }); + + describe('when the given UUID has a value, a cached entry is found, its response was successful, but the response is outdated', () => { + let reusable; + const now = 100000; + const timeAdded = 99899; + const msToLive = 100; + + beforeEach(() => { + spyOn(Date.prototype, 'getTime').and.returnValue(now); + spyOn(service, 'getByUUID').and.returnValue(observableOf({ + response: { + isSuccessful: true, + timeAdded: timeAdded + }, + request: { + responseMsToLive: msToLive + } + })); + const uuid = 'f9b85788-881c-4994-86b6-bae8dad024d2'; + reusable = serviceAsAny.isReusable(uuid); + }); + + it('return an observable emitting false', () => { + reusable.subscribe((isReusable) => expect(isReusable).toBe(false)); + }) + }); + + describe('when the given UUID has a value, a cached entry is found, its response was successful, and the response is not outdated', () => { + let reusable; + const now = 100000; + const timeAdded = 99999; + const msToLive = 100; + + beforeEach(() => { + spyOn(Date.prototype, 'getTime').and.returnValue(now); + spyOn(service, 'getByUUID').and.returnValue(observableOf({ + response: { + isSuccessful: true, + timeAdded: timeAdded + }, + request: { + responseMsToLive: msToLive + } + })); + const uuid = 'f9b85788-881c-4994-86b6-bae8dad024d2'; + reusable = serviceAsAny.isReusable(uuid); + }); + + it('return an observable emitting true', () => { + reusable.subscribe((isReusable) => expect(isReusable).toBe(true)); + }) + }) + }) }); diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 4b7a7c9b49..285ed06545 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -76,7 +76,7 @@ export class RequestService { } getByUUID(uuid: string): Observable { - return observableRace( + return observableRace( this.store.pipe(select(this.entryFromUUIDSelector(uuid))), this.store.pipe( select(this.originalUUIDFromUUIDSelector(uuid)), @@ -94,6 +94,12 @@ export class RequestService { ); } + /** + * Configure a certain request + * Used to make sure a request is in the cache + * @param {RestRequest} request The request to send out + * @param {boolean} forceBypassCache When true, a new request is always dispatched + */ // TODO to review "overrideRequest" param when https://github.com/DSpace/dspace-angular/issues/217 will be fixed configure(request: RestRequest, forceBypassCache: boolean = false): void { const isGetRequest = request.method === RestRequestMethod.GET; @@ -113,6 +119,11 @@ export class RequestService { } } + /** + * Check if a request is in the cache or if it's still pending + * @param {GetRequest} request The request to check + * @returns {boolean} True if the request is cached or still pending + */ private isCachedOrPending(request: GetRequest) { let isCached = this.objectCache.hasBySelfLink(request.href); if (isCached) { @@ -142,6 +153,10 @@ export class RequestService { return isCached || isPending; } + /** + * Configure and execute the request + * @param {RestRequest} request to dispatch + */ private dispatchRequest(request: RestRequest) { this.store.dispatch(new RequestConfigureAction(request)); this.store.dispatch(new RequestExecuteAction(request.uuid)); @@ -164,6 +179,10 @@ export class RequestService { }); } + /** + * Dispatch commit action to send all changes (for a certain method) to the server (buffer) + * @param {RestRequestMethod} method RestRequestMethod for which the changes should be committed + */ commit(method?: RestRequestMethod) { this.store.dispatch(new CommitSSBAction(method)) } @@ -171,11 +190,11 @@ export class RequestService { /** * Check whether a Response should still be cached * - * @param entry - * the entry to check + * @param uuid + * the uuid of the entry to check * @return boolean - * false if the entry is null, undefined, or its time to - * live has been exceeded, true otherwise + * false if the uuid has no value, no entry could be found, the response was nog successful or its time to + * live has exceeded, true otherwise */ private isReusable(uuid: string): Observable { if (hasNoValue(uuid)) { @@ -194,7 +213,6 @@ export class RequestService { } }) ); - return observableOf(false); } } } diff --git a/src/app/core/integration/integration-response-parsing.service.ts b/src/app/core/integration/integration-response-parsing.service.ts index 6eff7ab792..ef278c93de 100644 --- a/src/app/core/integration/integration-response-parsing.service.ts +++ b/src/app/core/integration/integration-response-parsing.service.ts @@ -32,7 +32,7 @@ export class IntegrationResponseParsingService extends BaseResponseParsingServic parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) { - const dataDefinition = this.process(data.payload, request.href); + const dataDefinition = this.process(data.payload, request.uuid); return new IntegrationSuccessResponse(dataDefinition, data.statusCode, this.processPageInfo(data.payload.page)); } else { return new ErrorResponse( diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 3a63be3f55..8bec058d08 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -263,13 +263,17 @@ export class MetadataService { .pipe( first((files) => isNotEmpty(files)), catchError((error) => { - console.debug(error); + console.debug(error.message); return [] })) .subscribe((bitstreams: Bitstream[]) => { for (const bitstream of bitstreams) { bitstream.format.pipe( first(), + catchError((error: Error) => { + console.debug(error.message); + return [] + }), map((rd: RemoteData) => rd.payload), filter((format: BitstreamFormat) => hasValue(format))) .subscribe((format: BitstreamFormat) => { diff --git a/src/app/core/shared/hal-endpoint.service.spec.ts b/src/app/core/shared/hal-endpoint.service.spec.ts index b65b3f905b..8b3011e7d7 100644 --- a/src/app/core/shared/hal-endpoint.service.spec.ts +++ b/src/app/core/shared/hal-endpoint.service.spec.ts @@ -15,6 +15,28 @@ describe('HALEndpointService', () => { const endpointMap = { test: 'https://rest.api/test', + foo: 'https://rest.api/foo', + bar: 'https://rest.api/bar', + endpoint: 'https://rest.api/endpoint', + link: 'https://rest.api/link', + another: 'https://rest.api/another', + }; + const start = 'http://start.com'; + const one = 'http://one.com'; + const two = 'http://two.com'; + const endpointMaps = { + [start]: { + one: one, + two: 'empty', + endpoint: 'https://rest.api/endpoint', + link: 'https://rest.api/link', + another: 'https://rest.api/another', + }, + [one]: { + one: 'empty', + two: two, + bar: 'https://rest.api/bar', + } }; const linkPath = 'test'; @@ -80,6 +102,50 @@ describe('HALEndpointService', () => { }); }); + describe('getEndpointAt', () => { + it('should throw an error when the list of hal endpoint names is empty', () => { + const endpointAtWithoutEndpointNames = () => { + (service as any).getEndpointAt('') + }; + expect(endpointAtWithoutEndpointNames).toThrow(); + }); + + it('should be at least called as many times as the length of halNames', () => { + spyOn(service as any, 'getEndpointMapAt').and.returnValue(observableOf(endpointMap)); + spyOn((service as any), 'getEndpointAt').and.callThrough(); + + (service as any).getEndpointAt('', 'endpoint').subscribe(); + + expect((service as any).getEndpointAt.calls.count()).toEqual(1); + + (service as any).getEndpointAt.calls.reset(); + + (service as any).getEndpointAt('', 'endpoint', 'another').subscribe(); + + expect((service as any).getEndpointAt.calls.count()).toBeGreaterThanOrEqual(2); + + (service as any).getEndpointAt.calls.reset(); + + (service as any).getEndpointAt('', 'endpoint', 'another', 'foo', 'bar', 'test').subscribe(); + + expect((service as any).getEndpointAt.calls.count()).toBeGreaterThanOrEqual(5); + }); + + it('should return the correct endpoint', () => { + spyOn(service as any, 'getEndpointMapAt').and.callFake((param) => { + return observableOf(endpointMaps[param]); + }); + + (service as any).getEndpointAt(start, 'one').subscribe((endpoint) => { + expect(endpoint).toEqual(one); + }); + + (service as any).getEndpointAt(start, 'one', 'two').subscribe((endpoint) => { + expect(endpoint).toEqual(two); + }); + }); + }); + describe('isEnabledOnRestApi', () => { beforeEach(() => { service = new HALEndpointService( diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index a0bf7aabd5..a93d54db64 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -47,6 +47,12 @@ export class HALEndpointService { return this.getEndpointAt(this.getRootHref(), ...linkPath.split('/')); } + /** + * Resolve the actual hal url based on a list of hal names + * @param {string} href The root url to start from + * @param {string} halNames List of hal names for which a url should be resolved + * @returns {Observable} Observable that emits the found hal url + */ private getEndpointAt(href: string, ...halNames: string[]): Observable { if (isEmpty(halNames)) { throw new Error('cant\'t fetch the URL without the HAL link names') diff --git a/src/app/core/shared/operators.spec.ts b/src/app/core/shared/operators.spec.ts index 5f29de9e93..6aeec230c4 100644 --- a/src/app/core/shared/operators.spec.ts +++ b/src/app/core/shared/operators.spec.ts @@ -7,22 +7,24 @@ import { RequestService } from '../data/request.service'; import { configureRequest, filterSuccessfulResponses, - getRemoteDataPayload, - getRequestFromSelflink, - getResourceLinksFromResponse, + getRemoteDataPayload, getRequestFromRequestHref, getRequestFromRequestUUID, + getResourceLinksFromResponse, getResponseFromEntry, } from './operators'; describe('Core Module - RxJS Operators', () => { let scheduler: TestScheduler; let requestService: RequestService; - const testSelfLink = 'https://rest.api/'; + const testRequestHref = 'https://rest.api/'; + const testRequestUUID = 'https://rest.api/'; const testRCEs = { a: { response: { isSuccessful: true, resourceSelfLinks: ['a', 'b', 'c', 'd'] } }, b: { response: { isSuccessful: false, resourceSelfLinks: ['e', 'f'] } }, c: { response: { isSuccessful: undefined, resourceSelfLinks: ['g', 'h', 'i'] } }, d: { response: { isSuccessful: true, resourceSelfLinks: ['j', 'k', 'l', 'm', 'n'] } }, - e: { response: { isSuccessful: 1, resourceSelfLinks: [] } } + e: { response: { isSuccessful: 1, resourceSelfLinks: [] } }, + f: { response: undefined }, + g: undefined }; const testResponses = { @@ -37,14 +39,14 @@ describe('Core Module - RxJS Operators', () => { scheduler = getTestScheduler(); }); - describe('getRequestFromSelflink', () => { + describe('getRequestFromRequestHref', () => { it('should return the RequestEntry corresponding to the self link in the source', () => { requestService = getMockRequestService(); - const source = hot('a', { a: testSelfLink }); - const result = source.pipe(getRequestFromSelflink(requestService)); - const expected = cold('a', { a: new RequestEntry()}); + const source = hot('a', { a: testRequestHref }); + const result = source.pipe(getRequestFromRequestHref(requestService)); + const expected = cold('a', { a: new RequestEntry() }); expect(result).toBeObservable(expected) }); @@ -52,18 +54,51 @@ describe('Core Module - RxJS Operators', () => { it('should use the requestService to fetch the request by its self link', () => { requestService = getMockRequestService(); - const source = hot('a', { a: testSelfLink }); - scheduler.schedule(() => source.pipe(getRequestFromSelflink(requestService)).subscribe()); + const source = hot('a', { a: testRequestHref }); + scheduler.schedule(() => source.pipe(getRequestFromRequestHref(requestService)).subscribe()); scheduler.flush(); - expect(requestService.getByHref).toHaveBeenCalledWith(testSelfLink) + expect(requestService.getByHref).toHaveBeenCalledWith(testRequestHref) }); it('shouldn\'t return anything if there is no request matching the self link', () => { requestService = getMockRequestService(cold('a', { a: undefined })); - const source = hot('a', { a: testSelfLink }); - const result = source.pipe(getRequestFromSelflink(requestService)); + const source = hot('a', { a: testRequestUUID }); + const result = source.pipe(getRequestFromRequestHref(requestService)); + const expected = cold('-'); + + expect(result).toBeObservable(expected) + }); + }); + + describe('getRequestFromRequestUUID', () => { + + it('should return the RequestEntry corresponding to the request uuid in the source', () => { + requestService = getMockRequestService(); + + const source = hot('a', { a: testRequestUUID }); + const result = source.pipe(getRequestFromRequestUUID(requestService)); + const expected = cold('a', { a: new RequestEntry() }); + + expect(result).toBeObservable(expected) + }); + + it('should use the requestService to fetch the request by its request uuid', () => { + requestService = getMockRequestService(); + + const source = hot('a', { a: testRequestUUID }); + scheduler.schedule(() => source.pipe(getRequestFromRequestUUID(requestService)).subscribe()); + scheduler.flush(); + + expect(requestService.getByUUID).toHaveBeenCalledWith(testRequestUUID) + }); + + it('shouldn\'t return anything if there is no request matching the request uuid', () => { + requestService = getMockRequestService(cold('a', { a: undefined })); + + const source = hot('a', { a: testRequestUUID }); + const result = source.pipe(getRequestFromRequestUUID(requestService)); const expected = cold('-'); expect(result).toBeObservable(expected) @@ -96,7 +131,7 @@ describe('Core Module - RxJS Operators', () => { describe('configureRequest', () => { it('should call requestService.configure with the source request', () => { requestService = getMockRequestService(); - const testRequest = new GetRequest('6b789e31-f026-4ff8-8993-4eb3b730c841', testSelfLink); + const testRequest = new GetRequest('6b789e31-f026-4ff8-8993-4eb3b730c841', testRequestHref); const source = hot('a', { a: testRequest }); scheduler.schedule(() => source.pipe(configureRequest(requestService)).subscribe()); scheduler.flush(); @@ -117,4 +152,20 @@ describe('Core Module - RxJS Operators', () => { expect(result).toBeObservable(expected) }); }); + + describe('getResponseFromEntry', () => { + it('should return the response for all not empty request entries, when they have a value', () => { + const source = hot('abcdefg', testRCEs); + const result = source.pipe(getResponseFromEntry()); + const expected = cold('abcde--', { + a: testRCEs.a.response, + b: testRCEs.b.response, + c: testRCEs.c.response, + d: testRCEs.d.response, + e: testRCEs.e.response + }); + + expect(result).toBeObservable(expected) + }); + }); }); diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index a6335ebb5d..4e6a95ff11 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -15,13 +15,20 @@ import { SearchResult } from '../../+search-page/search-result.model'; * This file contains custom RxJS operators that can be used in multiple places */ -export const getRequestFromSelflink = (requestService: RequestService) => +export const getRequestFromRequestHref = (requestService: RequestService) => (source: Observable): Observable => source.pipe( flatMap((href: string) => requestService.getByHref(href)), hasValueOperator() ); +export const getRequestFromRequestUUID = (requestService: RequestService) => + (source: Observable): Observable => + source.pipe( + flatMap((uuid: string) => requestService.getByUUID(uuid)), + hasValueOperator() + ); + export const filterSuccessfulResponses = () => (source: Observable): Observable => source.pipe( diff --git a/src/app/shared/mocks/mock-request.service.ts b/src/app/shared/mocks/mock-request.service.ts index d46100d56c..c947618ea1 100644 --- a/src/app/shared/mocks/mock-request.service.ts +++ b/src/app/shared/mocks/mock-request.service.ts @@ -2,10 +2,11 @@ import {of as observableOf, Observable } from 'rxjs'; import { RequestService } from '../../core/data/request.service'; import { RequestEntry } from '../../core/data/request.reducer'; -export function getMockRequestService(getByHref$: Observable = observableOf(new RequestEntry())): RequestService { +export function getMockRequestService(requestEntry$: Observable = observableOf(new RequestEntry())): RequestService { return jasmine.createSpyObj('requestService', { configure: false, generateRequestId: 'clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78', - getByHref: getByHref$ + getByHref: requestEntry$, + getByUUID: requestEntry$ }); } From dd439109a43c2b10dd5f683ea7385d6e8905a2a8 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 6 Nov 2018 09:57:27 +0100 Subject: [PATCH 54/69] 54472: Send normalized collections/communities instead of FormData --- .../create-collection-page.component.ts | 7 +++++-- .../create-community-page.component.ts | 7 +++++-- src/app/core/data/data.service.ts | 8 ++++++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index f3dbff9c1f..a8ee329a0f 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -9,6 +9,8 @@ import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { RemoteData } from '../../core/data/remote-data'; import { isNotEmpty } from '../../shared/empty.util'; +import { NormalizedCollection } from '../../core/cache/models/normalized-collection.model'; +import { ResourceType } from '../../core/shared/resource-type'; @Component({ selector: 'ds-create-collection', @@ -36,7 +38,7 @@ export class CreateCollectionPageComponent { onSubmit(data: any) { this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { - const collection = Object.assign(new Collection(), { + const collection = Object.assign(new NormalizedCollection(), { name: data.name, metadata: [ { key: 'dc.description', value: data.introductory }, @@ -44,7 +46,8 @@ export class CreateCollectionPageComponent { { key: 'dc.rights', value: data.copyright }, { key: 'dc.rights.license', value: data.license } // TODO: metadata for news and provenance - ] + ], + type: ResourceType.Collection }); this.collectionDataService.create(collection, uuid).subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index 8c75af0d64..0d97f24028 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -8,6 +8,8 @@ import { RemoteData } from '../../core/data/remote-data'; import { isNotEmpty } from '../../shared/empty.util'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { take } from 'rxjs/operators'; +import { ResourceType } from '../../core/shared/resource-type'; +import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model'; @Component({ selector: 'ds-create-community', @@ -34,14 +36,15 @@ export class CreateCommunityPageComponent { onSubmit(data: any) { this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { - const community = Object.assign(new Community(), { + const community = Object.assign(new NormalizedCommunity(), { name: data.name, metadata: [ { key: 'dc.description', value: data.introductory }, { key: 'dc.description.abstract', value: data.description }, { key: 'dc.rights', value: data.copyright } // TODO: metadata for news - ] + ], + type: ResourceType.Community }); this.communityDataService.create(community, uuid).pipe(take(1)).subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 6a82577aa5..db3882ac08 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -30,6 +30,8 @@ import { } from '../shared/operators'; import { DSOSuccessResponse, ErrorResponse, RestResponse } from '../cache/response.models'; import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; +import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; +import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; export abstract class DataService { protected abstract requestService: RequestService; @@ -128,7 +130,7 @@ export abstract class DataService } } - create(dso: TDomain, parentUUID: string): Observable> { + create(dso: TNormalized, parentUUID: string): Observable> { const requestId = this.requestService.generateRequestId(); const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe( isNotEmptyOperator(), @@ -136,9 +138,11 @@ export abstract class DataService map((endpoint: string) => parentUUID ? `${endpoint}?parent=${parentUUID}` : endpoint) ); + const serializedDso = new DSpaceRESTv2Serializer(NormalizedObjectFactory.getConstructor(dso.type)).serialize(dso); + const request$ = endpoint$.pipe( take(1), - map((endpoint: string) => new CreateRequest(requestId, endpoint, dso)) + map((endpoint: string) => new CreateRequest(requestId, endpoint, JSON.stringify(serializedDso))) ); // Execute the post request From 9911578bcc760463df17a71869f5f2064000ab9d Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 19 Dec 2018 14:53:11 +0100 Subject: [PATCH 55/69] remove all instances of first as it can cause an error if it's never triggered --- .../search-facet-filter.component.ts | 8 ++++---- .../search-filter/search-filter.component.ts | 4 ++-- src/app/app.component.ts | 2 +- src/app/core/auth/auth.effects.ts | 4 ++-- src/app/core/auth/auth.service.ts | 4 ++-- src/app/core/auth/server-auth.service.ts | 4 ++-- .../cache/builders/remote-data-build.service.ts | 12 ++++++++++-- src/app/core/cache/object-cache.service.ts | 6 +++--- src/app/core/cache/server-sync-buffer.effects.ts | 6 +++--- src/app/core/data/data.service.ts | 4 ++-- src/app/core/metadata/metadata.service.ts | 14 +++++++++++--- src/app/core/shared/operators.ts | 4 ++-- 12 files changed, 44 insertions(+), 28 deletions(-) diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts index 4a171a3f3a..faaf3b9fb5 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts @@ -6,7 +6,7 @@ import { Subject, Subscription } from 'rxjs'; -import { switchMap, distinctUntilChanged, first, map } from 'rxjs/operators'; +import { switchMap, distinctUntilChanged, first, map, take } from 'rxjs/operators'; import { animate, state, style, transition, trigger } from '@angular/animations'; import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @@ -126,7 +126,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { this.animationState = 'ready'; 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)) })); })); @@ -189,7 +189,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { * @param data The string from the input field */ onSubmit(data: any) { - this.selectedValues.pipe(first()).subscribe((selectedValues) => { + this.selectedValues.pipe(take(1)).subscribe((selectedValues) => { if (isNotEmpty(data)) { this.router.navigate([this.getSearchLink()], { queryParams: @@ -258,7 +258,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { */ findSuggestions(data): void { if (isNotEmpty(data)) { - this.searchConfigService.searchOptions.pipe(first()).subscribe( + this.searchConfigService.searchOptions.pipe(take(1)).subscribe( (options) => { this.filterSearchResults = this.searchService.getFacetValuesFor(this.filterConfig, 1, options, data.toLowerCase()) .pipe( diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts index 87f8edc1ea..ec239e3628 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts @@ -1,5 +1,5 @@ -import {first} from 'rxjs/operators'; +import { first, take } from 'rxjs/operators'; import { Component, Input, OnInit } from '@angular/core'; import { SearchFilterConfig } from '../../search-service/search-filter-config.model'; import { SearchFilterService } from './search-filter.service'; @@ -37,7 +37,7 @@ export class SearchFilterComponent implements OnInit { * Else, the filter should initially be collapsed */ ngOnInit() { - this.getSelectedValues().pipe(first()).subscribe((isActive) => { + this.getSelectedValues().pipe(take(1)).subscribe((isActive) => { if (this.filter.isOpenByDefault || isNotEmpty(isActive)) { this.initialExpand(); } else { diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 7d4bfe4f33..32ba6e752f 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -64,7 +64,7 @@ export class AppComponent implements OnInit, AfterViewInit { // Whether is not authenticathed try to retrieve a possible stored auth token this.store.pipe(select(isAuthenticated), - first(), + take(1), filter((authenticated) => !authenticated) ).subscribe((authenticated) => this.authService.checkAuthenticationToken()); diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index c57fa3f70e..56a5411ef2 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -47,7 +47,7 @@ export class AuthEffects { ofType(AuthActionTypes.AUTHENTICATE), switchMap((action: AuthenticateAction) => { return this.authService.authenticate(action.payload.email, action.payload.password).pipe( - first(), + take(1), map((response: AuthStatus) => new AuthenticationSuccessAction(response.token)), catchError((error) => observableOf(new AuthenticationErrorAction(error))) ); @@ -127,7 +127,7 @@ export class AuthEffects { switchMap(() => { return this.store.pipe( select(isAuthenticated), - first(), + take(1), filter((authenticated) => !authenticated), tap(() => this.authService.removeToken()), tap(() => this.authService.resetAuthenticationError()) diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 67f533d4ad..66fe65a22e 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -277,7 +277,7 @@ export class AuthService { public isTokenExpiring(): Observable { return this.store.pipe( select(isTokenRefreshing), - first(), + take(1), map((isRefreshing: boolean) => { if (this.isTokenExpired() || isRefreshing) { return false; @@ -360,7 +360,7 @@ export class AuthService { */ public redirectToPreviousUrl() { this.getRedirectUrl().pipe( - first()) + take(1)) .subscribe((redirectUrl) => { if (isNotEmpty(redirectUrl)) { diff --git a/src/app/core/auth/server-auth.service.ts b/src/app/core/auth/server-auth.service.ts index c83410f6e3..903926fbcf 100644 --- a/src/app/core/auth/server-auth.service.ts +++ b/src/app/core/auth/server-auth.service.ts @@ -1,4 +1,4 @@ -import { first, map, switchMap } from 'rxjs/operators'; +import { first, map, switchMap, take } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; @@ -62,7 +62,7 @@ export class ServerAuthService extends AuthService { */ public redirectToPreviousUrl() { this.getRedirectUrl().pipe( - first()) + take(1)) .subscribe((redirectUrl) => { if (isNotEmpty(redirectUrl)) { // override the route reuse strategy diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 52ec4382ae..1dc255f26e 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -5,7 +5,15 @@ import { race as observableRace } from 'rxjs'; import { Injectable } from '@angular/core'; -import { distinctUntilChanged, first, flatMap, map, startWith, switchMap } from 'rxjs/operators'; +import { + distinctUntilChanged, + first, + flatMap, + map, + startWith, + switchMap, + take +} from 'rxjs/operators'; import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util'; import { PaginatedList } from '../../data/paginated-list'; import { RemoteData } from '../../data/remote-data'; @@ -44,7 +52,7 @@ export class RemoteDataBuildService { href$.pipe(getRequestFromRequestHref(this.requestService)), requestUUID$.pipe(getRequestFromRequestUUID(this.requestService)), ).pipe( - first() + take(1) ); // always use self link if that is cached, only if it isn't, get it via the response. diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 44297d6f61..40f41be14d 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -1,6 +1,6 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { distinctUntilChanged, filter, first, map, mergeMap, } from 'rxjs/operators'; +import { distinctUntilChanged, filter, first, map, mergeMap, take, } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { MemoizedSelector, select, Store } from '@ngrx/store'; import { IndexName } from '../index/index.reducer'; @@ -165,7 +165,7 @@ export class ObjectCacheService { this.store.pipe( select(selfLinkFromUuidSelector(uuid)), - first() + take(1) ).subscribe((selfLink: string) => result = this.hasBySelfLink(selfLink)); return result; @@ -184,7 +184,7 @@ export class ObjectCacheService { let result = false; this.store.pipe(select(entryFromSelfLinkSelector(selfLink)), - first() + take(1) ).subscribe((entry: ObjectCacheEntry) => result = this.isValid(entry)); return result; diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts index 5a0a5527d1..d0a194705b 100644 --- a/src/app/core/cache/server-sync-buffer.effects.ts +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -1,4 +1,4 @@ -import { delay, exhaustMap, first, map, switchMap, tap } from 'rxjs/operators'; +import { delay, exhaustMap, first, map, switchMap, take, tap } from 'rxjs/operators'; import { Inject, Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { @@ -56,7 +56,7 @@ export class ServerSyncBufferEffects { switchMap((action: CommitSSBAction) => { return this.store.pipe( select(serverSyncBufferSelector()), - first(), /* necessary, otherwise delay will not have any effect after the first run */ + take(1), /* necessary, otherwise delay will not have any effect after the first run */ switchMap((bufferState: ServerSyncBufferState) => { const actions: Array> = bufferState.buffer .filter((entry: ServerSyncBufferEntry) => { @@ -95,7 +95,7 @@ export class ServerSyncBufferEffects { * @returns {Observable} ApplyPatchObjectCacheAction to be dispatched */ private applyPatch(href: string): Observable { - const patchObject = this.objectCache.getBySelfLink(href).pipe(first()); + const patchObject = this.objectCache.getBySelfLink(href).pipe(take(1)); return patchObject.pipe( map((object) => { diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 6a7916854b..6afc84df5a 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -1,4 +1,4 @@ -import { delay, distinctUntilChanged, filter, first, map, take, tap } from 'rxjs/operators'; +import { delay, distinctUntilChanged, filter, find, first, map, take, tap } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { Store } from '@ngrx/store'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; @@ -74,7 +74,7 @@ export abstract class DataService map((endpoint: string) => this.getFindByIDHref(endpoint, id))); hrefObs.pipe( - first((href: string) => hasValue(href))) + find((href: string) => hasValue(href))) .subscribe((href: string) => { const request = new FindByIDRequest(this.requestService.generateRequestId(), href, id); this.requestService.configure(request); diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 8bec058d08..e73613fddb 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -1,4 +1,12 @@ -import { catchError, distinctUntilKeyChanged, filter, first, map, take } from 'rxjs/operators'; +import { + catchError, + distinctUntilKeyChanged, + filter, + find, + first, + map, + take +} from 'rxjs/operators'; import { Inject, Injectable } from '@angular/core'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; @@ -261,7 +269,7 @@ export class MetadataService { const item = this.currentObject.value as Item; item.getFiles() .pipe( - first((files) => isNotEmpty(files)), + find((files) => isNotEmpty(files)), catchError((error) => { console.debug(error.message); return [] @@ -269,7 +277,7 @@ export class MetadataService { .subscribe((bitstreams: Bitstream[]) => { for (const bitstream of bitstreams) { bitstream.format.pipe( - first(), + take(1), catchError((error: Error) => { console.debug(error.message); return [] diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index 4e6a95ff11..388c4289e2 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -1,5 +1,5 @@ import { Observable } from 'rxjs'; -import { filter, first, flatMap, map, tap } from 'rxjs/operators'; +import { filter, find, first, flatMap, map, tap } from 'rxjs/operators'; import { hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util'; import { DSOSuccessResponse, RestResponse } from '../cache/response.models'; import { RemoteData } from '../data/remote-data'; @@ -60,7 +60,7 @@ export const getRemoteDataPayload = () => export const getSucceededRemoteData = () => (source: Observable>): Observable> => - source.pipe(first((rd: RemoteData) => rd.hasSucceeded)); + source.pipe(find((rd: RemoteData) => rd.hasSucceeded)); export const toDSpaceObjectListRD = () => (source: Observable>>>): Observable>> => From 7a12332d700c8c09cb177aad8902bcb0bd0a5e5a Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 20 Dec 2018 15:54:47 +0100 Subject: [PATCH 56/69] Edit and create communities --- resources/i18n/en.json | 1 + .../create-collection-page.component.ts | 2 +- .../community-form.component.html | 31 +----- .../community-form.component.ts | 94 +++++++++++++++---- .../community-page-routing.module.ts | 12 ++- .../community-page.component.html | 2 + .../community-page.component.ts | 7 -- .../+community-page/community-page.module.ts | 2 + .../create-community-page.component.html | 2 +- .../create-community-page.component.spec.ts | 2 - .../create-community-page.component.ts | 47 ++++------ .../edit-community-page.component.html | 11 +++ .../edit-community-page.component.scss | 1 + .../edit-community-page.component.spec.ts | 90 ++++++++++++++++++ .../edit-community-page.component.ts | 46 +++++++++ .../core/cache/builders/data-build.service.ts | 36 +++++++ .../builders/remote-data-build.service.ts | 3 - .../models/normalized-community.model.ts | 12 +-- .../models/normalized-dspace-object.model.ts | 14 +-- src/app/core/core.module.ts | 4 + .../data/base-response-parsing.service.ts | 20 ++-- src/app/core/data/collection-data.service.ts | 9 +- src/app/core/data/comcol-data.service.spec.ts | 12 ++- src/app/core/data/comcol-data.service.ts | 3 +- src/app/core/data/community-data.service.ts | 6 +- .../config-response-parsing.service.spec.ts | 2 +- src/app/core/data/data.service.ts | 42 +++++++-- src/app/core/data/dso-update-comparator.ts | 12 +++ .../core/data/dspace-object-data.service.ts | 12 ++- src/app/core/data/item-data.service.ts | 6 +- src/app/core/data/paginated-list.ts | 8 ++ src/app/core/data/request.service.ts | 2 +- src/app/core/data/update-comparator.ts | 6 ++ src/app/core/shared/dspace-object.model.ts | 7 +- src/app/core/shared/page-info.model.ts | 3 + .../ds-dynamic-form-control.component.ts | 1 - src/app/shared/form/form.component.html | 2 +- src/app/shared/form/form.component.ts | 4 +- src/app/shared/form/form.service.ts | 2 +- 39 files changed, 427 insertions(+), 151 deletions(-) create mode 100644 src/app/+community-page/edit-community-page/edit-community-page.component.html create mode 100644 src/app/+community-page/edit-community-page/edit-community-page.component.scss create mode 100644 src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts create mode 100644 src/app/+community-page/edit-community-page/edit-community-page.component.ts create mode 100644 src/app/core/cache/builders/data-build.service.ts create mode 100644 src/app/core/data/dso-update-comparator.ts create mode 100644 src/app/core/data/update-comparator.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 814adba9a7..a4f9014980 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -42,6 +42,7 @@ "head": "Collections of this Community" }, "edit": { + "head": "Edit collction", "name": "Name", "description": "Short Description", "introductory": "Introductory text (HTML)", diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index a8ee329a0f..d934e1d7a0 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -38,7 +38,7 @@ export class CreateCollectionPageComponent { onSubmit(data: any) { this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { - const collection = Object.assign(new NormalizedCollection(), { + const collection = Object.assign(new Collection(), { name: data.name, metadata: [ { key: 'dc.description', value: data.introductory }, diff --git a/src/app/+community-page/community-form/community-form.component.html b/src/app/+community-page/community-form/community-form.component.html index 578e2a0d6a..637506f86d 100644 --- a/src/app/+community-page/community-form/community-form.component.html +++ b/src/app/+community-page/community-form/community-form.component.html @@ -1,27 +1,4 @@ -
-
- - -
{{ 'community.edit.required.name' | translate }}
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
+ + diff --git a/src/app/+community-page/community-form/community-form.component.ts b/src/app/+community-page/community-form/community-form.component.ts index d7797028b7..fd0b490146 100644 --- a/src/app/+community-page/community-form/community-form.component.ts +++ b/src/app/+community-page/community-form/community-form.component.ts @@ -1,39 +1,93 @@ -import { Component, EventEmitter, Output } from '@angular/core'; -import { isNotEmpty } from '../../shared/empty.util'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Location } from '@angular/common'; +import { + DynamicFormService, + DynamicInputModel, + DynamicTextAreaModel +} from '@ng-dynamic-forms/core'; +import { FormGroup } from '@angular/forms'; +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 { hasValue, isNotEmpty } from '../../shared/empty.util'; @Component({ selector: 'ds-community-form', styleUrls: ['./community-form.component.scss'], templateUrl: './community-form.component.html' }) -export class CommunityFormComponent { +export class CommunityFormComponent implements OnInit { - name: string; - description: string; - introductory: string; - copyright: string; - news: string; + @Input() community: Community = new Community(); + formModel: DynamicFormControlModel[] = [ + new DynamicInputModel({ + id: 'title', + name: 'dc.title', + label: 'Name', + required: true, + validators: { + required: null + }, + errorMessages: { + required: 'Please enter a name for this title' + } + }), + new DynamicTextAreaModel({ + id: 'description', + name: 'dc.description', + label: 'Introductory text (HTML)', + }), + new DynamicTextAreaModel({ + id: 'abstract', + name: 'dc.description.abstract', + label: 'Short Description', + }), + new DynamicTextAreaModel({ + id: 'rights', + name: 'dc.rights', + label: 'Copyright text (HTML)', + }), + new DynamicTextAreaModel({ + id: 'tableofcontents', + name: 'dc.description.tableofcontents', + label: 'News (HTML)', + }), + ]; - nameRequiredError = false; + formGroup: FormGroup; - @Output() submitted: EventEmitter = new EventEmitter(); - - public constructor(private location: Location) { + @Output() submitForm: EventEmitter = new EventEmitter(); + public constructor(private location: Location, private formService: DynamicFormService) { } - onSubmit(data: any) { - if (isNotEmpty(data.name)) { - this.submitted.emit(data); - this.nameRequiredError = false; - } else { - this.nameRequiredError = true; - } + ngOnInit(): void { + this.formModel.forEach( + (fieldModel: DynamicInputModel) => { + fieldModel.value = this.community.findMetadata(fieldModel.name); + } + ); + this.formGroup = this.formService.createFormGroup(this.formModel); + } + + onSubmit(event: Event) { + event.stopPropagation(); + const metadata = this.formModel.map( + (fieldModel: DynamicInputModel) => { + return { key: fieldModel.name, value: fieldModel.value } + } + ); + const filteredOldMetadata = this.community.metadata.filter((filter) => !metadata.map((md) => md.key).includes(filter.key)); + const filteredNewMetadata = metadata.filter((md) => isNotEmpty(md.value)); + const newMetadata = [...filteredOldMetadata, ...filteredNewMetadata]; + const updatedCommunity = Object.assign({}, this.community, { + metadata: newMetadata, + type: ResourceType.Community + }); + this.submitForm.emit(updatedCommunity); } cancel() { this.location.back(); } - } diff --git a/src/app/+community-page/community-page-routing.module.ts b/src/app/+community-page/community-page-routing.module.ts index 17889d6e48..8fcc2cde6f 100644 --- a/src/app/+community-page/community-page-routing.module.ts +++ b/src/app/+community-page/community-page-routing.module.ts @@ -5,13 +5,23 @@ import { CommunityPageComponent } from './community-page.component'; 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'; @NgModule({ imports: [ RouterModule.forChild([ { path: 'create', component: CreateCommunityPageComponent, - canActivate: [AuthenticatedGuard] }, + canActivate: [AuthenticatedGuard] + }, + { path: ':id/edit', + pathMatch: 'full', + component: EditCommunityPageComponent, + canActivate: [AuthenticatedGuard], + resolve: { + community: CommunityPageResolver + } + }, { path: ':id', component: CommunityPageComponent, diff --git a/src/app/+community-page/community-page.component.html b/src/app/+community-page/community-page.component.html index 1bf322a688..52bc9fe928 100644 --- a/src/app/+community-page/community-page.component.html +++ b/src/app/+community-page/community-page.component.html @@ -28,6 +28,8 @@ [community]="communityPayload"> + Edit + diff --git a/src/app/+community-page/community-page.component.ts b/src/app/+community-page/community-page.component.ts index ce260aefc0..bfaac33c1c 100644 --- a/src/app/+community-page/community-page.component.ts +++ b/src/app/+community-page/community-page.component.ts @@ -24,8 +24,6 @@ import { hasValue } from '../shared/empty.util'; export class CommunityPageComponent implements OnInit, OnDestroy { communityRD$: Observable>; logoRD$: Observable>; - - private subs: Subscription[] = []; constructor( @@ -42,14 +40,9 @@ export class CommunityPageComponent implements OnInit, OnDestroy { map((rd: RemoteData) => rd.payload), filter((community: Community) => hasValue(community)), mergeMap((community: Community) => community.logo)); - - } ngOnDestroy(): void { this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); } - - - } diff --git a/src/app/+community-page/community-page.module.ts b/src/app/+community-page/community-page.module.ts index 292e6aaf9c..23050f40db 100644 --- a/src/app/+community-page/community-page.module.ts +++ b/src/app/+community-page/community-page.module.ts @@ -8,6 +8,7 @@ import { CommunityPageSubCollectionListComponent } from './sub-collection-list/c import { CommunityPageRoutingModule } from './community-page-routing.module'; 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'; @NgModule({ imports: [ @@ -19,6 +20,7 @@ import { CommunityFormComponent } from './community-form/community-form.componen CommunityPageComponent, CommunityPageSubCollectionListComponent, CreateCommunityPageComponent, + EditCommunityPageComponent, CommunityFormComponent ] }) diff --git a/src/app/+community-page/create-community-page/create-community-page.component.html b/src/app/+community-page/create-community-page/create-community-page.component.html index 35f1deaa73..bb0f5b83af 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.html +++ b/src/app/+community-page/create-community-page/create-community-page.component.html @@ -7,5 +7,5 @@ - + diff --git a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts index 6c5eb5f591..e76b10bde6 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts @@ -8,12 +8,10 @@ import { Observable } from 'rxjs/Observable'; import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; import { DSOSuccessResponse, ErrorResponse } from '../../core/cache/response-cache.models'; -import { BrowserModule } from '@angular/platform-browser'; import { SharedModule } from '../../shared/shared.module'; import { CommonModule } from '@angular/common'; import { CommunityFormComponent } from '../community-form/community-form.component'; import { RouterTestingModule } from '@angular/router/testing'; -import { RequestError } from '../../core/data/request.models'; describe('CreateCommunityPageComponent', () => { let comp: CreateCommunityPageComponent; diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index 0d97f24028..5373abfe22 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { Community } from '../../core/shared/community.model'; import { CommunityDataService } from '../../core/data/community-data.service'; import { Observable } from 'rxjs'; @@ -7,55 +7,46 @@ import { Router } from '@angular/router'; import { RemoteData } from '../../core/data/remote-data'; import { isNotEmpty } from '../../shared/empty.util'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; -import { take } from 'rxjs/operators'; -import { ResourceType } from '../../core/shared/resource-type'; -import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model'; +import { map, take } from 'rxjs/operators'; +import { getSucceededRemoteData } from '../../core/shared/operators'; @Component({ selector: 'ds-create-community', styleUrls: ['./create-community-page.component.scss'], templateUrl: './create-community-page.component.html' }) -export class CreateCommunityPageComponent { +export class CreateCommunityPageComponent implements OnInit { public parentUUID$: Observable; - public communityRDObs: Observable>; + public parentRD$: Observable>; public constructor( private communityDataService: CommunityDataService, private routeService: RouteService, private router: Router ) { + + } + + ngOnInit(): void { this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); - this.parentUUID$.subscribe((uuid: string) => { - if (isNotEmpty(uuid)) { - this.communityRDObs = this.communityDataService.findById(uuid); + this.parentUUID$.subscribe((parentID: string) => { + if (isNotEmpty(parentID)) { + this.parentRD$ = this.communityDataService.findById(parentID); } }); } - onSubmit(data: any) { + onSubmit(community: Community) { this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { - const community = Object.assign(new NormalizedCommunity(), { - name: data.name, - metadata: [ - { key: 'dc.description', value: data.introductory }, - { key: 'dc.description.abstract', value: data.description }, - { key: 'dc.rights', value: data.copyright } - // TODO: metadata for news - ], - type: ResourceType.Community - }); - this.communityDataService.create(community, uuid).pipe(take(1)).subscribe((rd: RemoteData) => { - if (rd.hasSucceeded) { - if (uuid) { - this.router.navigate(['communities', uuid]); - } else { - this.router.navigate([]); - } - } + this.communityDataService.create(community, uuid) + .pipe(getSucceededRemoteData()) + .subscribe((communityRD: RemoteData) => { + const newUUID = communityRD.payload.uuid; + this.router.navigate(['/communities/' + newUUID]); }); }); } + } diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.html b/src/app/+community-page/edit-community-page/edit-community-page.component.html new file mode 100644 index 0000000000..eb9f797b3d --- /dev/null +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.html @@ -0,0 +1,11 @@ +
+
+
+ + +

{{ 'community.edit.sub-head' | translate:{ parent: parent.name } }}

+
+
+
+ +
diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.scss b/src/app/+community-page/edit-community-page/edit-community-page.component.scss new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.scss @@ -0,0 +1 @@ + diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts b/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts new file mode 100644 index 0000000000..6c5eb5f591 --- /dev/null +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts @@ -0,0 +1,90 @@ +import { CreateCommunityPageComponent } from './create-community-page.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { RouteService } from '../../shared/services/route.service'; +import { Router } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { Observable } from 'rxjs/Observable'; +import { RemoteData } from '../../core/data/remote-data'; +import { Community } from '../../core/shared/community.model'; +import { DSOSuccessResponse, ErrorResponse } from '../../core/cache/response-cache.models'; +import { BrowserModule } from '@angular/platform-browser'; +import { SharedModule } from '../../shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { CommunityFormComponent } from '../community-form/community-form.component'; +import { RouterTestingModule } from '@angular/router/testing'; +import { RequestError } from '../../core/data/request.models'; + +describe('CreateCommunityPageComponent', () => { + let comp: CreateCommunityPageComponent; + let fixture: ComponentFixture; + let communityDataService: CommunityDataService; + let routeService: RouteService; + let router: Router; + + const community = Object.assign(new Community(), { + uuid: 'a20da287-e174-466a-9926-f66b9300d347', + name: 'test community' + }); + + const newCommunity = Object.assign(new Community(), { + uuid: '1ff59938-a69a-4e62-b9a4-718569c55d48', + name: 'new community' + }); + + const communityDataServiceStub = { + findById: (uuid) => Observable.of(new RemoteData(false, false, true, null, Object.assign(new Community(), { + uuid: uuid, + name: community.name + }))), + create: (com, uuid?) => Observable.of(new RemoteData(false, false, true, undefined, newCommunity)) + }; + const routeServiceStub = { + getQueryParameterValue: (param) => Observable.of(community.uuid) + }; + const routerStub = { + navigate: (commands) => commands + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + declarations: [CreateCommunityPageComponent, CommunityFormComponent], + providers: [ + { provide: CommunityDataService, useValue: communityDataServiceStub }, + { provide: RouteService, useValue: routeServiceStub }, + { provide: Router, useValue: routerStub } + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CreateCommunityPageComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + communityDataService = (comp as any).communityDataService; + routeService = (comp as any).routeService; + router = (comp as any).router; + }); + + describe('onSubmit', () => { + const data = { + name: 'test' + }; + + it('should navigate when successful', () => { + spyOn(router, 'navigate'); + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should not navigate on failure', () => { + spyOn(router, 'navigate'); + spyOn(communityDataService, 'create').and.returnValue(Observable.of(new RemoteData(true, true, false, undefined, newCommunity))); + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigate).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.ts b/src/app/+community-page/edit-community-page/edit-community-page.component.ts new file mode 100644 index 0000000000..58805af80a --- /dev/null +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.ts @@ -0,0 +1,46 @@ +import { Component } from '@angular/core'; +import { Community } from '../../core/shared/community.model'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { Observable } from 'rxjs'; +import { RouteService } from '../../shared/services/route.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { RemoteData } from '../../core/data/remote-data'; +import { isNotEmpty } from '../../shared/empty.util'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { first, map, take, tap } from 'rxjs/operators'; +import { ResourceType } from '../../core/shared/resource-type'; +import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model'; +import { getSucceededRemoteData } from '../../core/shared/operators'; + +@Component({ + selector: 'ds-edit-community', + styleUrls: ['./edit-community-page.component.scss'], + templateUrl: './edit-community-page.component.html' +}) +export class EditCommunityPageComponent { + + public parentUUID$: Observable; + public parentRD$: Observable>; + public communityRD$: Observable>; + + public constructor( + private communityDataService: CommunityDataService, + private routeService: RouteService, + private router: Router, + private route: ActivatedRoute + ) { + } + + ngOnInit(): void { + this.communityRD$ = this.route.data.pipe(first(), map((data) => data.community)); + } + + onSubmit(community: Community) { + this.communityDataService.update(community) + .pipe(getSucceededRemoteData()) + .subscribe((communityRD: RemoteData) => { + const newUUID = communityRD.payload.uuid; + this.router.navigate(['/communities/' + newUUID]); + }); + } +} diff --git a/src/app/core/cache/builders/data-build.service.ts b/src/app/core/cache/builders/data-build.service.ts new file mode 100644 index 0000000000..8ba3ebee0c --- /dev/null +++ b/src/app/core/cache/builders/data-build.service.ts @@ -0,0 +1,36 @@ +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 { map, take } from 'rxjs/operators'; +import { hasValue, isNotEmpty } from '../../../shared/empty.util'; +import { PaginatedList } from '../../data/paginated-list'; + +export function isRestDataObject(halObj: any) { + return isNotEmpty(halObj._links) && hasValue(halObj._links.self); +} + +export function isRestPaginatedList(halObj: any) { + return hasValue(halObj.page) && hasValue(halObj._embedded); +} + +export function isPaginatedList(halObj: any) { + return hasValue(halObj.page) && hasValue(halObj.pageInfo); +} + +@Injectable() +export class DataBuildService { + normalize(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; + } +} diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 1dc255f26e..7b51526432 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -51,10 +51,7 @@ export class RemoteDataBuildService { const requestEntry$ = observableRace( href$.pipe(getRequestFromRequestHref(this.requestService)), requestUUID$.pipe(getRequestFromRequestUUID(this.requestService)), - ).pipe( - take(1) ); - // always use self link if that is cached, only if it isn't, get it via the response. const payload$ = observableCombineLatest( diff --git a/src/app/core/cache/models/normalized-community.model.ts b/src/app/core/cache/models/normalized-community.model.ts index 4ab2408a53..e915d2f50a 100644 --- a/src/app/core/cache/models/normalized-community.model.ts +++ b/src/app/core/cache/models/normalized-community.model.ts @@ -1,4 +1,4 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; +import { autoserialize, deserialize, inheritSerialization, serialize } from 'cerialize'; import { NormalizedDSpaceObject } from './normalized-dspace-object.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 */ - @autoserialize + @deserialize @relationship(ResourceType.Bitstream, false) logo: string; /** * An array of Communities that are direct parents of this Community */ - @autoserialize + @deserialize @relationship(ResourceType.Community, true) parents: string[]; /** * The Community that owns this Community */ - @autoserialize + @deserialize @relationship(ResourceType.Community, false) owner: string; /** * List of Collections that are owned by this Community */ - @autoserialize + @deserialize @relationship(ResourceType.Collection, true) collections: string[]; - @autoserialize + @deserialize @relationship(ResourceType.Community, true) subcommunities: string[]; diff --git a/src/app/core/cache/models/normalized-dspace-object.model.ts b/src/app/core/cache/models/normalized-dspace-object.model.ts index 92174c40f7..efdfa6dd74 100644 --- a/src/app/core/cache/models/normalized-dspace-object.model.ts +++ b/src/app/core/cache/models/normalized-dspace-object.model.ts @@ -1,4 +1,4 @@ -import { autoserialize, autoserializeAs } from 'cerialize'; +import { autoserialize, autoserializeAs, deserialize, serialize } from 'cerialize'; import { DSpaceObject } from '../../shared/dspace-object.model'; import { Metadatum } from '../../shared/metadatum.model'; @@ -45,12 +45,6 @@ export class NormalizedDSpaceObject extends NormalizedObject { @autoserialize type: ResourceType; - /** - * The name for this DSpaceObject - */ - @autoserialize - name: string; - /** * 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 */ - @autoserialize + @deserialize parents: string[]; /** * The DSpaceObject that owns this DSpaceObject */ - @autoserialize + @deserialize owner: string; /** @@ -75,7 +69,7 @@ export class NormalizedDSpaceObject extends NormalizedObject { * Repeated here to make the serialization work, * inheritSerialization doesn't seem to work for more than one level */ - @autoserialize + @deserialize _links: { [name: string]: string } diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index dcbdbd0049..2191743871 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -63,6 +63,8 @@ import { NotificationsService } from '../shared/notifications/notifications.serv import { UploaderService } from '../shared/uploader/uploader.service'; import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service'; import { DSpaceObjectDataService } from './data/dspace-object-data.service'; +import { DataBuildService } from './cache/builders/data-build.service'; +import { DSOUpdateComparator } from './data/dso-update-comparator'; const IMPORTS = [ CommonModule, @@ -99,6 +101,7 @@ const PROVIDERS = [ ObjectCacheService, PaginationComponentOptions, RegistryService, + DataBuildService, RemoteDataBuildService, RequestService, EndpointMapResponseParsingService, @@ -126,6 +129,7 @@ const PROVIDERS = [ UploaderService, UUIDService, DSpaceObjectDataService, + DSOUpdateComparator, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index eada156ce9..d5c1c58296 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -8,15 +8,7 @@ import { GenericConstructor } from '../shared/generic-constructor'; import { PaginatedList } from './paginated-list'; import { ResourceType } from '../shared/resource-type'; import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; - -function isObjectLevel(halObj: any) { - return isNotEmpty(halObj._links) && hasValue(halObj._links.self); -} - -function isPaginatedResponse(halObj: any) { - return hasValue(halObj.page) && hasValue(halObj._embedded); -} - +import { isRestDataObject, isRestPaginatedList } from '../cache/builders/data-build.service'; /* tslint:disable:max-classes-per-file */ export abstract class BaseResponseParsingService { @@ -29,11 +21,11 @@ export abstract class BaseResponseParsingService { if (isNotEmpty(data)) { if (hasNoValue(data) || (typeof data !== 'object')) { return data; - } else if (isPaginatedResponse(data)) { + } else if (isRestPaginatedList(data)) { return this.processPaginatedList(data, requestUUID); } else if (Array.isArray(data)) { return this.processArray(data, requestUUID); - } else if (isObjectLevel(data)) { + } else if (isRestDataObject(data)) { data = this.fixBadEPersonRestResponse(data); const object = this.deserialize(data); if (isNotEmpty(data._embedded)) { @@ -43,10 +35,10 @@ export abstract class BaseResponseParsingService { .forEach((property) => { const parsedObj = this.process(data._embedded[property], requestUUID); if (isNotEmpty(parsedObj)) { - if (isPaginatedResponse(data._embedded[property])) { + if (isRestPaginatedList(data._embedded[property])) { object[property] = parsedObj; object[property].page = parsedObj.page.map((obj) => obj.self); - } else if (isObjectLevel(data._embedded[property])) { + } else if (isRestDataObject(data._embedded[property])) { object[property] = parsedObj.self; } else if (Array.isArray(parsedObj)) { object[property] = parsedObj.map((obj) => obj.self) @@ -80,7 +72,7 @@ export abstract class BaseResponseParsingService { list = this.flattenSingleKeyObject(list); } const page: ObjectDomain[] = this.processArray(list, requestUUID); - return new PaginatedList(pageInfo, page); + return new PaginatedList(pageInfo, page, ); } protected processArray(data: any, requestUUID: string): ObjectDomain[] { diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index b936d71c30..ef294c1296 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NormalizedCollection } from '../cache/models/normalized-collection.model'; @@ -10,9 +10,10 @@ import { CommunityDataService } from './community-data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { AuthService } from '../auth/auth.service'; -import { Community } from '../shared/community.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; +import { DataBuildService } from '../cache/builders/data-build.service'; +import { DSOUpdateComparator } from './dso-update-comparator'; @Injectable() export class CollectionDataService extends ComColDataService { @@ -21,13 +22,15 @@ export class CollectionDataService extends ComColDataService, protected cds: CommunityDataService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, protected authService: AuthService, protected notificationsService: NotificationsService, - protected http: HttpClient + protected http: HttpClient, + protected comparator: DSOUpdateComparator ) { super(); } diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index 3325dc6ac3..0bf5f85749 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -14,10 +14,12 @@ import { NormalizedObject } from '../cache/models/normalized-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RequestEntry } from './request.reducer'; import { of as observableOf } from 'rxjs'; -import { Community } from '../shared/community.model'; import { AuthService } from '../auth/auth.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; +import { DataBuildService } from '../cache/builders/data-build.service'; +import { DSOUpdateComparator } from './dso-update-comparator'; +import { UpdateComparator } from './update-comparator'; const LINK_NAME = 'test'; @@ -30,6 +32,7 @@ class TestService extends ComColDataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, + protected dataBuildService: DataBuildService, protected store: Store, protected EnvConfig: GlobalConfig, protected cds: CommunityDataService, @@ -38,6 +41,7 @@ class TestService extends ComColDataService { protected authService: AuthService, protected notificationsService: NotificationsService, protected http: HttpClient, + protected comparator: DSOUpdateComparator, protected linkPath: string ) { super(); @@ -60,6 +64,8 @@ describe('ComColDataService', () => { const EnvConfig = {} as GlobalConfig; const notificationsService = {} as NotificationsService; const http = {} as HttpClient; + const comparator = {} as any; + const dataBuildService = {} as DataBuildService; const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d'; const options = Object.assign(new FindAllOptions(), { @@ -78,7 +84,7 @@ describe('ComColDataService', () => { const authHeader = 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJlaWQiOiJhNjA4NmIzNC0zOTE4LTQ1YjctOGRkZC05MzI5YTcwMmEyNmEiLCJzZyI6W10sImV4cCI6MTUzNDk0MDcyNX0.RV5GAtiX6cpwBN77P_v16iG9ipeyiO7faNYSNMzq_sQ'; const mockHalService = { - getEndpoint: (linkPath) => Observable.of(communitiesEndpoint) + getEndpoint: (linkPath) => observableOf(communitiesEndpoint) }; function initMockCommunityDataService(): CommunityDataService { @@ -112,6 +118,7 @@ describe('ComColDataService', () => { return new TestService( requestService, rdbService, + dataBuildService, store, EnvConfig, cds, @@ -120,6 +127,7 @@ describe('ComColDataService', () => { authService, notificationsService, http, + comparator, LINK_NAME ); } diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index d3eed88ffd..0616e9c2ed 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -20,8 +20,9 @@ import { NormalizedObject } from '../cache/models/normalized-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RequestEntry } from './request.reducer'; import { getResponseFromEntry } from '../shared/operators'; +import { CacheableObject } from '../cache/object-cache.reducer'; -export abstract class ComColDataService extends DataService { +export abstract class ComColDataService extends DataService { protected abstract cds: CommunityDataService; protected abstract objectCache: ObjectCacheService; protected abstract halService: HALEndpointService; diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index 9645a970c6..40d433a245 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -18,6 +18,8 @@ import { Observable } from 'rxjs'; import { PaginatedList } from './paginated-list'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; +import { DataBuildService } from '../cache/builders/data-build.service'; +import { DSOUpdateComparator } from './dso-update-comparator'; @Injectable() export class CommunityDataService extends ComColDataService { @@ -28,12 +30,14 @@ export class CommunityDataService extends ComColDataService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, protected authService: AuthService, protected notificationsService: NotificationsService, - protected http: HttpClient + protected http: HttpClient, + protected comparator: DSOUpdateComparator ) { super(); } diff --git a/src/app/core/data/config-response-parsing.service.spec.ts b/src/app/core/data/config-response-parsing.service.spec.ts index a33c5cf5b5..caf8ef4a19 100644 --- a/src/app/core/data/config-response-parsing.service.spec.ts +++ b/src/app/core/data/config-response-parsing.service.spec.ts @@ -177,7 +177,7 @@ describe('ConfigResponseParsingService', () => { 'https://rest.api/config/submissionsections/traditionalpagetwo', 'https://rest.api/config/submissionsections/upload', 'https://rest.api/config/submissionsections/license' - ]) + ], 'https://rest.api/config/submissiondefinitions/traditional/sections') }); it('should return a ConfigSuccessResponse if data contains a valid config endpoint response', () => { diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 4bbe775f3b..85d6579176 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -1,4 +1,13 @@ -import { delay, distinctUntilChanged, filter, find, switchMap, map, take, tap } from 'rxjs/operators'; +import { + delay, + distinctUntilChanged, + filter, + find, + switchMap, + map, + take, + tap, first, mergeMap +} from 'rxjs/operators'; import { Observable } from 'rxjs'; import { Store } from '@ngrx/store'; import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; @@ -32,10 +41,14 @@ import { DSOSuccessResponse, ErrorResponse, RestResponse } from '../cache/respon import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; +import { CacheableObject } from '../cache/object-cache.reducer'; +import { DataBuildService } from '../cache/builders/data-build.service'; +import { UpdateComparator } from './update-comparator'; -export abstract class DataService { +export abstract class DataService { protected abstract requestService: RequestService; protected abstract rdbService: RemoteDataBuildService; + protected abstract dataBuildService: DataBuildService; protected abstract store: Store; protected abstract linkPath: string; protected abstract halService: HALEndpointService; @@ -43,6 +56,7 @@ export abstract class DataService protected abstract authService: AuthService; protected abstract notificationsService: NotificationsService; protected abstract http: HttpClient; + protected abstract comparator: UpdateComparator; public abstract getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable @@ -122,15 +136,21 @@ export abstract class DataService * The patch is derived from the differences between the given object and its version in the object cache * @param {DSpaceObject} object The given object */ - update(object: DSpaceObject) { - const oldVersion = this.objectCache.getBySelfLink(object.self); - const operations = compare(oldVersion, object); - if (isNotEmpty(operations)) { - this.objectCache.addPatch(object.self, operations); - } + update(object: TDomain): Observable> { + const oldVersion$ = this.objectCache.getBySelfLink(object.self); + return oldVersion$.pipe(first(), mergeMap((oldVersion: TNormalized) => { + const newVersion = this.dataBuildService.normalize(object); + const operations = this.comparator.compare(oldVersion, newVersion); + if (isNotEmpty(operations)) { + this.objectCache.addPatch(object.self, operations); + } + return this.findById(object.uuid); + } + )); + } - create(dso: TNormalized, parentUUID: string): Observable> { + create(dso: TDomain, parentUUID: string): Observable> { const requestId = this.requestService.generateRequestId(); const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe( isNotEmptyOperator(), @@ -138,7 +158,8 @@ export abstract class DataService map((endpoint: string) => parentUUID ? `${endpoint}?parent=${parentUUID}` : endpoint) ); - const serializedDso = new DSpaceRESTv2Serializer(NormalizedObjectFactory.getConstructor(dso.type)).serialize(dso); + const normalizedObject: TNormalized = this.dataBuildService.normalize(dso); + const serializedDso = new DSpaceRESTv2Serializer(NormalizedObjectFactory.getConstructor(dso.type)).serialize(normalizedObject); const request$ = endpoint$.pipe( take(1), @@ -150,6 +171,7 @@ export abstract class DataService configureRequest(this.requestService) ).subscribe(); + // Resolve self link for new object const selfLink$ = this.requestService.getByUUID(requestId).pipe( getResponseFromEntry(), map((response: RestResponse) => { diff --git a/src/app/core/data/dso-update-comparator.ts b/src/app/core/data/dso-update-comparator.ts new file mode 100644 index 0000000000..245fbfaef0 --- /dev/null +++ b/src/app/core/data/dso-update-comparator.ts @@ -0,0 +1,12 @@ +import { Operation } from 'fast-json-patch/lib/core'; +import { compare } from 'fast-json-patch'; +import { UpdateComparator } from './update-comparator'; +import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model'; +import { Injectable } from '@angular/core'; + +@Injectable() +export class DSOUpdateComparator implements UpdateComparator { + compare(object1: NormalizedDSpaceObject, object2: NormalizedDSpaceObject): Operation[] { + return compare(object1.metadata, object2.metadata).map((operation: Operation) => Object.assign({}, operation, { path: '/metadata' + operation.path })); + } +} diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index 3e8d8bdc04..f1d0f21762 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -14,6 +14,8 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { AuthService } from '../auth/auth.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; +import { DataBuildService } from '../cache/builders/data-build.service'; +import { DSOUpdateComparator } from './dso-update-comparator'; /* tslint:disable:max-classes-per-file */ class DataServiceImpl extends DataService { @@ -22,12 +24,14 @@ class DataServiceImpl extends DataService constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, + protected dataBuildService: DataBuildService, protected store: Store, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, protected authService: AuthService, protected notificationsService: NotificationsService, - protected http: HttpClient) { + protected http: HttpClient, + protected comparator: DSOUpdateComparator) { super(); } @@ -48,12 +52,14 @@ export class DSpaceObjectDataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, + protected dataBuildService: DataBuildService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, protected authService: AuthService, protected notificationsService: NotificationsService, - protected http: HttpClient) { - this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, authService, notificationsService, http); + protected http: HttpClient, + protected comparator: DSOUpdateComparator) { + this.dataService = new DataServiceImpl(requestService, rdbService, dataBuildService, null, objectCache, halService, authService, notificationsService, http, comparator); } findById(uuid: string): Observable> { diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 68380ddaa2..411daa9b35 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -19,6 +19,8 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { AuthService } from '../auth/auth.service'; import { HttpClient } from '@angular/common/http'; +import { DataBuildService } from '../cache/builders/data-build.service'; +import { DSOUpdateComparator } from './dso-update-comparator'; @Injectable() export class ItemDataService extends DataService { @@ -27,13 +29,15 @@ export class ItemDataService extends DataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, + protected dataBuildService: DataBuildService, protected store: Store, private bs: BrowseService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, protected authService: AuthService, protected notificationsService: NotificationsService, - protected http: HttpClient) { + protected http: HttpClient, + protected comparator: DSOUpdateComparator) { super(); } diff --git a/src/app/core/data/paginated-list.ts b/src/app/core/data/paginated-list.ts index 07d53739d0..8efdccd75d 100644 --- a/src/app/core/data/paginated-list.ts +++ b/src/app/core/data/paginated-list.ts @@ -81,4 +81,12 @@ export class PaginatedList { set last(last: string) { this.pageInfo.last = last; } + + get self(): string { + return this.pageInfo.self; + } + + set self(self: string) { + this.pageInfo.self = self; + } } diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 285ed06545..20bc85a87f 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -80,7 +80,7 @@ export class RequestService { this.store.pipe(select(this.entryFromUUIDSelector(uuid))), this.store.pipe( select(this.originalUUIDFromUUIDSelector(uuid)), - switchMap((originalUUID) => { + mergeMap((originalUUID) => { return this.store.pipe(select(this.entryFromUUIDSelector(originalUUID))) }, )) diff --git a/src/app/core/data/update-comparator.ts b/src/app/core/data/update-comparator.ts new file mode 100644 index 0000000000..884ac585f5 --- /dev/null +++ b/src/app/core/data/update-comparator.ts @@ -0,0 +1,6 @@ +import { NormalizedObject } from '../cache/models/normalized-object.model'; +import { Operation } from 'fast-json-patch/lib/core'; + +export interface UpdateComparator { + compare(object1: TNormalized, object2: TNormalized): Operation[]; +} \ No newline at end of file diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 68338143ba..3e08da151c 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -34,14 +34,15 @@ export class DSpaceObject implements CacheableObject, ListableObject { /** * The name for this DSpaceObject */ - @autoserialize - name: string; + get name(): string { + return this.findMetadata('dc.title'); + } /** * An array containing all metadata of this DSpaceObject */ @autoserialize - metadata: Metadatum[]; + metadata: Metadatum[] = []; /** * An array of DSpaceObjects that are direct parents of this DSpaceObject diff --git a/src/app/core/shared/page-info.model.ts b/src/app/core/shared/page-info.model.ts index ba2af24dce..4ed281657d 100644 --- a/src/app/core/shared/page-info.model.ts +++ b/src/app/core/shared/page-info.model.ts @@ -39,4 +39,7 @@ export class PageInfo { @autoserialize first: string; + + @autoserialize + self: string; } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts index 3544bce280..8a3cf52abb 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts @@ -96,7 +96,6 @@ export class DsDynamicFormControlComponent extends DynamicFormControlContainerCo } static getFormControlType(model: DynamicFormControlModel): Type | null { - switch (model.type) { case DYNAMIC_FORM_CONTROL_TYPE_ARRAY: diff --git a/src/app/shared/form/form.component.html b/src/app/shared/form/form.component.html index 958c9a6c73..18d6533df4 100644 --- a/src/app/shared/form/form.component.html +++ b/src/app/shared/form/form.component.html @@ -51,7 +51,7 @@
-
diff --git a/src/app/shared/form/form.component.ts b/src/app/shared/form/form.component.ts index 2d74ddf8d4..9848f9feed 100644 --- a/src/app/shared/form/form.component.ts +++ b/src/app/shared/form/form.component.ts @@ -73,7 +73,7 @@ export class FormComponent implements OnDestroy, OnInit { * An event fired when form is valid and submitted . * Event's payload equals to the form content. */ - @Output() submit: EventEmitter> = new EventEmitter>(); + @Output() submitForm: EventEmitter> = new EventEmitter>(); /** * An object of FormGroup type @@ -264,7 +264,7 @@ export class FormComponent implements OnDestroy, OnInit { */ onSubmit(): void { if (this.getFormGroupValidStatus()) { - this.submit.emit(this.formService.getFormData(this.formId)); + this.submitForm.emit(this.formService.getFormData(this.formId)); } else { this.formService.validateAllFormFields(this.formGroup); } diff --git a/src/app/shared/form/form.service.ts b/src/app/shared/form/form.service.ts index ae5ba9f278..9356f86e8c 100644 --- a/src/app/shared/form/form.service.ts +++ b/src/app/shared/form/form.service.ts @@ -98,7 +98,7 @@ export class FormService { const errorKey = this.getValidatorNameFromMap(message); let errorMsg = message; - // if form control model has not errorMessages object, create it + // if form control model has no errorMessages object, create it if (!model.errorMessages) { model.errorMessages = {}; } From d37a2e051d35ae9ea2195b335fc8b51534c28f25 Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 21 Dec 2018 16:22:59 +0100 Subject: [PATCH 57/69] Started fixing tests --- resources/i18n/en.json | 24 ++-- .../collection-form.component.spec.ts | 8 +- .../create-collection-page.component.spec.ts | 44 ++++--- .../create-collection-page.component.ts | 1 - .../community-form.component.html | 5 +- .../community-form.component.spec.ts | 111 ++++++++++++------ .../community-form.component.ts | 40 +++++-- .../create-community-page.component.html | 6 +- .../create-community-page.component.spec.ts | 92 +++++++++------ .../create-community-page.component.ts | 8 +- .../edit-community-page.component.html | 5 +- .../edit-community-page.component.spec.ts | 111 +++++++++++------- .../edit-community-page.component.ts | 13 +- ...-page-sub-community-list.component.spec.ts | 2 - .../search-results.component.spec.ts | 2 - src/app/core/config/config.service.spec.ts | 1 - src/app/core/data/collection-data.service.ts | 2 - src/app/core/data/comcol-data.service.spec.ts | 14 --- src/app/core/data/community-data.service.ts | 2 - .../config-response-parsing.service.spec.ts | 2 +- src/app/core/data/data.service.spec.ts | 30 +++-- src/app/core/data/data.service.ts | 4 +- .../data/dspace-object-data.service.spec.ts | 13 +- .../core/data/dspace-object-data.service.ts | 5 +- src/app/core/data/item-data.service.spec.ts | 13 +- src/app/core/data/item-data.service.ts | 2 - .../integration/integration.service.spec.ts | 2 +- src/app/shared/form/form.component.spec.ts | 4 +- src/app/shared/mocks/mock-item.ts | 3 - .../search-form/search-form.component.spec.ts | 2 - src/app/shared/testing/eperson-mock.ts | 6 +- 31 files changed, 351 insertions(+), 226 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 70a1fc20b0..46d7617f6c 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -44,19 +44,21 @@ "sub-community-list": { "head": "Communities of this Community" }, - "edit": { - "head": "Edit collction", - "name": "Name", - "description": "Short Description", - "introductory": "Introductory text (HTML)", - "copyright": "Copyright text (HTML)", - "news": "News (HTML)", - "submit": "Submit", - "cancel": "Cancel", - "required": { - "name": "Please enter a community name" + "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 collection" + }, "create": { "head": "Create a Community", "sub-head": "Create a Sub-Community for Community {{ parent }}" diff --git a/src/app/+collection-page/collection-form/collection-form.component.spec.ts b/src/app/+collection-page/collection-form/collection-form.component.spec.ts index d2aafa2050..71ae92572d 100644 --- a/src/app/+collection-page/collection-form/collection-form.component.spec.ts +++ b/src/app/+collection-page/collection-form/collection-form.component.spec.ts @@ -4,9 +4,10 @@ import { TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; +import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; import { CollectionFormComponent } from './collection-form.component'; import { Location } from '@angular/common'; +import { DynamicFormService } from '@ng-dynamic-forms/core'; describe('CommunityFormComponent', () => { let comp: CollectionFormComponent; @@ -24,8 +25,9 @@ describe('CommunityFormComponent', () => { imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], declarations: [CollectionFormComponent], providers: [ - { provide: Location, useValue: locationStub } - ] + { provide: Location, useValue: locationStub }, + ], + schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); })); diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts index 6196945d80..837741d593 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts @@ -1,15 +1,11 @@ import { SharedModule } from '../../shared/shared.module'; import { Community } from '../../core/shared/community.model'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { DSOSuccessResponse, ErrorResponse } from '../../core/cache/response-cache.models'; -import { CommonModule } from '@angular/common'; -import { CreateCommunityPageComponent } from '../../+community-page/create-community-page/create-community-page.component'; +import { CommonModule, Location } from '@angular/common'; import { Router } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; -import { CommunityFormComponent } from '../../+community-page/community-form/community-form.component'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { CommunityDataService } from '../../core/data/community-data.service'; -import { RequestError } from '../../core/data/request.models'; import { RouteService } from '../../shared/services/route.service'; import { RemoteData } from '../../core/data/remote-data'; import { CreateCollectionPageComponent } from './create-collection-page.component'; @@ -17,6 +13,8 @@ import { CollectionDataService } from '../../core/data/collection-data.service'; import { Collection } from '../../core/shared/collection.model'; import { RouterTestingModule } from '@angular/router/testing'; import { CollectionFormComponent } from '../collection-form/collection-form.component'; +import { of as observableOf } from 'rxjs'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; describe('CreateCollectionPageComponent', () => { let comp: CreateCollectionPageComponent; @@ -28,25 +26,33 @@ describe('CreateCollectionPageComponent', () => { const community = Object.assign(new Community(), { uuid: 'a20da287-e174-466a-9926-f66b9300d347', - name: 'test community' + metadata: [{ + key: 'dc.title', + value: 'test collection' + }] }); const collection = Object.assign(new Collection(), { uuid: 'ce41d451-97ed-4a9c-94a1-7de34f16a9f4', - name: 'new collection' - }); + metadata: [{ + key: 'dc.title', + value: 'new collection' + }] }); const collectionDataServiceStub = { - create: (col, uuid?) => Observable.of(new RemoteData(false, false, true, undefined, collection)) + create: (col, uuid?) => observableOf(new RemoteData(false, false, true, undefined, collection)) }; const communityDataServiceStub = { - findById: (uuid) => Observable.of(new RemoteData(false, false, true, null, Object.assign(new Community(), { + findById: (uuid) => observableOf(new RemoteData(false, false, true, null, Object.assign(new Community(), { uuid: uuid, - name: community.name + metadata: [{ + key: 'dc.title', + value: community.name + }] }))) }; const routeServiceStub = { - getQueryParameterValue: (param) => Observable.of(community.uuid) + getQueryParameterValue: (param) => observableOf(community.uuid) }; const routerStub = { navigate: (commands) => commands @@ -55,13 +61,14 @@ describe('CreateCollectionPageComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [CreateCollectionPageComponent, CollectionFormComponent], + declarations: [CreateCollectionPageComponent], providers: [ { provide: CollectionDataService, useValue: collectionDataServiceStub }, { provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: RouteService, useValue: routeServiceStub }, { provide: Router, useValue: routerStub } - ] + ], + schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); })); @@ -77,7 +84,10 @@ describe('CreateCollectionPageComponent', () => { describe('onSubmit', () => { const data = { - name: 'test' + metadata: [{ + key: 'dc.title', + value:'test' + }] }; it('should navigate when successful', () => { @@ -89,7 +99,7 @@ describe('CreateCollectionPageComponent', () => { it('should not navigate on failure', () => { spyOn(router, 'navigate'); - spyOn(collectionDataService, 'create').and.returnValue(Observable.of(new RemoteData(true, true, false, undefined, collection))); + spyOn(collectionDataService, 'create').and.returnValue(observableOf(new RemoteData(true, true, false, undefined, collection))); comp.onSubmit(data); fixture.detectChanges(); expect(router.navigate).not.toHaveBeenCalled(); diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index d934e1d7a0..1bb4c31596 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -9,7 +9,6 @@ import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { RemoteData } from '../../core/data/remote-data'; import { isNotEmpty } from '../../shared/empty.util'; -import { NormalizedCollection } from '../../core/cache/models/normalized-collection.model'; import { ResourceType } from '../../core/shared/resource-type'; @Component({ diff --git a/src/app/+community-page/community-form/community-form.component.html b/src/app/+community-page/community-form/community-form.component.html index 637506f86d..c47f077022 100644 --- a/src/app/+community-page/community-form/community-form.component.html +++ b/src/app/+community-page/community-form/community-form.component.html @@ -1,4 +1,3 @@ - - + [formModel]="formModel" (submitForm)="onSubmit()"> \ No newline at end of file diff --git a/src/app/+community-page/community-form/community-form.component.spec.ts b/src/app/+community-page/community-form/community-form.component.spec.ts index ea17d15942..b90a2b0713 100644 --- a/src/app/+community-page/community-form/community-form.component.spec.ts +++ b/src/app/+community-page/community-form/community-form.component.spec.ts @@ -1,31 +1,65 @@ import { CommunityFormComponent } from './community-form.component'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { SharedModule } from '../../shared/shared.module'; import { TranslateModule } from '@ngx-translate/core'; -import { CommonModule } from '@angular/common'; -import { RouterTestingModule } from '@angular/router/testing'; -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; import { Location } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + DynamicFormService, + DynamicInputControlModel, + DynamicInputModel +} from '@ng-dynamic-forms/core'; +import { FormControl, FormGroup } from '@angular/forms'; +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'; -describe('CommunityFormComponent', () => { +fdescribe('CommunityFormComponent', () => { let comp: CommunityFormComponent; let fixture: ComponentFixture; let location: Location; + const formServiceStub: any = { + createFormGroup: (formModel: DynamicFormControlModel[]) => { + const controls = {}; + formModel.forEach((controlModel) => { + controls[controlModel.id] = new FormControl((controlModel as any).value); + }); + return new FormGroup(controls); + } + }; + const titleMD = { key: 'dc.title', value: 'Community Title' }; + const randomMD = { key: 'dc.random', value: 'Random metadata excluded from form' }; + const abstractMD = { key: 'dc.description.abstract', value: 'Community description' }; + const newTitleMD = { key: 'dc.title', value: 'New Community Title' }; + const formModel = [ + new DynamicInputModel({ + id: 'title', + name: newTitleMD.key, + value: newTitleMD.value + }), + new DynamicInputModel({ + id: 'abstract', + name: abstractMD.key, + value: abstractMD.value + }) + ]; /* tslint:disable:no-empty */ const locationStub = { - back: () => {} + back: () => { + } }; /* tslint:enable:no-empty */ beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + imports: [TranslateModule.forRoot(), RouterTestingModule], declarations: [CommunityFormComponent], providers: [ - { provide: Location, useValue: locationStub } - ] + { provide: Location, useValue: locationStub }, + { provide: DynamicFormService, useValue: formServiceStub } + ], + schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); })); @@ -34,38 +68,41 @@ describe('CommunityFormComponent', () => { comp = fixture.componentInstance; fixture.detectChanges(); location = (comp as any).location; + comp.formModel = formModel; }); - describe('when submitting', () => { - let input: DebugElement; - let submit: DebugElement; - let cancel: DebugElement; - let error: DebugElement; - + describe('onSubmit', () => { beforeEach(() => { - input = fixture.debugElement.query(By.css('input#community-name')); - submit = fixture.debugElement.query(By.css('button#community-submit')); - cancel = fixture.debugElement.query(By.css('button#community-cancel')); - error = fixture.debugElement.query(By.css('div.invalid-feedback')); + spyOn(comp.submitForm, 'emit'); }); - it('should display an error when leaving name empty', () => { - const el = input.nativeElement; + it('should update emit the new version of the community', () => { + comp.community = Object.assign( + new Community(), + { + metadata: [ + titleMD, + randomMD + ] + } + ); - el.value = ''; - el.dispatchEvent(new Event('input')); - submit.nativeElement.click(); - fixture.detectChanges(); + comp.onSubmit(); - expect(error.nativeElement.style.display).not.toEqual('none'); - }); - - it('should navigate back when pressing cancel', () => { - spyOn(location, 'back'); - cancel.nativeElement.click(); - fixture.detectChanges(); - - expect(location.back).toHaveBeenCalled(); - }); - }) + expect(comp.submitForm.emit).toHaveBeenCalledWith( + Object.assign( + {}, + new Community(), + { + metadata: [ + randomMD, + newTitleMD, + abstractMD + ], + type: ResourceType.Community + }, + ) + ); + }) + }); }); diff --git a/src/app/+community-page/community-form/community-form.component.ts b/src/app/+community-page/community-form/community-form.component.ts index fd0b490146..c470cbbf91 100644 --- a/src/app/+community-page/community-form/community-form.component.ts +++ b/src/app/+community-page/community-form/community-form.component.ts @@ -9,7 +9,11 @@ import { FormGroup } from '@angular/forms'; 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 { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { isNotEmpty } from '../../shared/empty.util'; +import { TranslateService } from '@ngx-translate/core'; + +const LABEL_KEY_PREFIX = 'community.form.'; +const ERROR_KEY_PREFIX = 'community.form.errors.'; @Component({ selector: 'ds-community-form', @@ -17,40 +21,34 @@ import { hasValue, isNotEmpty } from '../../shared/empty.util'; templateUrl: './community-form.component.html' }) export class CommunityFormComponent implements OnInit { - @Input() community: Community = new Community(); formModel: DynamicFormControlModel[] = [ new DynamicInputModel({ id: 'title', name: 'dc.title', - label: 'Name', required: true, validators: { required: null }, errorMessages: { required: 'Please enter a name for this title' - } + }, }), new DynamicTextAreaModel({ id: 'description', name: 'dc.description', - label: 'Introductory text (HTML)', }), new DynamicTextAreaModel({ id: 'abstract', name: 'dc.description.abstract', - label: 'Short Description', }), new DynamicTextAreaModel({ id: 'rights', name: 'dc.rights', - label: 'Copyright text (HTML)', }), new DynamicTextAreaModel({ id: 'tableofcontents', name: 'dc.description.tableofcontents', - label: 'News (HTML)', }), ]; @@ -58,7 +56,9 @@ export class CommunityFormComponent implements OnInit { @Output() submitForm: EventEmitter = new EventEmitter(); - public constructor(private location: Location, private formService: DynamicFormService) { + public constructor(private location: Location, + private formService: DynamicFormService, + private translate: TranslateService) { } ngOnInit(): void { @@ -68,10 +68,14 @@ export class CommunityFormComponent implements OnInit { } ); this.formGroup = this.formService.createFormGroup(this.formModel); + this.updateFieldTranslations(); + this.translate.onLangChange + .subscribe(() => { + this.updateFieldTranslations(); + }); } - onSubmit(event: Event) { - event.stopPropagation(); + onSubmit() { const metadata = this.formModel.map( (fieldModel: DynamicInputModel) => { return { key: fieldModel.name, value: fieldModel.value } @@ -87,7 +91,17 @@ export class CommunityFormComponent implements OnInit { this.submitForm.emit(updatedCommunity); } - cancel() { - this.location.back(); + private updateFieldTranslations() { + this.formModel.forEach( + (fieldModel: DynamicInputModel) => { + fieldModel.label = this.translate.instant(LABEL_KEY_PREFIX + fieldModel.id); + if (isNotEmpty(fieldModel.validators)) { + fieldModel.errorMessages = {}; + Object.keys(fieldModel.validators).forEach((key) => { + fieldModel.errorMessages[key] = this.translate.instant(ERROR_KEY_PREFIX + fieldModel.id + '.' + key); + }); + } + } + ); } } diff --git a/src/app/+community-page/create-community-page/create-community-page.component.html b/src/app/+community-page/create-community-page/create-community-page.component.html index bb0f5b83af..aeb4713b52 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.html +++ b/src/app/+community-page/create-community-page/create-community-page.component.html @@ -1,9 +1,9 @@
- - -

{{ 'community.create.sub-head' | translate:{ parent: community.name } }}

+ + +

{{ 'community.create.sub-head' | translate:{ parent: parent.name } }}

diff --git a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts index e76b10bde6..83db9561cc 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts @@ -4,55 +4,76 @@ import { CommunityDataService } from '../../core/data/community-data.service'; import { RouteService } from '../../shared/services/route.service'; import { Router } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; -import { Observable } from 'rxjs/Observable'; +import { of as observableOf } from 'rxjs'; import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; -import { DSOSuccessResponse, ErrorResponse } from '../../core/cache/response-cache.models'; import { SharedModule } from '../../shared/shared.module'; import { CommonModule } from '@angular/common'; -import { CommunityFormComponent } from '../community-form/community-form.component'; import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; -describe('CreateCommunityPageComponent', () => { +fdescribe('CreateCommunityPageComponent', () => { let comp: CreateCommunityPageComponent; let fixture: ComponentFixture; let communityDataService: CommunityDataService; let routeService: RouteService; let router: Router; - const community = Object.assign(new Community(), { - uuid: 'a20da287-e174-466a-9926-f66b9300d347', - name: 'test community' - }); + let community; + let newCommunity; + let communityDataServiceStub; + let routeServiceStub; + let routerStub; - const newCommunity = Object.assign(new Community(), { - uuid: '1ff59938-a69a-4e62-b9a4-718569c55d48', - name: 'new community' - }); + function initializeVars() { + community = Object.assign(new Community(), { + uuid: 'a20da287-e174-466a-9926-f66b9300d347', + metadata: [{ + key: 'dc.title', + value: 'test community' + }] + }); - const communityDataServiceStub = { - findById: (uuid) => Observable.of(new RemoteData(false, false, true, null, Object.assign(new Community(), { - uuid: uuid, - name: community.name - }))), - create: (com, uuid?) => Observable.of(new RemoteData(false, false, true, undefined, newCommunity)) - }; - const routeServiceStub = { - getQueryParameterValue: (param) => Observable.of(community.uuid) - }; - const routerStub = { - navigate: (commands) => commands - }; + newCommunity = Object.assign(new Community(), { + uuid: '1ff59938-a69a-4e62-b9a4-718569c55d48', + metadata: [{ + key: 'dc.title', + value: 'new community' + }] + }); + + communityDataServiceStub = { + findById: (uuid) => observableOf(new RemoteData(false, false, true, null, Object.assign(new Community(), { + uuid: uuid, + metadata: [{ + key: 'dc.title', + value: community.name + }] + }))), + create: (com, uuid?) => observableOf(new RemoteData(false, false, true, undefined, newCommunity)) + + }; + + routeServiceStub = { + getQueryParameterValue: (param) => observableOf(community.uuid) + }; + routerStub = { + navigate: (commands) => commands + }; + + } beforeEach(async(() => { + initializeVars(); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [CreateCommunityPageComponent, CommunityFormComponent], + declarations: [CreateCommunityPageComponent], providers: [ { provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: RouteService, useValue: routeServiceStub }, - { provide: Router, useValue: routerStub } - ] + { provide: Router, useValue: routerStub }, + ], + schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); })); @@ -66,10 +87,15 @@ describe('CreateCommunityPageComponent', () => { }); describe('onSubmit', () => { - const data = { - name: 'test' - }; - + let data; + beforeEach(() => { + data = Object.assign(new Community(), { + metadata: [{ + key: 'dc.title', + value: 'test' + }] + }); + }); it('should navigate when successful', () => { spyOn(router, 'navigate'); comp.onSubmit(data); @@ -79,7 +105,7 @@ describe('CreateCommunityPageComponent', () => { it('should not navigate on failure', () => { spyOn(router, 'navigate'); - spyOn(communityDataService, 'create').and.returnValue(Observable.of(new RemoteData(true, true, false, undefined, newCommunity))); + spyOn(communityDataService, 'create').and.returnValue(observableOf(new RemoteData(true, true, false, undefined, newCommunity))); comp.onSubmit(data); fixture.detectChanges(); expect(router.navigate).not.toHaveBeenCalled(); diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index 5373abfe22..8871d33547 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -5,7 +5,7 @@ import { Observable } from 'rxjs'; import { RouteService } from '../../shared/services/route.service'; import { Router } from '@angular/router'; import { RemoteData } from '../../core/data/remote-data'; -import { isNotEmpty } from '../../shared/empty.util'; +import { isNotEmpty, isNotUndefined } from '../../shared/empty.util'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { map, take } from 'rxjs/operators'; import { getSucceededRemoteData } from '../../core/shared/operators'; @@ -42,8 +42,10 @@ export class CreateCommunityPageComponent implements OnInit { this.communityDataService.create(community, uuid) .pipe(getSucceededRemoteData()) .subscribe((communityRD: RemoteData) => { - const newUUID = communityRD.payload.uuid; - this.router.navigate(['/communities/' + newUUID]); + if (isNotUndefined(communityRD)) { + const newUUID = communityRD.payload.uuid; + this.router.navigate(['/communities/' + newUUID]); + } }); }); } diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.html b/src/app/+community-page/edit-community-page/edit-community-page.component.html index eb9f797b3d..1dc6442307 100644 --- a/src/app/+community-page/edit-community-page/edit-community-page.component.html +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.html @@ -1,10 +1,7 @@
- - -

{{ 'community.edit.sub-head' | translate:{ parent: parent.name } }}

-
+
diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts b/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts index 6c5eb5f591..84edd47e1d 100644 --- a/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts @@ -1,65 +1,91 @@ -import { CreateCommunityPageComponent } from './create-community-page.component'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { CommunityDataService } from '../../core/data/community-data.service'; import { RouteService } from '../../shared/services/route.service'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; -import { Observable } from 'rxjs/Observable'; +import { of as observableOf } from 'rxjs'; import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; -import { DSOSuccessResponse, ErrorResponse } from '../../core/cache/response-cache.models'; -import { BrowserModule } from '@angular/platform-browser'; import { SharedModule } from '../../shared/shared.module'; import { CommonModule } from '@angular/common'; -import { CommunityFormComponent } from '../community-form/community-form.component'; import { RouterTestingModule } from '@angular/router/testing'; -import { RequestError } from '../../core/data/request.models'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { EditCommunityPageComponent } from './edit-community-page.component'; +import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; -describe('CreateCommunityPageComponent', () => { - let comp: CreateCommunityPageComponent; - let fixture: ComponentFixture; +fdescribe('EditCommunityPageComponent', () => { + let comp: EditCommunityPageComponent; + let fixture: ComponentFixture; let communityDataService: CommunityDataService; let routeService: RouteService; let router: Router; - const community = Object.assign(new Community(), { - uuid: 'a20da287-e174-466a-9926-f66b9300d347', - name: 'test community' - }); + let community; + let newCommunity; + let communityDataServiceStub; + let routeServiceStub; + let routerStub; + let routeStub; - const newCommunity = Object.assign(new Community(), { - uuid: '1ff59938-a69a-4e62-b9a4-718569c55d48', - name: 'new community' - }); + function initializeVars() { + community = Object.assign(new Community(), { + uuid: 'a20da287-e174-466a-9926-f66b9300d347', + metadata: [{ + key: 'dc.title', + value: 'test community' + }] + }); - const communityDataServiceStub = { - findById: (uuid) => Observable.of(new RemoteData(false, false, true, null, Object.assign(new Community(), { - uuid: uuid, - name: community.name - }))), - create: (com, uuid?) => Observable.of(new RemoteData(false, false, true, undefined, newCommunity)) - }; - const routeServiceStub = { - getQueryParameterValue: (param) => Observable.of(community.uuid) - }; - const routerStub = { - navigate: (commands) => commands - }; + newCommunity = Object.assign(new Community(), { + uuid: '1ff59938-a69a-4e62-b9a4-718569c55d48', + metadata: [{ + key: 'dc.title', + value: 'new community' + }] + }); + + communityDataServiceStub = { + findById: (uuid) => observableOf(new RemoteData(false, false, true, null, Object.assign(new Community(), { + uuid: uuid, + metadata: [{ + key: 'dc.title', + value: community.name + }] + }))), + update: (com, uuid?) => observableOf(new RemoteData(false, false, true, undefined, newCommunity)) + + }; + + routeServiceStub = { + getQueryParameterValue: (param) => observableOf(community.uuid) + }; + routerStub = { + navigate: (commands) => commands + }; + + routeStub = { + data: observableOf(community) + }; + + } beforeEach(async(() => { + initializeVars(); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [CreateCommunityPageComponent, CommunityFormComponent], + declarations: [EditCommunityPageComponent], providers: [ { provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: RouteService, useValue: routeServiceStub }, - { provide: Router, useValue: routerStub } - ] + { provide: Router, useValue: routerStub }, + { provide: ActivatedRoute, useValue: routeStub }, + ], + schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(CreateCommunityPageComponent); + fixture = TestBed.createComponent(EditCommunityPageComponent); comp = fixture.componentInstance; fixture.detectChanges(); communityDataService = (comp as any).communityDataService; @@ -68,10 +94,15 @@ describe('CreateCommunityPageComponent', () => { }); describe('onSubmit', () => { - const data = { - name: 'test' - }; - + let data; + beforeEach(() => { + data = Object.assign(new Community(), { + metadata: [{ + key: 'dc.title', + value: 'test' + }] + }); + }); it('should navigate when successful', () => { spyOn(router, 'navigate'); comp.onSubmit(data); @@ -81,7 +112,7 @@ describe('CreateCommunityPageComponent', () => { it('should not navigate on failure', () => { spyOn(router, 'navigate'); - spyOn(communityDataService, 'create').and.returnValue(Observable.of(new RemoteData(true, true, false, undefined, newCommunity))); + spyOn(communityDataService, 'update').and.returnValue(observableOf(new RemoteData(true, true, false, undefined, newCommunity))); comp.onSubmit(data); fixture.detectChanges(); expect(router.navigate).not.toHaveBeenCalled(); diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.ts b/src/app/+community-page/edit-community-page/edit-community-page.component.ts index 58805af80a..1528c9e8d5 100644 --- a/src/app/+community-page/edit-community-page/edit-community-page.component.ts +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.ts @@ -5,11 +5,8 @@ import { Observable } from 'rxjs'; import { RouteService } from '../../shared/services/route.service'; import { ActivatedRoute, Router } from '@angular/router'; import { RemoteData } from '../../core/data/remote-data'; -import { isNotEmpty } from '../../shared/empty.util'; -import { DSpaceObject } from '../../core/shared/dspace-object.model'; -import { first, map, take, tap } from 'rxjs/operators'; -import { ResourceType } from '../../core/shared/resource-type'; -import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model'; +import { isNotUndefined } from '../../shared/empty.util'; +import { first, map } from 'rxjs/operators'; import { getSucceededRemoteData } from '../../core/shared/operators'; @Component({ @@ -39,8 +36,10 @@ export class EditCommunityPageComponent { this.communityDataService.update(community) .pipe(getSucceededRemoteData()) .subscribe((communityRD: RemoteData) => { - const newUUID = communityRD.payload.uuid; - this.router.navigate(['/communities/' + newUUID]); + if (isNotUndefined(communityRD)) { + const newUUID = communityRD.payload.uuid; + this.router.navigate(['/communities/' + newUUID]); + } }); } } diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts index 0c985e37f9..ace748c7de 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts @@ -17,7 +17,6 @@ describe('SubCommunityList Component', () => { let fixture: ComponentFixture; const subcommunities = [Object.assign(new Community(), { - name: 'SubCommunity 1', id: '123456789-1', metadata: [ { @@ -27,7 +26,6 @@ describe('SubCommunityList Component', () => { }] }), Object.assign(new Community(), { - name: 'SubCommunity 2', id: '123456789-2', metadata: [ { diff --git a/src/app/+search-page/search-results/search-results.component.spec.ts b/src/app/+search-page/search-results/search-results.component.spec.ts index 54463d916d..b7ac11553a 100644 --- a/src/app/+search-page/search-results/search-results.component.spec.ts +++ b/src/app/+search-page/search-results/search-results.component.spec.ts @@ -111,7 +111,6 @@ export const objects = [ id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', type: ResourceType.Community, - name: 'OR2017 - Demonstration', metadata: [ { key: 'dc.description', @@ -161,7 +160,6 @@ export const objects = [ id: '9076bd16-e69a-48d6-9e41-0238cb40d863', uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863', type: ResourceType.Community, - name: 'Sample Community', metadata: [ { key: 'dc.description', diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts index 8e9f7db27a..44cfdee358 100644 --- a/src/app/core/config/config.service.spec.ts +++ b/src/app/core/config/config.service.spec.ts @@ -36,7 +36,6 @@ describe('ConfigService', () => { const scopedEndpoint = `${serviceEndpoint}/${scopeName}`; const searchEndpoint = `${serviceEndpoint}/${BROWSE}?uuid=${scopeID}`; - function initTestService(): TestService { return new TestService( requestService, diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index ef294c1296..b08b1005b7 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -9,7 +9,6 @@ import { ComColDataService } from './comcol-data.service'; import { CommunityDataService } from './community-data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { AuthService } from '../auth/auth.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; import { DataBuildService } from '../cache/builders/data-build.service'; @@ -27,7 +26,6 @@ export class CollectionDataService extends ComColDataService { protected cds: CommunityDataService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, - protected authService: AuthService, protected notificationsService: NotificationsService, protected http: HttpClient, protected comparator: DSOUpdateComparator, @@ -56,7 +53,6 @@ describe('ComColDataService', () => { let requestService: RequestService; let cds: CommunityDataService; let objectCache: ObjectCacheService; - let authService: AuthService; let halService: any = {}; const rdbService = {} as RemoteDataBuildService; @@ -106,14 +102,6 @@ describe('ComColDataService', () => { }); } - function initMockAuthService(): AuthService { - return jasmine.createSpyObj('authService', { - buildAuthHeader: cold('c-', { - c: authHeader - }) - }); - } - function initTestService(): TestService { return new TestService( requestService, @@ -124,7 +112,6 @@ describe('ComColDataService', () => { cds, objectCache, halService, - authService, notificationsService, http, comparator, @@ -137,7 +124,6 @@ describe('ComColDataService', () => { requestService = getMockRequestService(); objectCache = initMockObjectCacheService(); halService = mockHalService; - authService = initMockAuthService(); service = initTestService(); }); diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index 40d433a245..63fbe3a21a 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -10,7 +10,6 @@ import { Community } from '../shared/community.model'; import { ComColDataService } from './comcol-data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { AuthService } from '../auth/auth.service'; import { FindAllOptions, FindAllRequest } from './request.models'; import { RemoteData } from './remote-data'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; @@ -34,7 +33,6 @@ export class CommunityDataService extends ComColDataService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, - protected authService: AuthService, protected notificationsService: NotificationsService, protected http: HttpClient, protected comparator: DSOUpdateComparator diff --git a/src/app/core/data/config-response-parsing.service.spec.ts b/src/app/core/data/config-response-parsing.service.spec.ts index caf8ef4a19..a33c5cf5b5 100644 --- a/src/app/core/data/config-response-parsing.service.spec.ts +++ b/src/app/core/data/config-response-parsing.service.spec.ts @@ -177,7 +177,7 @@ describe('ConfigResponseParsingService', () => { 'https://rest.api/config/submissionsections/traditionalpagetwo', 'https://rest.api/config/submissionsections/upload', 'https://rest.api/config/submissionsections/license' - ], 'https://rest.api/config/submissiondefinitions/traditional/sections') + ]) }); it('should return a ConfigSuccessResponse if data contains a valid config endpoint response', () => { diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index 7da709abd5..ad6fb5f096 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -12,6 +12,11 @@ import { of as observableOf } from 'rxjs'; import { ObjectCacheService } from '../cache/object-cache.service'; import { Operation } from '../../../../node_modules/fast-json-patch'; import { DSpaceObject } from '../shared/dspace-object.model'; +import { AuthService } from '../auth/auth.service'; +import { UpdateComparator } from './update-comparator'; +import { HttpClient } from '@angular/common/http'; +import { DataBuildService } from '../cache/builders/data-build.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; const endpoint = 'https://rest.api/core'; @@ -23,10 +28,14 @@ class TestService extends DataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, + protected dataBuildService: DataBuildService, protected store: Store, protected linkPath: string, protected halService: HALEndpointService, - protected objectCache: ObjectCacheService + protected objectCache: ObjectCacheService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: UpdateComparator ) { super(); } @@ -42,6 +51,10 @@ describe('DataService', () => { const requestService = {} as RequestService; const halService = {} as HALEndpointService; const rdbService = {} as RemoteDataBuildService; + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = {} as any; + const dataBuildService = {} as DataBuildService; const objectCache = { addPatch: () => { /* empty */ @@ -56,13 +69,16 @@ describe('DataService', () => { return new TestService( requestService, rdbService, + dataBuildService, store, endpoint, halService, - objectCache + objectCache, + notificationsService, + http, + comparator, ); } - service = initTestService(); describe('getFindAllHref', () => { @@ -134,7 +150,7 @@ describe('DataService', () => { let selfLink; beforeEach(() => { - operations = [{ op: 'replace', path: '/name', value: 'random string' } as Operation]; + operations = [{ op: 'replace', path: '/metadata/dc.title', value: 'random string' } as Operation]; selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; spyOn(objectCache, 'addPatch'); }); @@ -153,16 +169,16 @@ describe('DataService', () => { const name1 = 'random string'; const name2 = 'another random string'; beforeEach(() => { - operations = [{ op: 'replace', path: '/name', value: name2 } as Operation]; + operations = [{ op: 'replace', path: '/metadata/dc.title', value: name2 } as Operation]; selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; dso = new DSpaceObject(); dso.self = selfLink; - dso.name = name1; + dso.metadata = [{ key: 'dc.title', value: name1 }]; dso2 = new DSpaceObject(); dso2.self = selfLink; - dso2.name = name2; + dso2.metadata = [{ key: 'dc.title', value: name2 }]; spyOn(objectCache, 'getBySelfLink').and.returnValue(dso); spyOn(objectCache, 'addPatch'); diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 85d6579176..47be86c296 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -29,7 +29,6 @@ import { NormalizedObject } from '../cache/models/normalized-object.model'; import { compare, Operation } from 'fast-json-patch'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { AuthService } from '../auth/auth.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; import { @@ -53,7 +52,6 @@ export abstract class DataService; @@ -155,7 +153,7 @@ export abstract class DataService parentUUID ? `${endpoint}?parent=${parentUUID}` : endpoint) + map((endpoint: string) => parentUUID ? `${endpoint}?parentCommunity=${parentUUID}` : endpoint) ); const normalizedObject: TNormalized = this.dataBuildService.normalize(dso); diff --git a/src/app/core/data/dspace-object-data.service.spec.ts b/src/app/core/data/dspace-object-data.service.spec.ts index cdddcb7ce6..2d478b8f73 100644 --- a/src/app/core/data/dspace-object-data.service.spec.ts +++ b/src/app/core/data/dspace-object-data.service.spec.ts @@ -7,6 +7,9 @@ import { FindByIDRequest } from './request.models'; import { RequestService } from './request.service'; import { DSpaceObjectDataService } from './dspace-object-data.service'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { DataBuildService } from '../cache/builders/data-build.service'; describe('DSpaceObjectDataService', () => { let scheduler: TestScheduler; @@ -40,12 +43,20 @@ describe('DSpaceObjectDataService', () => { }) }); objectCache = {} as ObjectCacheService; + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = {} as any; + const dataBuildService = {} as DataBuildService; service = new DSpaceObjectDataService( requestService, rdbService, + dataBuildService, + objectCache, halService, - objectCache + notificationsService, + http, + comparator ) }); diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index f1d0f21762..9a069c4d61 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -11,7 +11,6 @@ import { RemoteData } from './remote-data'; import { RequestService } from './request.service'; import { FindAllOptions } from './request.models'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { AuthService } from '../auth/auth.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; import { DataBuildService } from '../cache/builders/data-build.service'; @@ -28,7 +27,6 @@ class DataServiceImpl extends DataService protected store: Store, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, - protected authService: AuthService, protected notificationsService: NotificationsService, protected http: HttpClient, protected comparator: DSOUpdateComparator) { @@ -55,11 +53,10 @@ export class DSpaceObjectDataService { protected dataBuildService: DataBuildService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, - protected authService: AuthService, protected notificationsService: NotificationsService, protected http: HttpClient, protected comparator: DSOUpdateComparator) { - this.dataService = new DataServiceImpl(requestService, rdbService, dataBuildService, null, objectCache, halService, authService, notificationsService, http, comparator); + this.dataService = new DataServiceImpl(requestService, rdbService, dataBuildService, null, objectCache, halService, notificationsService, http, comparator); } findById(uuid: string): Observable> { diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index bb67fc8412..1be361cb9d 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -9,6 +9,9 @@ import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { FindAllOptions } from './request.models'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { DataBuildService } from '../cache/builders/data-build.service'; describe('ItemDataService', () => { let scheduler: TestScheduler; @@ -34,6 +37,10 @@ describe('ItemDataService', () => { const scopedEndpoint = `${itemBrowseEndpoint}?scope=${scopeID}`; const serviceEndpoint = `https://rest.api/core/items`; const browseError = new Error('getBrowseURL failed'); + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = {} as any; + const dataBuildService = {} as DataBuildService; function initMockBrowseService(isSuccessful: boolean) { const obs = isSuccessful ? @@ -48,10 +55,14 @@ describe('ItemDataService', () => { return new ItemDataService( requestService, rdbService, + dataBuildService, store, bs, + objectCache, halEndpointService, - objectCache + notificationsService, + http, + comparator ); } diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 411daa9b35..2fb2c017dc 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -17,7 +17,6 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { FindAllOptions } from './request.models'; import { ObjectCacheService } from '../cache/object-cache.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { AuthService } from '../auth/auth.service'; import { HttpClient } from '@angular/common/http'; import { DataBuildService } from '../cache/builders/data-build.service'; import { DSOUpdateComparator } from './dso-update-comparator'; @@ -34,7 +33,6 @@ export class ItemDataService extends DataService { private bs: BrowseService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, - protected authService: AuthService, protected notificationsService: NotificationsService, protected http: HttpClient, protected comparator: DSOUpdateComparator) { diff --git a/src/app/core/integration/integration.service.spec.ts b/src/app/core/integration/integration.service.spec.ts index 158f4b0680..152d7ab165 100644 --- a/src/app/core/integration/integration.service.spec.ts +++ b/src/app/core/integration/integration.service.spec.ts @@ -40,7 +40,7 @@ describe('IntegrationService', () => { findOptions = new IntegrationSearchOptions(uuid, name, metadata); - function initTestService(): TestService { + function initTestService(): TestService { return new TestService( requestService, halService diff --git a/src/app/shared/form/form.component.spec.ts b/src/app/shared/form/form.component.spec.ts index 06676d191e..38e95b5c49 100644 --- a/src/app/shared/form/form.component.spec.ts +++ b/src/app/shared/form/form.component.spec.ts @@ -350,13 +350,13 @@ describe('FormComponent test suite', () => { const control = formComp.formGroup.get(['dc_title']); control.setValue('Test Title'); formState.testForm.valid = true; - spyOn(formComp.submit, 'emit'); + spyOn(formComp.submitForm, 'emit'); form.next(formState.testForm); formFixture.detectChanges(); formComp.onSubmit(); - expect(formComp.submit.emit).toHaveBeenCalled(); + expect(formComp.submitForm.emit).toHaveBeenCalled(); }); it('should not emit submit Event on form submit whether the form is not valid', () => { diff --git a/src/app/shared/mocks/mock-item.ts b/src/app/shared/mocks/mock-item.ts index f3db69a0f2..2e5c764ee2 100644 --- a/src/app/shared/mocks/mock-item.ts +++ b/src/app/shared/mocks/mock-item.ts @@ -51,7 +51,6 @@ export const MockItem: Item = Object.assign(new Item(), { id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713', uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713', type: 'bitstream', - name: 'test_word.docx', metadata: [ { key: 'dc.title', @@ -86,7 +85,6 @@ export const MockItem: Item = Object.assign(new Item(), { id: '99b00f3c-1cc6-4689-8158-91965bee6b28', uuid: '99b00f3c-1cc6-4689-8158-91965bee6b28', type: 'bitstream', - name: 'test_pdf.pdf', metadata: [ { key: 'dc.title', @@ -102,7 +100,6 @@ export const MockItem: Item = Object.assign(new Item(), { id: '0ec7ff22-f211-40ab-a69e-c819b0b1f357', uuid: '0ec7ff22-f211-40ab-a69e-c819b0b1f357', type: 'item', - name: 'Test PowerPoint Document', metadata: [ { key: 'dc.creator', diff --git a/src/app/shared/search-form/search-form.component.spec.ts b/src/app/shared/search-form/search-form.component.spec.ts index 30f5801cc2..004d0c5b21 100644 --- a/src/app/shared/search-form/search-form.component.spec.ts +++ b/src/app/shared/search-form/search-form.component.spec.ts @@ -121,7 +121,6 @@ export const objects: DSpaceObject[] = [ id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', type: ResourceType.Community, - name: 'OR2017 - Demonstration', metadata: [ { key: 'dc.description', @@ -171,7 +170,6 @@ export const objects: DSpaceObject[] = [ id: '9076bd16-e69a-48d6-9e41-0238cb40d863', uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863', type: ResourceType.Community, - name: 'Sample Community', metadata: [ { key: 'dc.description', diff --git a/src/app/shared/testing/eperson-mock.ts b/src/app/shared/testing/eperson-mock.ts index f163a490b9..ef27f4983d 100644 --- a/src/app/shared/testing/eperson-mock.ts +++ b/src/app/shared/testing/eperson-mock.ts @@ -13,8 +13,12 @@ export const EPersonMock: EPerson = Object.assign(new EPerson(),{ id: 'testid', uuid: 'testid', type: 'eperson', - name: 'User Test', metadata: [ + { + key: 'dc.title', + language: null, + value: 'User Test' + }, { key: 'eperson.firstname', language: null, From a35dffbe95a81283e9166868f5f3eee5c4a6274c Mon Sep 17 00:00:00 2001 From: lotte Date: Sun, 23 Dec 2018 00:07:24 +0100 Subject: [PATCH 58/69] refactoring comcol forms --- resources/i18n/en.json | 25 ++-- .../collection-form.component.html | 35 ----- .../collection-form.component.scss | 7 - .../collection-form.component.ts | 87 +++++++------ .../create-collection-page.component.html | 8 +- .../create-collection-page.component.ts | 60 ++++----- .../edit-collection-page.component.html | 8 ++ .../edit-community-page.component.scss | 1 + .../edit-community-page.component.spec.ts | 121 ++++++++++++++++++ .../edit-community-page.component.ts | 45 +++++++ .../community-form.component.scss | 7 - .../community-form.component.ts | 73 +---------- .../create-community-page.component.ts | 47 +------ .../comcol-form/comcol-form.component.html} | 4 +- .../comcol-form/comcol-form.component.ts | 77 +++++++++++ .../create-comcol-page.component.ts | 55 ++++++++ 16 files changed, 420 insertions(+), 240 deletions(-) delete mode 100644 src/app/+collection-page/collection-form/collection-form.component.html delete mode 100644 src/app/+collection-page/collection-form/collection-form.component.scss create mode 100644 src/app/+collection-page/edit-collection-page/edit-collection-page.component.html create mode 100644 src/app/+collection-page/edit-collection-page/edit-community-page.component.scss create mode 100644 src/app/+collection-page/edit-collection-page/edit-community-page.component.spec.ts create mode 100644 src/app/+collection-page/edit-collection-page/edit-community-page.component.ts delete mode 100644 src/app/+community-page/community-form/community-form.component.scss rename src/app/{+community-page/community-form/community-form.component.html => comcol-forms/comcol-form/comcol-form.component.html} (66%) create mode 100644 src/app/comcol-forms/comcol-form/comcol-form.component.ts create mode 100644 src/app/comcol-forms/create-comcol-page/create-comcol-page.component.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 46d7617f6c..1f85b8ddac 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -14,20 +14,23 @@ } } }, - "edit": { - "name": "Name", - "description": "Short Description", - "introductory": "Introductory text (HTML)", - "copyright": "Copyright text (HTML)", - "news": "News (HTML)", + "form": { + "title": "Name", + "description": "Introductory text (HTML)", + "abstract": "Short Description", + "rights": "Copyright text (HTML)", + "tableofcontents": "News (HTML)", "license": "License", "provenance": "Provenance", - "submit": "Submit", - "cancel": "Cancel", - "required": { - "name": "Please enter a collection name" + "errors": { + "title": { + "required": "Please enter a collection name" + } } }, + "edit": { + "head": "Edit Collection" + }, "create": { "head": "Create a Collection", "sub-head": "Create a Collection for Community {{ parent }}" @@ -57,7 +60,7 @@ } }, "edit": { - "head": "Edit collection" + "head": "Edit Community" }, "create": { "head": "Create a Community", diff --git a/src/app/+collection-page/collection-form/collection-form.component.html b/src/app/+collection-page/collection-form/collection-form.component.html deleted file mode 100644 index 85135af15d..0000000000 --- a/src/app/+collection-page/collection-form/collection-form.component.html +++ /dev/null @@ -1,35 +0,0 @@ -
-
- - -
{{ 'collection.edit.required.name' | translate }}
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
diff --git a/src/app/+collection-page/collection-form/collection-form.component.scss b/src/app/+collection-page/collection-form/collection-form.component.scss deleted file mode 100644 index d5811186e7..0000000000 --- a/src/app/+collection-page/collection-form/collection-form.component.scss +++ /dev/null @@ -1,7 +0,0 @@ -@import '../../../styles/variables.scss'; - -// temporary fix for bootstrap 4 beta btn color issue -.btn-secondary { - background-color: $input-bg; - color: $input-color; -} diff --git a/src/app/+collection-page/collection-form/collection-form.component.ts b/src/app/+collection-page/collection-form/collection-form.component.ts index 4238eb3811..03bd9bfda9 100644 --- a/src/app/+collection-page/collection-form/collection-form.component.ts +++ b/src/app/+collection-page/collection-form/collection-form.component.ts @@ -1,41 +1,56 @@ -import { Component, EventEmitter, Output } from '@angular/core'; -import { isNotEmpty } from '../../shared/empty.util'; -import { Location } from '@angular/common'; +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 '../../comcol-forms/comcol-form/comcol-form.component'; @Component({ selector: 'ds-collection-form', - styleUrls: ['./collection-form.component.scss'], - templateUrl: './collection-form.component.html' + styleUrls: ['../../comcol-forms/comcol-form.component.scss'], + templateUrl: '../../comcol-forms/comcol-form/comcol-form.component.html' }) -export class CollectionFormComponent { - - name: string; - description: string; - introductory: string; - copyright: string; - news: string; - license: string; - provenance: string; - - nameRequiredError = false; - - @Output() submitted: EventEmitter = new EventEmitter(); - - public constructor(private location: Location) { - - } - - onSubmit(data: any) { - if (isNotEmpty(data.name)) { - this.submitted.emit(data); - this.nameRequiredError = false; - } else { - this.nameRequiredError = true; - } - } - - cancel() { - this.location.back(); - } - +export class CollectionFormComponent extends ComColFormComponent { + @Input() dso: Collection = new Collection(); + type = ResourceType.Collection; + 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', + }), + ]; } diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.html b/src/app/+collection-page/create-collection-page/create-collection-page.component.html index 1d1f8deb6c..aa9569b6b8 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.html +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.html @@ -1,11 +1,11 @@
- - -

{{ 'collection.create.sub-head' | translate:{ parent: community.name } }}

+ + +

{{ 'collection.create.sub-head' | translate:{ parent: parent.name } }}

- +
diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index 1bb4c31596..6ece21c388 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -1,58 +1,54 @@ -import { Component } from '@angular/core'; -import { Community } from '../../core/shared/community.model'; -import { CommunityDataService } from '../../core/data/community-data.service'; -import { CollectionDataService } from '../../core/data/collection-data.service'; -import { Collection } from '../../core/shared/collection.model'; +import { Component, OnInit } from '@angular/core'; +import {Collection} from '../../core/shared/collection.model'; +import {CollectionDataService} from '../../core/data/collection-data.service'; +import { Observable } from 'rxjs'; import { RouteService } from '../../shared/services/route.service'; import { Router } from '@angular/router'; -import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; import { RemoteData } from '../../core/data/remote-data'; -import { isNotEmpty } from '../../shared/empty.util'; -import { ResourceType } from '../../core/shared/resource-type'; +import { isNotEmpty, isNotUndefined } from '../../shared/empty.util'; +import { take } from 'rxjs/operators'; +import { getSucceededRemoteData } from '../../core/shared/operators'; +import {Community} from '../../core/shared/community.model'; +import {CommunityDataService} from '../../core/data/community-data.service'; @Component({ selector: 'ds-create-collection', styleUrls: ['./create-collection-page.component.scss'], templateUrl: './create-collection-page.component.html' }) -export class CreateCollectionPageComponent { +export class CreateCommunityPageComponent implements OnInit { public parentUUID$: Observable; - public communityRDObs: Observable>; + public parentRD$: Observable>; public constructor( - private collectionDataService: CollectionDataService, private communityDataService: CommunityDataService, + private collectionDataService: CollectionDataService, private routeService: RouteService, private router: Router ) { + + } + + ngOnInit(): void { this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); - this.parentUUID$.subscribe((uuid: string) => { - if (isNotEmpty(uuid)) { - this.communityRDObs = this.communityDataService.findById(uuid); + this.parentUUID$.subscribe((parentID: string) => { + if (isNotEmpty(parentID)) { + this.parentRD$ = this.communityDataService.findById(parentID); } }); } - onSubmit(data: any) { + onSubmit(collection: Collection) { this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { - const collection = Object.assign(new Collection(), { - name: data.name, - metadata: [ - { key: 'dc.description', value: data.introductory }, - { key: 'dc.description.abstract', value: data.description }, - { key: 'dc.rights', value: data.copyright }, - { key: 'dc.rights.license', value: data.license } - // TODO: metadata for news and provenance - ], - type: ResourceType.Collection - }); - this.collectionDataService.create(collection, uuid).subscribe((rd: RemoteData) => { - if (rd.hasSucceeded) { - this.router.navigate(['collections', rd.payload.id]); - } - }); + this.collectionDataService.create(collection, uuid) + .pipe(getSucceededRemoteData()) + .subscribe((collectionRD: RemoteData) => { + if (isNotUndefined(collectionRD)) { + const newUUID = collectionRD.payload.uuid; + this.router.navigate(['/collections/' + newUUID]); + } + }); }); } diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.html b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.html new file mode 100644 index 0000000000..1dc6442307 --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.html @@ -0,0 +1,8 @@ +
+
+
+ +
+
+ +
diff --git a/src/app/+collection-page/edit-collection-page/edit-community-page.component.scss b/src/app/+collection-page/edit-collection-page/edit-community-page.component.scss new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/edit-community-page.component.scss @@ -0,0 +1 @@ + diff --git a/src/app/+collection-page/edit-collection-page/edit-community-page.component.spec.ts b/src/app/+collection-page/edit-collection-page/edit-community-page.component.spec.ts new file mode 100644 index 0000000000..84edd47e1d --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/edit-community-page.component.spec.ts @@ -0,0 +1,121 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { RouteService } from '../../shared/services/route.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; +import { RemoteData } from '../../core/data/remote-data'; +import { Community } from '../../core/shared/community.model'; +import { SharedModule } from '../../shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { EditCommunityPageComponent } from './edit-community-page.component'; +import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; + +fdescribe('EditCommunityPageComponent', () => { + let comp: EditCommunityPageComponent; + let fixture: ComponentFixture; + let communityDataService: CommunityDataService; + let routeService: RouteService; + let router: Router; + + let community; + let newCommunity; + let communityDataServiceStub; + let routeServiceStub; + let routerStub; + let routeStub; + + function initializeVars() { + community = Object.assign(new Community(), { + uuid: 'a20da287-e174-466a-9926-f66b9300d347', + metadata: [{ + key: 'dc.title', + value: 'test community' + }] + }); + + newCommunity = Object.assign(new Community(), { + uuid: '1ff59938-a69a-4e62-b9a4-718569c55d48', + metadata: [{ + key: 'dc.title', + value: 'new community' + }] + }); + + communityDataServiceStub = { + findById: (uuid) => observableOf(new RemoteData(false, false, true, null, Object.assign(new Community(), { + uuid: uuid, + metadata: [{ + key: 'dc.title', + value: community.name + }] + }))), + update: (com, uuid?) => observableOf(new RemoteData(false, false, true, undefined, newCommunity)) + + }; + + routeServiceStub = { + getQueryParameterValue: (param) => observableOf(community.uuid) + }; + routerStub = { + navigate: (commands) => commands + }; + + routeStub = { + data: observableOf(community) + }; + + } + + beforeEach(async(() => { + initializeVars(); + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + declarations: [EditCommunityPageComponent], + providers: [ + { provide: CommunityDataService, useValue: communityDataServiceStub }, + { provide: RouteService, useValue: routeServiceStub }, + { provide: Router, useValue: routerStub }, + { provide: ActivatedRoute, useValue: routeStub }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EditCommunityPageComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + communityDataService = (comp as any).communityDataService; + routeService = (comp as any).routeService; + router = (comp as any).router; + }); + + describe('onSubmit', () => { + let data; + beforeEach(() => { + data = Object.assign(new Community(), { + metadata: [{ + key: 'dc.title', + value: 'test' + }] + }); + }); + it('should navigate when successful', () => { + spyOn(router, 'navigate'); + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should not navigate on failure', () => { + spyOn(router, 'navigate'); + spyOn(communityDataService, 'update').and.returnValue(observableOf(new RemoteData(true, true, false, undefined, newCommunity))); + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigate).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/+collection-page/edit-collection-page/edit-community-page.component.ts b/src/app/+collection-page/edit-collection-page/edit-community-page.component.ts new file mode 100644 index 0000000000..236aaff05a --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/edit-community-page.component.ts @@ -0,0 +1,45 @@ +import { Component } from '@angular/core'; +import { Community } from '../../core/shared/community.model'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { Observable } from 'rxjs'; +import { RouteService } from '../../shared/services/route.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { RemoteData } from '../../core/data/remote-data'; +import { isNotUndefined } from '../../shared/empty.util'; +import { first, map } from 'rxjs/operators'; +import { getSucceededRemoteData } from '../../core/shared/operators'; + +@Component({ + selector: 'ds-edit-community', + styleUrls: ['./edit-community-page.component.scss'], + templateUrl: './edit-collection-page.component.html' +}) +export class EditCommunityPageComponent { + + public parentUUID$: Observable; + public parentRD$: Observable>; + public communityRD$: Observable>; + + public constructor( + private communityDataService: CommunityDataService, + private routeService: RouteService, + private router: Router, + private route: ActivatedRoute + ) { + } + + ngOnInit(): void { + this.communityRD$ = this.route.data.pipe(first(), map((data) => data.community)); + } + + onSubmit(community: Community) { + this.communityDataService.update(community) + .pipe(getSucceededRemoteData()) + .subscribe((communityRD: RemoteData) => { + if (isNotUndefined(communityRD)) { + const newUUID = communityRD.payload.uuid; + this.router.navigate(['/communities/' + newUUID]); + } + }); + } +} diff --git a/src/app/+community-page/community-form/community-form.component.scss b/src/app/+community-page/community-form/community-form.component.scss deleted file mode 100644 index d5811186e7..0000000000 --- a/src/app/+community-page/community-form/community-form.component.scss +++ /dev/null @@ -1,7 +0,0 @@ -@import '../../../styles/variables.scss'; - -// temporary fix for bootstrap 4 beta btn color issue -.btn-secondary { - background-color: $input-bg; - color: $input-color; -} diff --git a/src/app/+community-page/community-form/community-form.component.ts b/src/app/+community-page/community-form/community-form.component.ts index c470cbbf91..8dbd2d6490 100644 --- a/src/app/+community-page/community-form/community-form.component.ts +++ b/src/app/+community-page/community-form/community-form.component.ts @@ -1,27 +1,21 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { Location } from '@angular/common'; +import { Component, Input, OnInit, Output } from '@angular/core'; import { - DynamicFormService, DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core'; -import { FormGroup } from '@angular/forms'; 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 { isNotEmpty } from '../../shared/empty.util'; -import { TranslateService } from '@ngx-translate/core'; - -const LABEL_KEY_PREFIX = 'community.form.'; -const ERROR_KEY_PREFIX = 'community.form.errors.'; +import { ComColFormComponent } from '../../comcol-forms/comcol-form/comcol-form.component'; @Component({ selector: 'ds-community-form', - styleUrls: ['./community-form.component.scss'], - templateUrl: './community-form.component.html' + styleUrls: ['../../comcol-forms/comcol-form.component.scss'], + templateUrl: '../../comcol-forms/comcol-form/comcol-form.component.html' }) -export class CommunityFormComponent implements OnInit { - @Input() community: Community = new Community(); +export class CommunityFormComponent extends ComColFormComponent { + @Input() dso: Community = new Community(); + type = ResourceType.Community; formModel: DynamicFormControlModel[] = [ new DynamicInputModel({ id: 'title', @@ -51,57 +45,4 @@ export class CommunityFormComponent implements OnInit { name: 'dc.description.tableofcontents', }), ]; - - formGroup: FormGroup; - - @Output() submitForm: EventEmitter = new EventEmitter(); - - public constructor(private location: Location, - private formService: DynamicFormService, - private translate: TranslateService) { - } - - ngOnInit(): void { - this.formModel.forEach( - (fieldModel: DynamicInputModel) => { - fieldModel.value = this.community.findMetadata(fieldModel.name); - } - ); - this.formGroup = this.formService.createFormGroup(this.formModel); - this.updateFieldTranslations(); - this.translate.onLangChange - .subscribe(() => { - this.updateFieldTranslations(); - }); - } - - onSubmit() { - const metadata = this.formModel.map( - (fieldModel: DynamicInputModel) => { - return { key: fieldModel.name, value: fieldModel.value } - } - ); - const filteredOldMetadata = this.community.metadata.filter((filter) => !metadata.map((md) => md.key).includes(filter.key)); - const filteredNewMetadata = metadata.filter((md) => isNotEmpty(md.value)); - const newMetadata = [...filteredOldMetadata, ...filteredNewMetadata]; - const updatedCommunity = Object.assign({}, this.community, { - metadata: newMetadata, - type: ResourceType.Community - }); - this.submitForm.emit(updatedCommunity); - } - - private updateFieldTranslations() { - this.formModel.forEach( - (fieldModel: DynamicInputModel) => { - fieldModel.label = this.translate.instant(LABEL_KEY_PREFIX + fieldModel.id); - if (isNotEmpty(fieldModel.validators)) { - fieldModel.errorMessages = {}; - Object.keys(fieldModel.validators).forEach((key) => { - fieldModel.errorMessages[key] = this.translate.instant(ERROR_KEY_PREFIX + fieldModel.id + '.' + key); - }); - } - } - ); - } } diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index 8871d33547..98156f5a8a 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -1,54 +1,21 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { Community } from '../../core/shared/community.model'; import { CommunityDataService } from '../../core/data/community-data.service'; -import { Observable } from 'rxjs'; import { RouteService } from '../../shared/services/route.service'; import { Router } from '@angular/router'; -import { RemoteData } from '../../core/data/remote-data'; -import { isNotEmpty, isNotUndefined } from '../../shared/empty.util'; -import { DSpaceObject } from '../../core/shared/dspace-object.model'; -import { map, take } from 'rxjs/operators'; -import { getSucceededRemoteData } from '../../core/shared/operators'; +import { CreateComColPageComponent } from '../../comcol-forms/create-comcol-page/create-comcol-page.component'; @Component({ selector: 'ds-create-community', styleUrls: ['./create-community-page.component.scss'], templateUrl: './create-community-page.component.html' }) -export class CreateCommunityPageComponent implements OnInit { - - public parentUUID$: Observable; - public parentRD$: Observable>; - +export class CreateCommunityPageComponent extends CreateComColPageComponent { public constructor( - private communityDataService: CommunityDataService, - private routeService: RouteService, - private router: Router + protected communityDataService: CommunityDataService, + protected routeService: RouteService, + protected router: Router ) { - + super(communityDataService, communityDataService, routeService, router); } - - ngOnInit(): void { - this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); - this.parentUUID$.subscribe((parentID: string) => { - if (isNotEmpty(parentID)) { - this.parentRD$ = this.communityDataService.findById(parentID); - } - }); - } - - onSubmit(community: Community) { - this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { - this.communityDataService.create(community, uuid) - .pipe(getSucceededRemoteData()) - .subscribe((communityRD: RemoteData) => { - if (isNotUndefined(communityRD)) { - const newUUID = communityRD.payload.uuid; - this.router.navigate(['/communities/' + newUUID]); - } - }); - }); - } - - } diff --git a/src/app/+community-page/community-form/community-form.component.html b/src/app/comcol-forms/comcol-form/comcol-form.component.html similarity index 66% rename from src/app/+community-page/community-form/community-form.component.html rename to src/app/comcol-forms/comcol-form/comcol-form.component.html index c47f077022..720ad0c1cf 100644 --- a/src/app/+community-page/community-form/community-form.component.html +++ b/src/app/comcol-forms/comcol-form/comcol-form.component.html @@ -1,3 +1,3 @@ \ No newline at end of file + [formId]="'comcol-form-id'" + [formModel]="formModel" (submitForm)="onSubmit()"> diff --git a/src/app/comcol-forms/comcol-form/comcol-form.component.ts b/src/app/comcol-forms/comcol-form/comcol-form.component.ts new file mode 100644 index 0000000000..d49e197f5d --- /dev/null +++ b/src/app/comcol-forms/comcol-form/comcol-form.component.ts @@ -0,0 +1,77 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Location } from '@angular/common'; +import { + DynamicFormService, + DynamicInputModel +} from '@ng-dynamic-forms/core'; +import { FormGroup } from '@angular/forms'; +import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model'; +import { TranslateService } from '@ngx-translate/core'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { isNotEmpty } from '../../shared/empty.util'; +import { ResourceType } from '../../core/shared/resource-type'; + +@Component({ + selector: 'ds-comcol-form', + // styleUrls: ['./comcol-form.component.scss'], + templateUrl: './comcol-form.component.html' +}) +export class ComColFormComponent implements OnInit { + @Input() dso: T; + type; + LABEL_KEY_PREFIX = this.type + '.form.'; + ERROR_KEY_PREFIX = this.type + '.form.errors.'; + formModel: DynamicFormControlModel[]; + formGroup: FormGroup; + + @Output() submitForm: EventEmitter = new EventEmitter(); + + public constructor(private location: Location, + private formService: DynamicFormService, + private translate: TranslateService) { + } + + ngOnInit(): void { + this.formModel.forEach( + (fieldModel: DynamicInputModel) => { + fieldModel.value = this.dso.findMetadata(fieldModel.name); + } + ); + this.formGroup = this.formService.createFormGroup(this.formModel); + this.updateFieldTranslations(); + this.translate.onLangChange + .subscribe(() => { + this.updateFieldTranslations(); + }); + } + + onSubmit() { + const metadata = this.formModel.map( + (fieldModel: DynamicInputModel) => { + return { key: fieldModel.name, value: fieldModel.value } + } + ); + const filteredOldMetadata = this.dso.metadata.filter((filter) => !metadata.map((md) => md.key).includes(filter.key)); + const filteredNewMetadata = metadata.filter((md) => isNotEmpty(md.value)); + const newMetadata = [...filteredOldMetadata, ...filteredNewMetadata]; + const updatedDSO = Object.assign({}, this.dso, { + metadata: newMetadata, + type: ResourceType.Community + }); + this.submitForm.emit(updatedDSO); + } + + private updateFieldTranslations() { + this.formModel.forEach( + (fieldModel: DynamicInputModel) => { + fieldModel.label = this.translate.instant(this.LABEL_KEY_PREFIX + fieldModel.id); + if (isNotEmpty(fieldModel.validators)) { + fieldModel.errorMessages = {}; + Object.keys(fieldModel.validators).forEach((key) => { + fieldModel.errorMessages[key] = this.translate.instant(this.ERROR_KEY_PREFIX + fieldModel.id + '.' + key); + }); + } + } + ); + } +} diff --git a/src/app/comcol-forms/create-comcol-page/create-comcol-page.component.ts b/src/app/comcol-forms/create-comcol-page/create-comcol-page.component.ts new file mode 100644 index 0000000000..e36f45ad18 --- /dev/null +++ b/src/app/comcol-forms/create-comcol-page/create-comcol-page.component.ts @@ -0,0 +1,55 @@ +import { Component, OnInit } from '@angular/core'; +import { Community } from '../../core/shared/community.model'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { Observable } from 'rxjs'; +import { RouteService } from '../../shared/services/route.service'; +import { Router } from '@angular/router'; +import { RemoteData } from '../../core/data/remote-data'; +import { isNotEmpty, isNotUndefined } from '../../shared/empty.util'; +import { take } from 'rxjs/operators'; +import { getSucceededRemoteData } from '../../core/shared/operators'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { DataService } from '../../core/data/data.service'; + +@Component({ + selector: 'ds-create-community', + styleUrls: ['./create-community-page.component.scss'], + templateUrl: './create-community-page.component.html' +}) +export class CreateComColPageComponent implements OnInit { + protected frontendURL: string; + public parentUUID$: Observable; + public parentRD$: Observable>; + + public constructor( + protected dsoDataService: DataService, + protected parentoDataService: CommunityDataService, + protected routeService: RouteService, + protected router: Router + ) { + + } + + ngOnInit(): void { + this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); + this.parentUUID$.subscribe((parentID: string) => { + if (isNotEmpty(parentID)) { + this.parentRD$ = this.parentoDataService.findById(parentID); + } + }); + } + + onSubmit(dso: T) { + this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { + this.dsoDataService.create(dso, uuid) + .pipe(getSucceededRemoteData()) + .subscribe((dsoRD: RemoteData) => { + if (isNotUndefined(dsoRD)) { + const newUUID = dsoRD.payload.uuid; + this.router.navigate([frontendURL + newUUID]); + } + }); + }); + } + +} From e0ed960d4c814fec1413f40648bfdc3b3d1052ee Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 2 Jan 2019 07:44:38 +0100 Subject: [PATCH 59/69] refactoring continued --- .../create-collection-page.component.ts | 61 +++++-------------- .../create-community-page.component.ts | 4 +- .../create-comcol-page.component.ts | 17 +++--- .../edit-comcol-page.component.ts | 45 ++++++++++++++ 4 files changed, 73 insertions(+), 54 deletions(-) create mode 100644 src/app/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index 6ece21c388..8dbc9183e5 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -1,55 +1,26 @@ -import { Component, OnInit } from '@angular/core'; -import {Collection} from '../../core/shared/collection.model'; -import {CollectionDataService} from '../../core/data/collection-data.service'; -import { Observable } from 'rxjs'; +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 { RemoteData } from '../../core/data/remote-data'; -import { isNotEmpty, isNotUndefined } from '../../shared/empty.util'; -import { take } from 'rxjs/operators'; -import { getSucceededRemoteData } from '../../core/shared/operators'; -import {Community} from '../../core/shared/community.model'; -import {CommunityDataService} from '../../core/data/community-data.service'; +import { CreateComColPageComponent } from '../../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({ - selector: 'ds-create-collection', - styleUrls: ['./create-collection-page.component.scss'], - templateUrl: './create-collection-page.component.html' + selector: 'ds-create-community', + styleUrls: ['./create-community-page.component.scss'], + templateUrl: './create-community-page.component.html' }) -export class CreateCommunityPageComponent implements OnInit { - - public parentUUID$: Observable; - public parentRD$: Observable>; +export class CreateCommunityPageComponent extends CreateComColPageComponent { + protected frontendURL = 'collections'; public constructor( - private communityDataService: CommunityDataService, - private collectionDataService: CollectionDataService, - private routeService: RouteService, - private router: Router + protected communityDataService: CommunityDataService, + protected collectionDataService: CollectionDataService, + protected routeService: RouteService, + protected router: Router ) { - + super(collectionDataService, communityDataService, routeService, router); } - - ngOnInit(): void { - this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); - this.parentUUID$.subscribe((parentID: string) => { - if (isNotEmpty(parentID)) { - this.parentRD$ = this.communityDataService.findById(parentID); - } - }); - } - - onSubmit(collection: Collection) { - this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { - this.collectionDataService.create(collection, uuid) - .pipe(getSucceededRemoteData()) - .subscribe((collectionRD: RemoteData) => { - if (isNotUndefined(collectionRD)) { - const newUUID = collectionRD.payload.uuid; - this.router.navigate(['/collections/' + newUUID]); - } - }); - }); - } - } diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index 98156f5a8a..5a3cf1f4a8 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -4,13 +4,15 @@ import { CommunityDataService } from '../../core/data/community-data.service'; import { RouteService } from '../../shared/services/route.service'; import { Router } from '@angular/router'; import { CreateComColPageComponent } from '../../comcol-forms/create-comcol-page/create-comcol-page.component'; +import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model'; @Component({ selector: 'ds-create-community', styleUrls: ['./create-community-page.component.scss'], templateUrl: './create-community-page.component.html' }) -export class CreateCommunityPageComponent extends CreateComColPageComponent { +export class CreateCommunityPageComponent extends CreateComColPageComponent { + protected frontendURL = 'communities'; public constructor( protected communityDataService: CommunityDataService, protected routeService: RouteService, diff --git a/src/app/comcol-forms/create-comcol-page/create-comcol-page.component.ts b/src/app/comcol-forms/create-comcol-page/create-comcol-page.component.ts index e36f45ad18..18f23cf528 100644 --- a/src/app/comcol-forms/create-comcol-page/create-comcol-page.component.ts +++ b/src/app/comcol-forms/create-comcol-page/create-comcol-page.component.ts @@ -10,20 +10,21 @@ import { take } from 'rxjs/operators'; import { getSucceededRemoteData } from '../../core/shared/operators'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DataService } from '../../core/data/data.service'; +import { NormalizedDSpaceObject } from '../../core/cache/models/normalized-dspace-object.model'; @Component({ selector: 'ds-create-community', styleUrls: ['./create-community-page.component.scss'], templateUrl: './create-community-page.component.html' }) -export class CreateComColPageComponent implements OnInit { +export class CreateComColPageComponent implements OnInit { protected frontendURL: string; public parentUUID$: Observable; - public parentRD$: Observable>; + public parentRD$: Observable>; public constructor( - protected dsoDataService: DataService, - protected parentoDataService: CommunityDataService, + protected dsoDataService: DataService, + protected parentDataService: CommunityDataService, protected routeService: RouteService, protected router: Router ) { @@ -34,19 +35,19 @@ export class CreateComColPageComponent implements OnInit this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); this.parentUUID$.subscribe((parentID: string) => { if (isNotEmpty(parentID)) { - this.parentRD$ = this.parentoDataService.findById(parentID); + this.parentRD$ = this.parentDataService.findById(parentID); } }); } - onSubmit(dso: T) { + onSubmit(dso: TDomain) { this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { this.dsoDataService.create(dso, uuid) .pipe(getSucceededRemoteData()) - .subscribe((dsoRD: RemoteData) => { + .subscribe((dsoRD: RemoteData) => { if (isNotUndefined(dsoRD)) { const newUUID = dsoRD.payload.uuid; - this.router.navigate([frontendURL + newUUID]); + this.router.navigate([this.frontendURL + newUUID]); } }); }); diff --git a/src/app/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts b/src/app/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts new file mode 100644 index 0000000000..5e11a71c2e --- /dev/null +++ b/src/app/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts @@ -0,0 +1,45 @@ +import {Component, OnInit} from '@angular/core'; +import { Community } from '../../core/shared/community.model'; +import { Observable } from 'rxjs'; +import { RouteService } from '../../shared/services/route.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { RemoteData } from '../../core/data/remote-data'; +import { isNotUndefined } from '../../shared/empty.util'; +import { first, map } from 'rxjs/operators'; +import { getSucceededRemoteData } from '../../core/shared/operators'; +import { DataService } from '../../core/data/data.service'; +import { NormalizedDSpaceObject } from '../../core/cache/models/normalized-dspace-object.model'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; + +@Component({ + selector: 'ds-edit-community', + styleUrls: ['./edit-community-page.component.scss'], + templateUrl: './edit-community-page.component.html' +}) +export class EditComColPageComponent implements OnInit { + protected frontendURL: string; + public dsoRD$: Observable>; + + public constructor( + protected dsoDataService: DataService, + private routeService: RouteService, + private router: Router, + private route: ActivatedRoute + ) { + } + + ngOnInit(): void { + this.dsoRD$ = this.route.data.pipe(first(), map((data) => data.dso)); + } + + onSubmit(dso: TDomain) { + this.dsoDataService.update(dso) + .pipe(getSucceededRemoteData()) + .subscribe((dsoRD: RemoteData) => { + if (isNotUndefined(dsoRD)) { + const newUUID = dsoRD.payload.uuid; + this.router.navigate([this.frontendURL + newUUID]); + } + }); + } +} From 1ebd6f0e86a2f0e06edf43c7f90fc6e391411b5e Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 2 Jan 2019 16:51:41 +0100 Subject: [PATCH 60/69] Finished refactoring, guards, docs, tests --- .../collection-form.component.spec.ts | 73 ----------- .../collection-form.component.ts | 23 +++- .../collection-page-routing.module.ts | 18 ++- .../collection-page.component.ts | 3 +- .../collection-page.module.ts | 2 + .../create-collection-page.component.html | 13 +- .../create-collection-page.component.spec.ts | 108 ---------------- .../create-collection-page.component.ts | 15 ++- .../create-collection-page.guard.spec.ts | 67 ++++++++++ .../create-collection-page.guard.ts | 46 +++++++ .../edit-collection-page.component.html | 4 +- ...ss => edit-collection-page.component.scss} | 0 .../edit-collection-page.component.ts | 28 ++++ .../edit-community-page.component.spec.ts | 121 ------------------ .../edit-community-page.component.ts | 45 ------- .../community-form.component.ts | 30 +++-- .../community-page-routing.module.ts | 12 +- .../community-page.component.html | 1 - .../create-community-page.component.html | 2 +- .../create-community-page.component.ts | 8 +- .../create-community-page.guard.spec.ts | 67 ++++++++++ .../create-community-page.guard.ts | 46 +++++++ .../edit-community-page.component.html | 2 +- .../edit-community-page.component.ts | 41 ++---- .../edit-comcol-page.component.ts | 45 ------- .../config-response-parsing.service.spec.ts | 8 +- src/app/core/data/data.service.spec.ts | 30 +++-- src/app/core/data/update-comparator.ts | 2 +- .../core/metadata/metadata.service.spec.ts | 4 + src/app/core/metadata/metadata.service.ts | 6 +- src/app/core/shared/operators.ts | 4 + .../comcol-form/comcol-form.component.html | 0 .../comcol-form/comcol-form.component.scss} | 0 .../comcol-form.component.spec.ts} | 59 +++++---- .../comcol-form/comcol-form.component.ts | 53 ++++++-- .../create-comcol-page.component.spec.ts} | 29 +++-- .../create-comcol-page.component.ts | 43 +++++-- .../edit-comcol-page.component.spec.ts} | 31 +++-- .../edit-comcol-page.component.ts | 56 ++++++++ src/app/shared/shared.module.ts | 6 + src/app/shared/testing/utils.ts | 2 +- 41 files changed, 600 insertions(+), 553 deletions(-) delete mode 100644 src/app/+collection-page/collection-form/collection-form.component.spec.ts delete mode 100644 src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts create mode 100644 src/app/+collection-page/create-collection-page/create-collection-page.guard.spec.ts create mode 100644 src/app/+collection-page/create-collection-page/create-collection-page.guard.ts rename src/app/+collection-page/edit-collection-page/{edit-community-page.component.scss => edit-collection-page.component.scss} (100%) create mode 100644 src/app/+collection-page/edit-collection-page/edit-collection-page.component.ts delete mode 100644 src/app/+collection-page/edit-collection-page/edit-community-page.component.spec.ts delete mode 100644 src/app/+collection-page/edit-collection-page/edit-community-page.component.ts create mode 100644 src/app/+community-page/create-community-page/create-community-page.guard.spec.ts create mode 100644 src/app/+community-page/create-community-page/create-community-page.guard.ts delete mode 100644 src/app/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts rename src/app/{ => shared}/comcol-forms/comcol-form/comcol-form.component.html (100%) rename src/app/shared/{mocks/mock-response-cache.service.ts => comcol-forms/comcol-form/comcol-form.component.scss} (100%) rename src/app/{+community-page/community-form/community-form.component.spec.ts => shared/comcol-forms/comcol-form/comcol-form.component.spec.ts} (58%) rename src/app/{ => shared}/comcol-forms/comcol-form/comcol-form.component.ts (65%) rename src/app/{+community-page/create-community-page/create-community-page.component.spec.ts => shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts} (70%) rename src/app/{ => shared}/comcol-forms/create-comcol-page/create-comcol-page.component.ts (54%) rename src/app/{+community-page/edit-community-page/edit-community-page.component.spec.ts => shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.spec.ts} (71%) create mode 100644 src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts diff --git a/src/app/+collection-page/collection-form/collection-form.component.spec.ts b/src/app/+collection-page/collection-form/collection-form.component.spec.ts deleted file mode 100644 index 71ae92572d..0000000000 --- a/src/app/+collection-page/collection-form/collection-form.component.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { SharedModule } from '../../shared/shared.module'; -import { TranslateModule } from '@ngx-translate/core'; -import { CommonModule } from '@angular/common'; -import { RouterTestingModule } from '@angular/router/testing'; -import { By } from '@angular/platform-browser'; -import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; -import { CollectionFormComponent } from './collection-form.component'; -import { Location } from '@angular/common'; -import { DynamicFormService } from '@ng-dynamic-forms/core'; - -describe('CommunityFormComponent', () => { - let comp: CollectionFormComponent; - let fixture: ComponentFixture - let location: Location; - - /* tslint:disable:no-empty */ - const locationStub = { - back: () => {} - }; - /* tslint:enable:no-empty */ - - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [CollectionFormComponent], - providers: [ - { provide: Location, useValue: locationStub }, - ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(CollectionFormComponent); - comp = fixture.componentInstance; - fixture.detectChanges(); - location = (comp as any).location; - }); - - describe('when submitting', () => { - let input: DebugElement; - let submit: DebugElement; - let cancel: DebugElement; - let error: DebugElement; - - beforeEach(() => { - input = fixture.debugElement.query(By.css('input#collection-name')); - submit = fixture.debugElement.query(By.css('button#collection-submit')); - cancel = fixture.debugElement.query(By.css('button#collection-cancel')); - error = fixture.debugElement.query(By.css('div.invalid-feedback')); - }); - - it('should display an error when leaving name empty', () => { - const el = input.nativeElement; - - el.value = ''; - el.dispatchEvent(new Event('input')); - submit.nativeElement.click(); - fixture.detectChanges(); - - expect(error.nativeElement.style.display).not.toEqual('none'); - }); - - it('should navigate back when pressing cancel', () => { - spyOn(location, 'back'); - cancel.nativeElement.click(); - fixture.detectChanges(); - - expect(location.back).toHaveBeenCalled(); - }); - }) -}); diff --git a/src/app/+collection-page/collection-form/collection-form.component.ts b/src/app/+collection-page/collection-form/collection-form.component.ts index 03bd9bfda9..22f2f1271d 100644 --- a/src/app/+collection-page/collection-form/collection-form.component.ts +++ b/src/app/+collection-page/collection-form/collection-form.component.ts @@ -6,16 +6,31 @@ import { 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 '../../comcol-forms/comcol-form/comcol-form.component'; +import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component'; +/** + * Form used for creating and editing collections + */ @Component({ selector: 'ds-collection-form', - styleUrls: ['../../comcol-forms/comcol-form.component.scss'], - templateUrl: '../../comcol-forms/comcol-form/comcol-form.component.html' + 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 { + /** + * @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; + + /** + * @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', diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index 0b1245578c..ec53796e61 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -5,13 +5,26 @@ import { CollectionPageComponent } from './collection-page.component'; 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'; @NgModule({ imports: [ RouterModule.forChild([ - { path: 'create', + { + path: 'create', component: CreateCollectionPageComponent, - canActivate: [AuthenticatedGuard] }, + canActivate: [AuthenticatedGuard, CreateCollectionPageGuard] + }, + { + path: ':id/edit', + pathMatch: 'full', + component: EditCollectionPageComponent, + canActivate: [AuthenticatedGuard], + resolve: { + dso: CollectionPageResolver + } + }, { path: ':id', component: CollectionPageComponent, @@ -24,6 +37,7 @@ import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; ], providers: [ CollectionPageResolver, + CreateCollectionPageGuard ] }) export class CollectionPageRoutingModule { diff --git a/src/app/+collection-page/collection-page.component.ts b/src/app/+collection-page/collection-page.component.ts index b76c0a7520..7c4f2b92ac 100644 --- a/src/app/+collection-page/collection-page.component.ts +++ b/src/app/+collection-page/collection-page.component.ts @@ -55,7 +55,8 @@ export class CollectionPageComponent implements OnInit, OnDestroy { ngOnInit(): void { 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( map((rd: RemoteData) => rd.payload), diff --git a/src/app/+collection-page/collection-page.module.ts b/src/app/+collection-page/collection-page.module.ts index a6316e9189..63a03f4299 100644 --- a/src/app/+collection-page/collection-page.module.ts +++ b/src/app/+collection-page/collection-page.module.ts @@ -8,6 +8,7 @@ import { CollectionPageRoutingModule } from './collection-page-routing.module'; import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component'; import { CollectionFormComponent } from './collection-form/collection-form.component'; import { SearchPageModule } from '../+search-page/search-page.module'; +import { EditCollectionPageComponent } from './edit-collection-page/edit-collection-page.component'; @NgModule({ imports: [ @@ -19,6 +20,7 @@ import { SearchPageModule } from '../+search-page/search-page.module'; declarations: [ CollectionPageComponent, CreateCollectionPageComponent, + EditCollectionPageComponent, CollectionFormComponent ] }) diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.html b/src/app/+collection-page/create-collection-page/create-collection-page.component.html index aa9569b6b8..b3f4361bc6 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.html +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.html @@ -1,11 +1,8 @@
-
-
- - -

{{ 'collection.create.sub-head' | translate:{ parent: parent.name } }}

-
+
+
+

{{'collection.create.sub-head' | translate:{ parent: (parentRD$| async)?.payload.name } }}

+
-
- +
diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts deleted file mode 100644 index 837741d593..0000000000 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { SharedModule } from '../../shared/shared.module'; -import { Community } from '../../core/shared/community.model'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { CommonModule, Location } from '@angular/common'; -import { Router } from '@angular/router'; -import { TranslateModule } from '@ngx-translate/core'; -import { Observable } from 'rxjs'; -import { CommunityDataService } from '../../core/data/community-data.service'; -import { RouteService } from '../../shared/services/route.service'; -import { RemoteData } from '../../core/data/remote-data'; -import { CreateCollectionPageComponent } from './create-collection-page.component'; -import { CollectionDataService } from '../../core/data/collection-data.service'; -import { Collection } from '../../core/shared/collection.model'; -import { RouterTestingModule } from '@angular/router/testing'; -import { CollectionFormComponent } from '../collection-form/collection-form.component'; -import { of as observableOf } from 'rxjs'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; - -describe('CreateCollectionPageComponent', () => { - let comp: CreateCollectionPageComponent; - let fixture: ComponentFixture; - let collectionDataService: CollectionDataService; - let communityDataService: CommunityDataService; - let routeService: RouteService; - let router: Router; - - const community = Object.assign(new Community(), { - uuid: 'a20da287-e174-466a-9926-f66b9300d347', - metadata: [{ - key: 'dc.title', - value: 'test collection' - }] - }); - - const collection = Object.assign(new Collection(), { - uuid: 'ce41d451-97ed-4a9c-94a1-7de34f16a9f4', - metadata: [{ - key: 'dc.title', - value: 'new collection' - }] }); - - const collectionDataServiceStub = { - create: (col, uuid?) => observableOf(new RemoteData(false, false, true, undefined, collection)) - }; - const communityDataServiceStub = { - findById: (uuid) => observableOf(new RemoteData(false, false, true, null, Object.assign(new Community(), { - uuid: uuid, - metadata: [{ - key: 'dc.title', - value: community.name - }] - }))) - }; - const routeServiceStub = { - getQueryParameterValue: (param) => observableOf(community.uuid) - }; - const routerStub = { - navigate: (commands) => commands - }; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [CreateCollectionPageComponent], - providers: [ - { provide: CollectionDataService, useValue: collectionDataServiceStub }, - { provide: CommunityDataService, useValue: communityDataServiceStub }, - { provide: RouteService, useValue: routeServiceStub }, - { provide: Router, useValue: routerStub } - ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(CreateCollectionPageComponent); - comp = fixture.componentInstance; - fixture.detectChanges(); - collectionDataService = (comp as any).collectionDataService; - communityDataService = (comp as any).communityDataService; - routeService = (comp as any).routeService; - router = (comp as any).router; - }); - - describe('onSubmit', () => { - const data = { - metadata: [{ - key: 'dc.title', - value:'test' - }] - }; - - it('should navigate when successful', () => { - spyOn(router, 'navigate'); - comp.onSubmit(data); - fixture.detectChanges(); - expect(router.navigate).toHaveBeenCalled(); - }); - - it('should not navigate on failure', () => { - spyOn(router, 'navigate'); - spyOn(collectionDataService, 'create').and.returnValue(observableOf(new RemoteData(true, true, false, undefined, collection))); - comp.onSubmit(data); - fixture.detectChanges(); - expect(router.navigate).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index 8dbc9183e5..52497694b9 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -2,18 +2,21 @@ 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 '../../comcol-forms/create-comcol-page/create-comcol-page.component'; +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-community', - styleUrls: ['./create-community-page.component.scss'], - templateUrl: './create-community-page.component.html' + selector: 'ds-create-collection', + styleUrls: ['./create-collection-page.component.scss'], + templateUrl: './create-collection-page.component.html' }) -export class CreateCommunityPageComponent extends CreateComColPageComponent { - protected frontendURL = 'collections'; +export class CreateCollectionPageComponent extends CreateComColPageComponent { + protected frontendURL = '/collections/'; public constructor( protected communityDataService: CommunityDataService, diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.guard.spec.ts b/src/app/+collection-page/create-collection-page/create-collection-page.guard.spec.ts new file mode 100644 index 0000000000..5d21ae36b3 --- /dev/null +++ b/src/app/+collection-page/create-collection-page/create-collection-page.guard.spec.ts @@ -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) + ); + }); + }); +}); diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.guard.ts b/src/app/+collection-page/create-collection-page/create-collection-page.guard.ts new file mode 100644 index 0000000000..4cd842e926 --- /dev/null +++ b/src/app/+collection-page/create-collection-page/create-collection-page.guard.ts @@ -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 { + const parentID = route.queryParams.parent; + if (hasNoValue(parentID)) { + this.router.navigate(['/404']); + return observableOf(false); + } + const parent: Observable> = this.communityService.findById(parentID) + .pipe( + getFinishedRemoteData(), + ); + + return parent.pipe( + map((communityRD: RemoteData) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)), + tap((isValid: boolean) => { + if (!isValid) { + this.router.navigate(['/404']); + } + }) + ); + } +} diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.html b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.html index 1dc6442307..1308af919f 100644 --- a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.html +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.html @@ -1,8 +1,8 @@
- +
- +
diff --git a/src/app/+collection-page/edit-collection-page/edit-community-page.component.scss b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.scss similarity index 100% rename from src/app/+collection-page/edit-collection-page/edit-community-page.component.scss rename to src/app/+collection-page/edit-collection-page/edit-collection-page.component.scss diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.ts b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.ts new file mode 100644 index 0000000000..2ffb4925a0 --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; +import { RouteService } from '../../shared/services/route.service'; +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 { + protected frontendURL = '/collections/'; + + public constructor( + protected collectionDataService: CollectionDataService, + protected routeService: RouteService, + protected router: Router, + protected route: ActivatedRoute + ) { + super(collectionDataService, routeService, router, route); + } +} diff --git a/src/app/+collection-page/edit-collection-page/edit-community-page.component.spec.ts b/src/app/+collection-page/edit-collection-page/edit-community-page.component.spec.ts deleted file mode 100644 index 84edd47e1d..0000000000 --- a/src/app/+collection-page/edit-collection-page/edit-community-page.component.spec.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { CommunityDataService } from '../../core/data/community-data.service'; -import { RouteService } from '../../shared/services/route.service'; -import { ActivatedRoute, Router } from '@angular/router'; -import { TranslateModule } from '@ngx-translate/core'; -import { of as observableOf } from 'rxjs'; -import { RemoteData } from '../../core/data/remote-data'; -import { Community } from '../../core/shared/community.model'; -import { SharedModule } from '../../shared/shared.module'; -import { CommonModule } from '@angular/common'; -import { RouterTestingModule } from '@angular/router/testing'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { EditCommunityPageComponent } from './edit-community-page.component'; -import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; - -fdescribe('EditCommunityPageComponent', () => { - let comp: EditCommunityPageComponent; - let fixture: ComponentFixture; - let communityDataService: CommunityDataService; - let routeService: RouteService; - let router: Router; - - let community; - let newCommunity; - let communityDataServiceStub; - let routeServiceStub; - let routerStub; - let routeStub; - - function initializeVars() { - community = Object.assign(new Community(), { - uuid: 'a20da287-e174-466a-9926-f66b9300d347', - metadata: [{ - key: 'dc.title', - value: 'test community' - }] - }); - - newCommunity = Object.assign(new Community(), { - uuid: '1ff59938-a69a-4e62-b9a4-718569c55d48', - metadata: [{ - key: 'dc.title', - value: 'new community' - }] - }); - - communityDataServiceStub = { - findById: (uuid) => observableOf(new RemoteData(false, false, true, null, Object.assign(new Community(), { - uuid: uuid, - metadata: [{ - key: 'dc.title', - value: community.name - }] - }))), - update: (com, uuid?) => observableOf(new RemoteData(false, false, true, undefined, newCommunity)) - - }; - - routeServiceStub = { - getQueryParameterValue: (param) => observableOf(community.uuid) - }; - routerStub = { - navigate: (commands) => commands - }; - - routeStub = { - data: observableOf(community) - }; - - } - - beforeEach(async(() => { - initializeVars(); - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [EditCommunityPageComponent], - providers: [ - { provide: CommunityDataService, useValue: communityDataServiceStub }, - { provide: RouteService, useValue: routeServiceStub }, - { provide: Router, useValue: routerStub }, - { provide: ActivatedRoute, useValue: routeStub }, - ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(EditCommunityPageComponent); - comp = fixture.componentInstance; - fixture.detectChanges(); - communityDataService = (comp as any).communityDataService; - routeService = (comp as any).routeService; - router = (comp as any).router; - }); - - describe('onSubmit', () => { - let data; - beforeEach(() => { - data = Object.assign(new Community(), { - metadata: [{ - key: 'dc.title', - value: 'test' - }] - }); - }); - it('should navigate when successful', () => { - spyOn(router, 'navigate'); - comp.onSubmit(data); - fixture.detectChanges(); - expect(router.navigate).toHaveBeenCalled(); - }); - - it('should not navigate on failure', () => { - spyOn(router, 'navigate'); - spyOn(communityDataService, 'update').and.returnValue(observableOf(new RemoteData(true, true, false, undefined, newCommunity))); - comp.onSubmit(data); - fixture.detectChanges(); - expect(router.navigate).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/src/app/+collection-page/edit-collection-page/edit-community-page.component.ts b/src/app/+collection-page/edit-collection-page/edit-community-page.component.ts deleted file mode 100644 index 236aaff05a..0000000000 --- a/src/app/+collection-page/edit-collection-page/edit-community-page.component.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Component } from '@angular/core'; -import { Community } from '../../core/shared/community.model'; -import { CommunityDataService } from '../../core/data/community-data.service'; -import { Observable } from 'rxjs'; -import { RouteService } from '../../shared/services/route.service'; -import { ActivatedRoute, Router } from '@angular/router'; -import { RemoteData } from '../../core/data/remote-data'; -import { isNotUndefined } from '../../shared/empty.util'; -import { first, map } from 'rxjs/operators'; -import { getSucceededRemoteData } from '../../core/shared/operators'; - -@Component({ - selector: 'ds-edit-community', - styleUrls: ['./edit-community-page.component.scss'], - templateUrl: './edit-collection-page.component.html' -}) -export class EditCommunityPageComponent { - - public parentUUID$: Observable; - public parentRD$: Observable>; - public communityRD$: Observable>; - - public constructor( - private communityDataService: CommunityDataService, - private routeService: RouteService, - private router: Router, - private route: ActivatedRoute - ) { - } - - ngOnInit(): void { - this.communityRD$ = this.route.data.pipe(first(), map((data) => data.community)); - } - - onSubmit(community: Community) { - this.communityDataService.update(community) - .pipe(getSucceededRemoteData()) - .subscribe((communityRD: RemoteData) => { - if (isNotUndefined(communityRD)) { - const newUUID = communityRD.payload.uuid; - this.router.navigate(['/communities/' + newUUID]); - } - }); - } -} diff --git a/src/app/+community-page/community-form/community-form.component.ts b/src/app/+community-page/community-form/community-form.component.ts index 8dbd2d6490..9ae6f0955d 100644 --- a/src/app/+community-page/community-form/community-form.component.ts +++ b/src/app/+community-page/community-form/community-form.component.ts @@ -1,21 +1,33 @@ -import { Component, Input, OnInit, Output } from '@angular/core'; -import { - DynamicInputModel, - DynamicTextAreaModel -} from '@ng-dynamic-forms/core'; +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 '../../comcol-forms/comcol-form/comcol-form.component'; +import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component'; +/** + * Form used for creating and editing communities + */ @Component({ selector: 'ds-community-form', - styleUrls: ['../../comcol-forms/comcol-form.component.scss'], - templateUrl: '../../comcol-forms/comcol-form/comcol-form.component.html' + 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 { + /** + * @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; + + /** + * @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', diff --git a/src/app/+community-page/community-page-routing.module.ts b/src/app/+community-page/community-page-routing.module.ts index 8fcc2cde6f..4cd803a3a5 100644 --- a/src/app/+community-page/community-page-routing.module.ts +++ b/src/app/+community-page/community-page-routing.module.ts @@ -6,20 +6,23 @@ 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'; @NgModule({ imports: [ RouterModule.forChild([ - { path: 'create', + { + path: 'create', component: CreateCommunityPageComponent, - canActivate: [AuthenticatedGuard] + canActivate: [AuthenticatedGuard, CreateCommunityPageGuard] }, - { path: ':id/edit', + { + path: ':id/edit', pathMatch: 'full', component: EditCommunityPageComponent, canActivate: [AuthenticatedGuard], resolve: { - community: CommunityPageResolver + dso: CommunityPageResolver } }, { @@ -34,6 +37,7 @@ import { EditCommunityPageComponent } from './edit-community-page/edit-community ], providers: [ CommunityPageResolver, + CreateCommunityPageGuard ] }) export class CommunityPageRoutingModule { diff --git a/src/app/+community-page/community-page.component.html b/src/app/+community-page/community-page.component.html index a223ecd49c..b3a2f90bb3 100644 --- a/src/app/+community-page/community-page.component.html +++ b/src/app/+community-page/community-page.component.html @@ -28,7 +28,6 @@
- Edit diff --git a/src/app/+community-page/create-community-page/create-community-page.component.html b/src/app/+community-page/create-community-page/create-community-page.component.html index aeb4713b52..55a080d2a1 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.html +++ b/src/app/+community-page/create-community-page/create-community-page.component.html @@ -1,7 +1,7 @@
- +

{{ 'community.create.sub-head' | translate:{ parent: parent.name } }}

diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index 5a3cf1f4a8..47fb065038 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -3,16 +3,20 @@ 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 '../../comcol-forms/create-comcol-page/create-comcol-page.component'; +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 { - protected frontendURL = 'communities'; + protected frontendURL = '/communities/'; + public constructor( protected communityDataService: CommunityDataService, protected routeService: RouteService, diff --git a/src/app/+community-page/create-community-page/create-community-page.guard.spec.ts b/src/app/+community-page/create-community-page/create-community-page.guard.spec.ts new file mode 100644 index 0000000000..0cc7232871 --- /dev/null +++ b/src/app/+community-page/create-community-page/create-community-page.guard.spec.ts @@ -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) + ); + }); + }); +}); diff --git a/src/app/+community-page/create-community-page/create-community-page.guard.ts b/src/app/+community-page/create-community-page/create-community-page.guard.ts new file mode 100644 index 0000000000..2ee5cb6064 --- /dev/null +++ b/src/app/+community-page/create-community-page/create-community-page.guard.ts @@ -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 { + const parentID = route.queryParams.parent; + if (hasNoValue(parentID)) { + return observableOf(true); + } + + const parent: Observable> = this.communityService.findById(parentID) + .pipe( + getFinishedRemoteData(), + ); + + return parent.pipe( + map((communityRD: RemoteData) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)), + tap((isValid: boolean) => { + if (!isValid) { + this.router.navigate(['/404']); + } + }) + ); + } +} diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.html b/src/app/+community-page/edit-community-page/edit-community-page.component.html index 1dc6442307..98649243cc 100644 --- a/src/app/+community-page/edit-community-page/edit-community-page.component.html +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.html @@ -4,5 +4,5 @@
- +
diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.ts b/src/app/+community-page/edit-community-page/edit-community-page.component.ts index 1528c9e8d5..8db5eab204 100644 --- a/src/app/+community-page/edit-community-page/edit-community-page.component.ts +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.ts @@ -1,45 +1,28 @@ import { Component } from '@angular/core'; import { Community } from '../../core/shared/community.model'; import { CommunityDataService } from '../../core/data/community-data.service'; -import { Observable } from 'rxjs'; import { RouteService } from '../../shared/services/route.service'; import { ActivatedRoute, Router } from '@angular/router'; -import { RemoteData } from '../../core/data/remote-data'; -import { isNotUndefined } from '../../shared/empty.util'; -import { first, map } from 'rxjs/operators'; -import { getSucceededRemoteData } from '../../core/shared/operators'; +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 { - - public parentUUID$: Observable; - public parentRD$: Observable>; - public communityRD$: Observable>; +export class EditCommunityPageComponent extends EditComColPageComponent { + protected frontendURL = '/communities/'; public constructor( - private communityDataService: CommunityDataService, - private routeService: RouteService, - private router: Router, - private route: ActivatedRoute + protected communityDataService: CommunityDataService, + protected routeService: RouteService, + protected router: Router, + protected route: ActivatedRoute ) { - } - - ngOnInit(): void { - this.communityRD$ = this.route.data.pipe(first(), map((data) => data.community)); - } - - onSubmit(community: Community) { - this.communityDataService.update(community) - .pipe(getSucceededRemoteData()) - .subscribe((communityRD: RemoteData) => { - if (isNotUndefined(communityRD)) { - const newUUID = communityRD.payload.uuid; - this.router.navigate(['/communities/' + newUUID]); - } - }); + super(communityDataService, routeService, router, route); } } diff --git a/src/app/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts b/src/app/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts deleted file mode 100644 index 5e11a71c2e..0000000000 --- a/src/app/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts +++ /dev/null @@ -1,45 +0,0 @@ -import {Component, OnInit} from '@angular/core'; -import { Community } from '../../core/shared/community.model'; -import { Observable } from 'rxjs'; -import { RouteService } from '../../shared/services/route.service'; -import { ActivatedRoute, Router } from '@angular/router'; -import { RemoteData } from '../../core/data/remote-data'; -import { isNotUndefined } from '../../shared/empty.util'; -import { first, map } from 'rxjs/operators'; -import { getSucceededRemoteData } from '../../core/shared/operators'; -import { DataService } from '../../core/data/data.service'; -import { NormalizedDSpaceObject } from '../../core/cache/models/normalized-dspace-object.model'; -import { DSpaceObject } from '../../core/shared/dspace-object.model'; - -@Component({ - selector: 'ds-edit-community', - styleUrls: ['./edit-community-page.component.scss'], - templateUrl: './edit-community-page.component.html' -}) -export class EditComColPageComponent implements OnInit { - protected frontendURL: string; - public dsoRD$: Observable>; - - public constructor( - protected dsoDataService: DataService, - private routeService: RouteService, - private router: Router, - private route: ActivatedRoute - ) { - } - - ngOnInit(): void { - this.dsoRD$ = this.route.data.pipe(first(), map((data) => data.dso)); - } - - onSubmit(dso: TDomain) { - this.dsoDataService.update(dso) - .pipe(getSucceededRemoteData()) - .subscribe((dsoRD: RemoteData) => { - if (isNotUndefined(dsoRD)) { - const newUUID = dsoRD.payload.uuid; - this.router.navigate([this.frontendURL + newUUID]); - } - }); - } -} diff --git a/src/app/core/data/config-response-parsing.service.spec.ts b/src/app/core/data/config-response-parsing.service.spec.ts index a33c5cf5b5..a2c5cbbadc 100644 --- a/src/app/core/data/config-response-parsing.service.spec.ts +++ b/src/app/core/data/config-response-parsing.service.spec.ts @@ -161,7 +161,13 @@ describe('ConfigResponseParsingService', () => { page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } }, statusCode: '500' }; - const pageinfo = Object.assign(new PageInfo(), { elementsPerPage: 4, totalElements: 4, totalPages: 1, currentPage: 1 }); + const pageinfo = Object.assign(new PageInfo(), { + elementsPerPage: 4, + totalElements: 4, + totalPages: 1, + currentPage: 1, + self: 'https://rest.api/config/submissiondefinitions/traditional/sections' + }); const definitions = Object.assign(new SubmissionDefinitionsModel(), { isDefault: true, diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index ad6fb5f096..9a690e3c4b 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -5,18 +5,17 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv import { CoreState } from '../core.reducers'; import { Store } from '@ngrx/store'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { Observable } from 'rxjs'; +import { Observable, of as observableOf } from 'rxjs'; import { FindAllOptions } from './request.models'; -import { SortOptions, SortDirection } from '../cache/models/sort-options.model'; -import { of as observableOf } from 'rxjs'; +import { SortDirection, SortOptions } from '../cache/models/sort-options.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { Operation } from '../../../../node_modules/fast-json-patch'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { AuthService } from '../auth/auth.service'; import { UpdateComparator } from './update-comparator'; import { HttpClient } from '@angular/common/http'; import { DataBuildService } from '../cache/builders/data-build.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { compare } from 'fast-json-patch'; const endpoint = 'https://rest.api/core'; @@ -45,6 +44,12 @@ class TestService extends DataService { } } +class DummyComparator implements UpdateComparator { + compare(object1: NormalizedTestObject, object2: NormalizedTestObject): Operation[] { + return compare((object1 as any).metadata, (object2 as any).metadata); + } + +} describe('DataService', () => { let service: TestService; let options: FindAllOptions; @@ -53,8 +58,10 @@ describe('DataService', () => { const rdbService = {} as RemoteDataBuildService; const notificationsService = {} as NotificationsService; const http = {} as HttpClient; - const comparator = {} as any; - const dataBuildService = {} as DataBuildService; + const comparator = new DummyComparator() as any; + const dataBuildService = { + normalize: (object) => object + } as DataBuildService; const objectCache = { addPatch: () => { /* empty */ @@ -136,7 +143,7 @@ describe('DataService', () => { elementsPerPage: 10, sort: sortOptions, startsWith: 'ab' - } + }; const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` + `&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`; @@ -169,7 +176,7 @@ describe('DataService', () => { const name1 = 'random string'; const name2 = 'another random string'; beforeEach(() => { - operations = [{ op: 'replace', path: '/metadata/dc.title', value: name2 } as Operation]; + operations = [{ op: 'replace', path: '/0/value', value: name2 } as Operation]; selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; dso = new DSpaceObject(); @@ -180,17 +187,18 @@ describe('DataService', () => { dso2.self = selfLink; dso2.metadata = [{ key: 'dc.title', value: name2 }]; - spyOn(objectCache, 'getBySelfLink').and.returnValue(dso); + spyOn(service, 'findById').and.returnValues(observableOf(dso)); + spyOn(objectCache, 'getBySelfLink').and.returnValues(observableOf(dso)); spyOn(objectCache, 'addPatch'); }); it('should call addPatch on the object cache with the right parameters when there are differences', () => { - service.update(dso2); + service.update(dso2).subscribe(); expect(objectCache.addPatch).toHaveBeenCalledWith(selfLink, operations); }); it('should not call addPatch on the object cache with the right parameters when there are no differences', () => { - service.update(dso); + service.update(dso).subscribe(); expect(objectCache.addPatch).not.toHaveBeenCalled(); }); }); diff --git a/src/app/core/data/update-comparator.ts b/src/app/core/data/update-comparator.ts index 884ac585f5..f064d7f3f2 100644 --- a/src/app/core/data/update-comparator.ts +++ b/src/app/core/data/update-comparator.ts @@ -3,4 +3,4 @@ import { Operation } from 'fast-json-patch/lib/core'; export interface UpdateComparator { compare(object1: TNormalized, object2: TNormalized): Operation[]; -} \ No newline at end of file +} diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 21ace8a250..ef1d69b8b5 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -35,6 +35,8 @@ import { AuthService } from '../auth/auth.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; import { EmptyError } from 'rxjs/internal-compatibility'; +import { DataBuildService } from '../cache/builders/data-build.service'; +import { DSOUpdateComparator } from '../data/dso-update-comparator'; /* tslint:disable:max-classes-per-file */ @Component({ @@ -117,6 +119,8 @@ describe('MetadataService', () => { { provide: AuthService, useValue: {} }, { provide: NotificationsService, useValue: {} }, { provide: HttpClient, useValue: {} }, + { provide: DataBuildService, useValue: {} }, + { provide: DSOUpdateComparator, useValue: {} }, Meta, Title, ItemDataService, diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index e73613fddb..9dbfae3f90 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -67,7 +67,7 @@ export class MetadataService { public processRemoteData(remoteData: Observable>): void { remoteData.pipe(map((rd: RemoteData) => rd.payload), filter((co: CacheableObject) => hasValue(co)), - take(1),) + take(1)) .subscribe((dspaceObject: DSpaceObject) => { if (!this.initialized) { this.initialize(dspaceObject); @@ -269,7 +269,7 @@ export class MetadataService { const item = this.currentObject.value as Item; item.getFiles() .pipe( - find((files) => isNotEmpty(files)), + first((files) => isNotEmpty(files)), catchError((error) => { console.debug(error.message); return [] @@ -277,7 +277,7 @@ export class MetadataService { .subscribe((bitstreams: Bitstream[]) => { for (const bitstream of bitstreams) { bitstream.format.pipe( - take(1), + first(), catchError((error: Error) => { console.debug(error.message); return [] diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index 388c4289e2..09cb86e010 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -62,6 +62,10 @@ export const getSucceededRemoteData = () => (source: Observable>): Observable> => source.pipe(find((rd: RemoteData) => rd.hasSucceeded)); +export const getFinishedRemoteData = () => + (source: Observable>): Observable> => + source.pipe(find((rd: RemoteData) => !rd.isLoading)); + export const toDSpaceObjectListRD = () => (source: Observable>>>): Observable>> => source.pipe( diff --git a/src/app/comcol-forms/comcol-form/comcol-form.component.html b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.html similarity index 100% rename from src/app/comcol-forms/comcol-form/comcol-form.component.html rename to src/app/shared/comcol-forms/comcol-form/comcol-form.component.html diff --git a/src/app/shared/mocks/mock-response-cache.service.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.scss similarity index 100% rename from src/app/shared/mocks/mock-response-cache.service.ts rename to src/app/shared/comcol-forms/comcol-form/comcol-form.component.scss diff --git a/src/app/+community-page/community-form/community-form.component.spec.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts similarity index 58% rename from src/app/+community-page/community-form/community-form.component.spec.ts rename to src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts index b90a2b0713..a6f5e0d45a 100644 --- a/src/app/+community-page/community-form/community-form.component.spec.ts +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts @@ -1,41 +1,46 @@ -import { CommunityFormComponent } from './community-form.component'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; import { Location } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { - DynamicFormService, - DynamicInputControlModel, - DynamicInputModel -} from '@ng-dynamic-forms/core'; +import { DynamicFormService, DynamicInputModel } from '@ng-dynamic-forms/core'; import { FormControl, FormGroup } from '@angular/forms'; 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 { Community } from '../../../core/shared/community.model'; +import { ResourceType } from '../../../core/shared/resource-type'; +import { ComColFormComponent } from './comcol-form.component'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { hasValue } from '../../empty.util'; +import { Metadatum } from '../../../core/shared/metadatum.model'; -fdescribe('CommunityFormComponent', () => { - let comp: CommunityFormComponent; - let fixture: ComponentFixture; +describe('ComColFormComponent', () => { + let comp: ComColFormComponent; + let fixture: ComponentFixture>; let location: Location; const formServiceStub: any = { - createFormGroup: (formModel: DynamicFormControlModel[]) => { + createFormGroup: (fModel: DynamicFormControlModel[]) => { const controls = {}; - formModel.forEach((controlModel) => { - controls[controlModel.id] = new FormControl((controlModel as any).value); - }); - return new FormGroup(controls); + if (hasValue(fModel)) { + fModel.forEach((controlModel) => { + controls[controlModel.id] = new FormControl((controlModel as any).value); + }); + return new FormGroup(controls); + } + return undefined; } }; - const titleMD = { key: 'dc.title', value: 'Community Title' }; - const randomMD = { key: 'dc.random', value: 'Random metadata excluded from form' }; - const abstractMD = { key: 'dc.description.abstract', value: 'Community description' }; - const newTitleMD = { key: 'dc.title', value: 'New Community Title' }; + const titleMD = { key: 'dc.title', value: 'Community Title' } as Metadatum; + const randomMD = { key: 'dc.random', value: 'Random metadata excluded from form' } as Metadatum; + const abstractMD = { + key: 'dc.description.abstract', + value: 'Community description' + } as Metadatum; + const newTitleMD = { key: 'dc.title', value: 'New Community Title' } as Metadatum; const formModel = [ new DynamicInputModel({ id: 'title', name: newTitleMD.key, - value: newTitleMD.value + value: 'New Community Title' }), new DynamicInputModel({ id: 'abstract', @@ -54,7 +59,7 @@ fdescribe('CommunityFormComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), RouterTestingModule], - declarations: [CommunityFormComponent], + declarations: [ComColFormComponent], providers: [ { provide: Location, useValue: locationStub }, { provide: DynamicFormService, useValue: formServiceStub } @@ -64,20 +69,22 @@ fdescribe('CommunityFormComponent', () => { })); beforeEach(() => { - fixture = TestBed.createComponent(CommunityFormComponent); + fixture = TestBed.createComponent(ComColFormComponent); comp = fixture.componentInstance; + comp.formModel = []; + comp.dso = new Community(); fixture.detectChanges(); location = (comp as any).location; - comp.formModel = formModel; }); describe('onSubmit', () => { beforeEach(() => { spyOn(comp.submitForm, 'emit'); + comp.formModel = formModel; }); - it('should update emit the new version of the community', () => { - comp.community = Object.assign( + it('should emit the new version of the community', () => { + comp.dso = Object.assign( new Community(), { metadata: [ diff --git a/src/app/comcol-forms/comcol-form/comcol-form.component.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts similarity index 65% rename from src/app/comcol-forms/comcol-form/comcol-form.component.ts rename to src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts index d49e197f5d..19050e2bc2 100644 --- a/src/app/comcol-forms/comcol-form/comcol-form.component.ts +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts @@ -7,23 +7,50 @@ import { import { FormGroup } from '@angular/forms'; import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model'; import { TranslateService } from '@ngx-translate/core'; -import { DSpaceObject } from '../../core/shared/dspace-object.model'; -import { isNotEmpty } from '../../shared/empty.util'; -import { ResourceType } from '../../core/shared/resource-type'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { isNotEmpty } from '../../empty.util'; +import { ResourceType } from '../../../core/shared/resource-type'; @Component({ selector: 'ds-comcol-form', - // styleUrls: ['./comcol-form.component.scss'], + styleUrls: ['./comcol-form.component.scss'], templateUrl: './comcol-form.component.html' }) export class ComColFormComponent implements OnInit { + /** + * DSpaceObject that the form represents + */ @Input() dso: T; - type; - LABEL_KEY_PREFIX = this.type + '.form.'; - ERROR_KEY_PREFIX = this.type + '.form.errors.'; + + /** + * Type of DSpaceObject that the form represents + */ + protected type; + + /** + * @type {string} Key prefix used to generate form labels + */ + LABEL_KEY_PREFIX = '.form.'; + + /** + * @type {string} Key prefix used to generate form error messages + */ + ERROR_KEY_PREFIX = '.form.errors.'; + + /** + * The form model that represents the fields in the form + */ formModel: DynamicFormControlModel[]; + + /** + * The form group of this form + */ formGroup: FormGroup; + /** + * Emits DSO when the form is submitted + * @type {EventEmitter} + */ @Output() submitForm: EventEmitter = new EventEmitter(); public constructor(private location: Location, @@ -38,6 +65,7 @@ export class ComColFormComponent implements OnInit { } ); this.formGroup = this.formService.createFormGroup(this.formModel); + this.updateFieldTranslations(); this.translate.onLangChange .subscribe(() => { @@ -45,6 +73,9 @@ export class ComColFormComponent implements OnInit { }); } + /** + * Checks which new fields where added and sends the updated version of the DSO to the parent component + */ onSubmit() { const metadata = this.formModel.map( (fieldModel: DynamicInputModel) => { @@ -53,6 +84,7 @@ export class ComColFormComponent implements OnInit { ); const filteredOldMetadata = this.dso.metadata.filter((filter) => !metadata.map((md) => md.key).includes(filter.key)); const filteredNewMetadata = metadata.filter((md) => isNotEmpty(md.value)); + const newMetadata = [...filteredOldMetadata, ...filteredNewMetadata]; const updatedDSO = Object.assign({}, this.dso, { metadata: newMetadata, @@ -61,14 +93,17 @@ export class ComColFormComponent implements OnInit { this.submitForm.emit(updatedDSO); } + /** + * Used the update translations of errors and labels on init and on language change + */ private updateFieldTranslations() { this.formModel.forEach( (fieldModel: DynamicInputModel) => { - fieldModel.label = this.translate.instant(this.LABEL_KEY_PREFIX + fieldModel.id); + fieldModel.label = this.translate.instant(this.type + this.LABEL_KEY_PREFIX + fieldModel.id); if (isNotEmpty(fieldModel.validators)) { fieldModel.errorMessages = {}; Object.keys(fieldModel.validators).forEach((key) => { - fieldModel.errorMessages[key] = this.translate.instant(this.ERROR_KEY_PREFIX + fieldModel.id + '.' + key); + fieldModel.errorMessages[key] = this.translate.instant(this.type + this.ERROR_KEY_PREFIX + fieldModel.id + '.' + key); }); } } diff --git a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts similarity index 70% rename from src/app/+community-page/create-community-page/create-community-page.component.spec.ts rename to src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts index 83db9561cc..fd3464ba5e 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts @@ -1,21 +1,25 @@ -import { CreateCommunityPageComponent } from './create-community-page.component'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { CommunityDataService } from '../../core/data/community-data.service'; -import { RouteService } from '../../shared/services/route.service'; +import { CommunityDataService } from '../../../core/data/community-data.service'; +import { RouteService } from '../../services/route.service'; import { Router } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { RemoteData } from '../../core/data/remote-data'; -import { Community } from '../../core/shared/community.model'; -import { SharedModule } from '../../shared/shared.module'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Community } from '../../../core/shared/community.model'; +import { SharedModule } from '../../shared.module'; import { CommonModule } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { NormalizedDSpaceObject } from '../../../core/cache/models/normalized-dspace-object.model'; +import { CreateComColPageComponent } from './create-comcol-page.component'; +import { DataService } from '../../../core/data/data.service'; -fdescribe('CreateCommunityPageComponent', () => { - let comp: CreateCommunityPageComponent; - let fixture: ComponentFixture; +describe('CreateComColPageComponent', () => { + let comp: CreateComColPageComponent; + let fixture: ComponentFixture>; let communityDataService: CommunityDataService; + let dsoDataService: CommunityDataService; let routeService: RouteService; let router: Router; @@ -67,8 +71,8 @@ fdescribe('CreateCommunityPageComponent', () => { initializeVars(); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [CreateCommunityPageComponent], providers: [ + { provide: DataService, useValue: communityDataServiceStub }, { provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: RouteService, useValue: routeServiceStub }, { provide: Router, useValue: routerStub }, @@ -78,9 +82,10 @@ fdescribe('CreateCommunityPageComponent', () => { })); beforeEach(() => { - fixture = TestBed.createComponent(CreateCommunityPageComponent); + fixture = TestBed.createComponent(CreateComColPageComponent); comp = fixture.componentInstance; fixture.detectChanges(); + dsoDataService = (comp as any).dsoDataService; communityDataService = (comp as any).communityDataService; routeService = (comp as any).routeService; router = (comp as any).router; @@ -105,7 +110,7 @@ fdescribe('CreateCommunityPageComponent', () => { it('should not navigate on failure', () => { spyOn(router, 'navigate'); - spyOn(communityDataService, 'create').and.returnValue(observableOf(new RemoteData(true, true, false, undefined, newCommunity))); + spyOn(dsoDataService, 'create').and.returnValue(observableOf(new RemoteData(true, true, false, undefined, newCommunity))); comp.onSubmit(data); fixture.detectChanges(); expect(router.navigate).not.toHaveBeenCalled(); diff --git a/src/app/comcol-forms/create-comcol-page/create-comcol-page.component.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts similarity index 54% rename from src/app/comcol-forms/create-comcol-page/create-comcol-page.component.ts rename to src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts index 18f23cf528..2bcd9f9bd0 100644 --- a/src/app/comcol-forms/create-comcol-page/create-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts @@ -1,25 +1,38 @@ import { Component, OnInit } from '@angular/core'; -import { Community } from '../../core/shared/community.model'; -import { CommunityDataService } from '../../core/data/community-data.service'; +import { Community } from '../../../core/shared/community.model'; +import { CommunityDataService } from '../../../core/data/community-data.service'; import { Observable } from 'rxjs'; -import { RouteService } from '../../shared/services/route.service'; +import { RouteService } from '../../services/route.service'; import { Router } from '@angular/router'; -import { RemoteData } from '../../core/data/remote-data'; -import { isNotEmpty, isNotUndefined } from '../../shared/empty.util'; +import { RemoteData } from '../../../core/data/remote-data'; +import { isNotEmpty, isNotUndefined } from '../../empty.util'; import { take } from 'rxjs/operators'; -import { getSucceededRemoteData } from '../../core/shared/operators'; -import { DSpaceObject } from '../../core/shared/dspace-object.model'; -import { DataService } from '../../core/data/data.service'; -import { NormalizedDSpaceObject } from '../../core/cache/models/normalized-dspace-object.model'; +import { getSucceededRemoteData } from '../../../core/shared/operators'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { DataService } from '../../../core/data/data.service'; +import { NormalizedDSpaceObject } from '../../../core/cache/models/normalized-dspace-object.model'; +/** + * Component representing the create page for communities and collections + */ @Component({ - selector: 'ds-create-community', - styleUrls: ['./create-community-page.component.scss'], - templateUrl: './create-community-page.component.html' + selector: 'ds-create-comcol', + template: '' }) export class CreateComColPageComponent implements OnInit { + /** + * Frontend endpoint where for this type of DSP + */ protected frontendURL: string; + + /** + * The provided UUID for the parent community + */ public parentUUID$: Observable; + + /** + * The parent community of the object that is to be created + */ public parentRD$: Observable>; public constructor( @@ -40,6 +53,10 @@ export class CreateComColPageComponent { this.dsoDataService.create(dso, uuid) @@ -49,7 +66,7 @@ export class CreateComColPageComponent { - let comp: EditCommunityPageComponent; - let fixture: ComponentFixture; +describe('EditComColPageComponent', () => { + let comp: EditComColPageComponent; + let fixture: ComponentFixture>; let communityDataService: CommunityDataService; + let dsoDataService: CommunityDataService; let routeService: RouteService; let router: Router; @@ -73,9 +76,8 @@ fdescribe('EditCommunityPageComponent', () => { initializeVars(); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [EditCommunityPageComponent], providers: [ - { provide: CommunityDataService, useValue: communityDataServiceStub }, + { provide: DataService, useValue: communityDataServiceStub }, { provide: RouteService, useValue: routeServiceStub }, { provide: Router, useValue: routerStub }, { provide: ActivatedRoute, useValue: routeStub }, @@ -85,9 +87,10 @@ fdescribe('EditCommunityPageComponent', () => { })); beforeEach(() => { - fixture = TestBed.createComponent(EditCommunityPageComponent); + fixture = TestBed.createComponent(EditComColPageComponent); comp = fixture.componentInstance; fixture.detectChanges(); + dsoDataService = (comp as any).dsoDataService; communityDataService = (comp as any).communityDataService; routeService = (comp as any).routeService; router = (comp as any).router; @@ -112,7 +115,7 @@ fdescribe('EditCommunityPageComponent', () => { it('should not navigate on failure', () => { spyOn(router, 'navigate'); - spyOn(communityDataService, 'update').and.returnValue(observableOf(new RemoteData(true, true, false, undefined, newCommunity))); + spyOn(dsoDataService, 'update').and.returnValue(observableOf(new RemoteData(true, true, false, undefined, newCommunity))); comp.onSubmit(data); fixture.detectChanges(); expect(router.navigate).not.toHaveBeenCalled(); diff --git a/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts new file mode 100644 index 0000000000..8f03280fc0 --- /dev/null +++ b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts @@ -0,0 +1,56 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { RouteService } from '../../services/route.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { RemoteData } from '../../../core/data/remote-data'; +import { isNotUndefined } from '../../empty.util'; +import { first, map } from 'rxjs/operators'; +import { getSucceededRemoteData } from '../../../core/shared/operators'; +import { DataService } from '../../../core/data/data.service'; +import { NormalizedDSpaceObject } from '../../../core/cache/models/normalized-dspace-object.model'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; + +/** + * Component representing the edit page for communities and collections + */ +@Component({ + selector: 'ds-edit-comcol', + template: '' +}) +export class EditComColPageComponent implements OnInit { + /** + * Frontend endpoint where for this type of DSP + */ + protected frontendURL: string; + /** + * The initial DSO object + */ + public dsoRD$: Observable>; + + public constructor( + protected dsoDataService: DataService, + protected routeService: RouteService, + protected router: Router, + protected route: ActivatedRoute + ) { + } + + ngOnInit(): void { + this.dsoRD$ = this.route.data.pipe(first(), map((data) => data.dso)); + } + + /** + * @param {TDomain} dso The updated version of the DSO + * Updates an existing DSO based on the submitted user data and navigates to the editted object's home page + */ + onSubmit(dso: TDomain) { + this.dsoDataService.update(dso) + .pipe(getSucceededRemoteData()) + .subscribe((dsoRD: RemoteData) => { + if (isNotUndefined(dsoRD)) { + const newUUID = dsoRD.payload.uuid; + this.router.navigate([this.frontendURL + newUUID]); + } + }); + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index b7d42e4856..a261590b47 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -86,6 +86,9 @@ import { CapitalizePipe } from './utils/capitalize.pipe'; import { ObjectKeysPipe } from './utils/object-keys-pipe'; import { MomentModule } from 'ngx-moment'; import { MenuModule } from './menu/menu.module'; +import { ComColFormComponent } from './comcol-forms/comcol-form/comcol-form.component'; +import { CreateComColPageComponent } from './comcol-forms/create-comcol-page/create-comcol-page.component'; +import { EditComColPageComponent } from './comcol-forms/edit-comcol-page/edit-comcol-page.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -129,6 +132,9 @@ const COMPONENTS = [ ComcolPageContentComponent, ComcolPageHeaderComponent, ComcolPageLogoComponent, + ComColFormComponent, + CreateComColPageComponent, + EditComColPageComponent, DsDynamicFormComponent, DsDynamicFormControlComponent, DsDynamicListComponent, diff --git a/src/app/shared/testing/utils.ts b/src/app/shared/testing/utils.ts index 8714358100..cd17a1b1f5 100644 --- a/src/app/shared/testing/utils.ts +++ b/src/app/shared/testing/utils.ts @@ -41,4 +41,4 @@ export function spyOnOperator(obj: any, prop: string): any { }); return spyOn(obj, prop); -} \ No newline at end of file +} From 773973cdef1b6b3991de51a8d8dd1b8bc1371d34 Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 4 Jan 2019 14:16:35 +0100 Subject: [PATCH 61/69] fixed missing take 1 --- .../create-comcol-page/create-comcol-page.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts index 2bcd9f9bd0..f8bed5b814 100644 --- a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts @@ -46,7 +46,7 @@ export class CreateComColPageComponent { + this.parentUUID$.pipe(take(1)).subscribe((parentID: string) => { if (isNotEmpty(parentID)) { this.parentRD$ = this.parentDataService.findById(parentID); } From 303fc08520276c42426d33e63b92dfb482844346 Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 4 Jan 2019 16:55:16 +0100 Subject: [PATCH 62/69] 54472: fixed small test issue --- .../integration/integration-response-parsing.service.spec.ts | 2 +- .../comcol-forms/edit-comcol-page/edit-comcol-page.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/core/integration/integration-response-parsing.service.spec.ts b/src/app/core/integration/integration-response-parsing.service.spec.ts index 38741da4e2..03f350b868 100644 --- a/src/app/core/integration/integration-response-parsing.service.spec.ts +++ b/src/app/core/integration/integration-response-parsing.service.spec.ts @@ -143,7 +143,7 @@ describe('IntegrationResponseParsingService', () => { }, statusCode: '200' }; - const pageinfo = Object.assign(new PageInfo(), { elementsPerPage: 5, totalElements: 5, totalPages: 1, currentPage: 1 }); + const pageinfo = Object.assign(new PageInfo(), { elementsPerPage: 5, totalElements: 5, totalPages: 1, currentPage: 1, self: 'https://rest.api/integration/authorities/type/entries'}); const definitions = new PaginatedList(pageinfo,[ Object.assign({}, new AuthorityValueModel(), { type: 'authority', diff --git a/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts index 8f03280fc0..5ccbcfcc66 100644 --- a/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts @@ -41,7 +41,7 @@ export class EditComColPageComponent Date: Mon, 7 Jan 2019 09:17:22 +0100 Subject: [PATCH 63/69] 54472: fixed small test issue --- ...tegration-response-parsing.service.spec.ts | 102 ++++++++++-------- 1 file changed, 56 insertions(+), 46 deletions(-) diff --git a/src/app/core/integration/integration-response-parsing.service.spec.ts b/src/app/core/integration/integration-response-parsing.service.spec.ts index 03f350b868..baa4343724 100644 --- a/src/app/core/integration/integration-response-parsing.service.spec.ts +++ b/src/app/core/integration/integration-response-parsing.service.spec.ts @@ -23,15 +23,57 @@ describe('IntegrationResponseParsingService', () => { const uuid = 'd9d30c0c-69b7-4369-8397-ca67c888974d'; const integrationEndpoint = 'https://rest.api/integration/authorities'; const entriesEndpoint = `${integrationEndpoint}/${name}/entries?query=${query}&metadata=${metadata}&uuid=${uuid}`; + let validRequest; - beforeEach(() => { - service = new IntegrationResponseParsingService(EnvConfig, objectCacheService); - }); + let validResponse; - describe('parse', () => { - const validRequest = new IntegrationRequest('69f375b5-19f4-4453-8c7a-7dc5c55aafbb', entriesEndpoint); + let invalidResponse1; + let invalidResponse2; + let pageInfo; + let definitions; - const validResponse = { + function initVars() { + pageInfo = Object.assign(new PageInfo(), { elementsPerPage: 5, totalElements: 5, totalPages: 1, currentPage: 1, self: 'https://rest.api/integration/authorities/type/entries'}); + definitions = new PaginatedList(pageInfo,[ + Object.assign({}, new AuthorityValueModel(), { + type: 'authority', + display: 'One', + id: 'One', + otherInformation: undefined, + value: 'One' + }), + Object.assign({}, new AuthorityValueModel(), { + type: 'authority', + display: 'Two', + id: 'Two', + otherInformation: undefined, + value: 'Two' + }), + Object.assign({}, new AuthorityValueModel(), { + type: 'authority', + display: 'Three', + id: 'Three', + otherInformation: undefined, + value: 'Three' + }), + Object.assign({}, new AuthorityValueModel(), { + type: 'authority', + display: 'Four', + id: 'Four', + otherInformation: undefined, + value: 'Four' + }), + Object.assign({}, new AuthorityValueModel(), { + type: 'authority', + display: 'Five', + id: 'Five', + otherInformation: undefined, + value: 'Five' + }) + ]); + validRequest = new IntegrationRequest('69f375b5-19f4-4453-8c7a-7dc5c55aafbb', entriesEndpoint); + + validResponse = { payload: { page: { number: 0, @@ -86,12 +128,12 @@ describe('IntegrationResponseParsingService', () => { statusCode: '200' }; - const invalidResponse1 = { + invalidResponse1 = { payload: {}, statusCode: '200' }; - const invalidResponse2 = { + invalidResponse2 = { payload: { page: { number: 0, @@ -143,45 +185,13 @@ describe('IntegrationResponseParsingService', () => { }, statusCode: '200' }; - const pageinfo = Object.assign(new PageInfo(), { elementsPerPage: 5, totalElements: 5, totalPages: 1, currentPage: 1, self: 'https://rest.api/integration/authorities/type/entries'}); - const definitions = new PaginatedList(pageinfo,[ - Object.assign({}, new AuthorityValueModel(), { - type: 'authority', - display: 'One', - id: 'One', - otherInformation: undefined, - value: 'One' - }), - Object.assign({}, new AuthorityValueModel(), { - type: 'authority', - display: 'Two', - id: 'Two', - otherInformation: undefined, - value: 'Two' - }), - Object.assign({}, new AuthorityValueModel(), { - type: 'authority', - display: 'Three', - id: 'Three', - otherInformation: undefined, - value: 'Three' - }), - Object.assign({}, new AuthorityValueModel(), { - type: 'authority', - display: 'Four', - id: 'Four', - otherInformation: undefined, - value: 'Four' - }), - Object.assign({}, new AuthorityValueModel(), { - type: 'authority', - display: 'Five', - id: 'Five', - otherInformation: undefined, - value: 'Five' - }) - ]); + } + beforeEach(() => { + initVars(); + service = new IntegrationResponseParsingService(EnvConfig, objectCacheService); + }); + describe('parse', () => { it('should return a IntegrationSuccessResponse if data contains a valid endpoint response', () => { const response = service.parse(validRequest, validResponse); expect(response.constructor).toBe(IntegrationSuccessResponse); From b0da712915b15ef3b9643012eefceafa905f97af Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 10 Jan 2019 13:01:57 +0100 Subject: [PATCH 64/69] added trivial tests --- .../create-collection-page.component.spec.ts | 46 +++++++++++++++++++ .../edit-collection-page.component.spec.ts | 42 +++++++++++++++++ .../create-community-page.component.spec.ts | 42 +++++++++++++++++ .../edit-community-page.component.spec.ts | 42 +++++++++++++++++ src/app/core/cache/object-cache.reducer.ts | 2 +- 5 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts create mode 100644 src/app/+collection-page/edit-collection-page/edit-collection-page.component.spec.ts create mode 100644 src/app/+community-page/create-community-page/create-community-page.component.spec.ts create mode 100644 src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts new file mode 100644 index 0000000000..29350a83e0 --- /dev/null +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts @@ -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; + + 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/'); + }) + }); +}); diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.spec.ts b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.spec.ts new file mode 100644 index 0000000000..6b01f13574 --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.spec.ts @@ -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 { EditCollectionPageComponent } from './edit-collection-page.component'; +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'; + +describe('EditCollectionPageComponent', () => { + let comp: EditCollectionPageComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + declarations: [EditCollectionPageComponent], + providers: [ + { provide: CollectionDataService, useValue: {} }, + { provide: RouteService, useValue: {} }, + { provide: Router, useValue: {} }, + { provide: ActivatedRoute, useValue: { data: observableOf({dso: undefined}) } }, + ], + 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/'); + }) + }); +}); diff --git a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts new file mode 100644 index 0000000000..dba15dbe88 --- /dev/null +++ b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts @@ -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; + + 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/'); + }) + }); +}); diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts b/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts new file mode 100644 index 0000000000..764cffe8b5 --- /dev/null +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts @@ -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 { EditCommunityPageComponent } from './edit-community-page.component'; +import { CommunityDataService } from '../../core/data/community-data.service'; + +describe('EditCommunityPageComponent', () => { + let comp: EditCommunityPageComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + declarations: [EditCommunityPageComponent], + providers: [ + { provide: CommunityDataService, useValue: {} }, + { provide: RouteService, useValue: {} }, + { provide: Router, useValue: {} }, + { provide: ActivatedRoute, useValue: { data: observableOf({dso: undefined}) } }, + ], + 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/'); + }) + }); +}); diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index 867f31e1bb..991ae5466e 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -22,7 +22,7 @@ export interface Patch { operations: Operation[]; } -/**conca +/** * An interface to represent objects that can be cached * * A cacheable object should have a self link From f51ac8086d5eb68f66bcf9bb07659976b681c134 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 10 Jan 2019 13:16:27 +0100 Subject: [PATCH 65/69] remove exportToZip config property that was added accidentally --- config/environment.default.js | 1 - src/config/cache-config.interface.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/config/environment.default.js b/config/environment.default.js index 527e12936e..3c1144fc6f 100644 --- a/config/environment.default.js +++ b/config/environment.default.js @@ -20,7 +20,6 @@ module.exports = { // NOTE: how long should objects be cached for by default msToLive: { default: 15 * 60 * 1000, // 15 minutes - exportToZip: 5 * 1000 // 5 seconds }, // msToLive: 1000, // 15 minutes control: 'max-age=60', // revalidate browser diff --git a/src/config/cache-config.interface.ts b/src/config/cache-config.interface.ts index a52eca60e2..ef2d19e76e 100644 --- a/src/config/cache-config.interface.ts +++ b/src/config/cache-config.interface.ts @@ -4,7 +4,6 @@ import { AutoSyncConfig } from './auto-sync-config.interface'; export interface CacheConfig extends Config { msToLive: { default: number; - exportToZip: number; }, control: string, autoSync: AutoSyncConfig From e387f9446f0a7ff8b399729ca2f3bad708984996 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 17 Jan 2019 13:53:34 +0100 Subject: [PATCH 66/69] remove unused 'first' imports --- src/app/+community-page/community-page.component.ts | 2 +- .../search-facet-filter/search-facet-filter.component.ts | 2 +- .../search-filters/search-filter/search-filter.component.ts | 2 +- src/app/app.component.ts | 2 +- src/app/core/auth/auth.effects.ts | 2 +- src/app/core/auth/server-auth.service.ts | 2 +- src/app/core/cache/object-cache.service.ts | 2 +- src/app/core/cache/server-sync-buffer.effects.ts | 2 +- src/app/core/shared/operators.ts | 2 +- src/app/shared/host-window.service.ts | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app/+community-page/community-page.component.ts b/src/app/+community-page/community-page.component.ts index bfaac33c1c..0483143230 100644 --- a/src/app/+community-page/community-page.component.ts +++ b/src/app/+community-page/community-page.component.ts @@ -1,4 +1,4 @@ -import { mergeMap, filter, map, first, tap } from 'rxjs/operators'; +import { mergeMap, filter, map } from 'rxjs/operators'; import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts index faaf3b9fb5..fd5a75e7d1 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts @@ -6,7 +6,7 @@ import { Subject, Subscription } from 'rxjs'; -import { switchMap, distinctUntilChanged, first, map, take } from 'rxjs/operators'; +import { switchMap, distinctUntilChanged, map, take } from 'rxjs/operators'; import { animate, state, style, transition, trigger } from '@angular/animations'; import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { Router } from '@angular/router'; diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts index ec239e3628..dcc01f2b46 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts @@ -1,5 +1,5 @@ -import { first, take } from 'rxjs/operators'; +import { take } from 'rxjs/operators'; import { Component, Input, OnInit } from '@angular/core'; import { SearchFilterConfig } from '../../search-service/search-filter-config.model'; import { SearchFilterService } from './search-filter.service'; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 98e0d614ae..30a8f01251 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { filter, first, map, take } from 'rxjs/operators'; +import { filter, map, take } from 'rxjs/operators'; import { AfterViewInit, ChangeDetectionStrategy, diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index 56a5411ef2..1e68802af8 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -1,6 +1,6 @@ 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 @ngrx diff --git a/src/app/core/auth/server-auth.service.ts b/src/app/core/auth/server-auth.service.ts index e11800233e..868d444c26 100644 --- a/src/app/core/auth/server-auth.service.ts +++ b/src/app/core/auth/server-auth.service.ts @@ -1,4 +1,4 @@ -import { first, map, switchMap, take } from 'rxjs/operators'; +import { map, switchMap, take } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 40f41be14d..af30646f53 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -1,6 +1,6 @@ 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 { MemoizedSelector, select, Store } from '@ngrx/store'; import { IndexName } from '../index/index.reducer'; diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts index d0a194705b..0d7392e555 100644 --- a/src/app/core/cache/server-sync-buffer.effects.ts +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -1,4 +1,4 @@ -import { delay, exhaustMap, first, map, switchMap, take, tap } from 'rxjs/operators'; +import { delay, exhaustMap, map, switchMap, take } from 'rxjs/operators'; import { Inject, Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index 09cb86e010..64f4ffa570 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -1,5 +1,5 @@ import { Observable } from 'rxjs'; -import { filter, find, first, flatMap, map, tap } from 'rxjs/operators'; +import { filter, find, flatMap, map, tap } from 'rxjs/operators'; import { hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util'; import { DSOSuccessResponse, RestResponse } from '../cache/response.models'; import { RemoteData } from '../data/remote-data'; diff --git a/src/app/shared/host-window.service.ts b/src/app/shared/host-window.service.ts index 840b134996..146c616e38 100644 --- a/src/app/shared/host-window.service.ts +++ b/src/app/shared/host-window.service.ts @@ -1,6 +1,6 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { filter, distinctUntilChanged, map, first } from 'rxjs/operators'; +import { filter, distinctUntilChanged, map } from 'rxjs/operators'; import { HostWindowState } from './host-window.reducer'; import { Injectable } from '@angular/core'; import { createSelector, select, Store } from '@ngrx/store'; From 586a4ac35b002778e3d25753f9960b4fa1284c52 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 17 Jan 2019 14:59:48 +0100 Subject: [PATCH 67/69] add docs, rename classes for clarity --- ....ts => normalized-object-build.service.ts} | 32 +++++++++++++------ src/app/core/cache/object-cache.reducer.ts | 10 ++++++ src/app/core/core.module.ts | 8 ++--- .../data/base-response-parsing.service.ts | 2 +- src/app/core/data/change-analyzer.ts | 20 ++++++++++++ src/app/core/data/collection-data.service.ts | 8 ++--- src/app/core/data/comcol-data.service.spec.ts | 10 +++--- src/app/core/data/community-data.service.ts | 8 ++--- src/app/core/data/data.service.spec.ts | 16 +++++----- src/app/core/data/data.service.ts | 19 ++++++++--- .../core/data/dso-change-analyzer.service.ts | 26 +++++++++++++++ src/app/core/data/dso-update-comparator.ts | 12 ------- .../data/dspace-object-data.service.spec.ts | 4 +-- .../core/data/dspace-object-data.service.ts | 12 +++---- src/app/core/data/item-data.service.spec.ts | 4 +-- src/app/core/data/item-data.service.ts | 8 ++--- src/app/core/data/request.reducer.ts | 12 ++++++- src/app/core/data/update-comparator.ts | 6 ---- .../dspace-rest-v2/dspace-rest-v2.service.ts | 8 +++++ .../core/metadata/metadata.service.spec.ts | 8 ++--- .../comcol-form/comcol-form.component.ts | 3 ++ src/app/shared/testing/utils.ts | 8 +++++ src/config/auto-sync-config.interface.ts | 19 +++++++++++ 23 files changed, 186 insertions(+), 77 deletions(-) rename src/app/core/cache/builders/{data-build.service.ts => normalized-object-build.service.ts} (63%) create mode 100644 src/app/core/data/change-analyzer.ts create mode 100644 src/app/core/data/dso-change-analyzer.service.ts delete mode 100644 src/app/core/data/dso-update-comparator.ts delete mode 100644 src/app/core/data/update-comparator.ts diff --git a/src/app/core/cache/builders/data-build.service.ts b/src/app/core/cache/builders/normalized-object-build.service.ts similarity index 63% rename from src/app/core/cache/builders/data-build.service.ts rename to src/app/core/cache/builders/normalized-object-build.service.ts index 8ba3ebee0c..9d97ccda75 100644 --- a/src/app/core/cache/builders/data-build.service.ts +++ b/src/app/core/cache/builders/normalized-object-build.service.ts @@ -3,24 +3,38 @@ 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 { map, take } from 'rxjs/operators'; import { hasValue, isNotEmpty } from '../../../shared/empty.util'; -import { PaginatedList } from '../../data/paginated-list'; -export function isRestDataObject(halObj: any) { +/** + * 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); } -export function isRestPaginatedList(halObj: any) { +/** + * 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); } -export function isPaginatedList(halObj: any) { - return hasValue(halObj.page) && hasValue(halObj.pageInfo); -} - +/** + * A service to turn domain models in to their normalized + * counterparts. + */ @Injectable() -export class DataBuildService { +export class NormalizedObjectBuildService { + + /** + * Returns the normalized model that corresponds to the given domain model + * + * @param {TDomain} domainModel a domain model + */ normalize(domainModel: TDomain): TNormalized { const normalizedConstructor = NormalizedObjectFactory.getConstructor(domainModel.type); const relationships = getRelationships(normalizedConstructor) || []; diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index 991ae5466e..982c77341e 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -17,8 +17,18 @@ export enum DirtyType { 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[]; } diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index b1904782bd..0868773550 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -65,8 +65,8 @@ import { BrowseItemsResponseParsingService } from './data/browse-items-response- import { DSpaceObjectDataService } from './data/dspace-object-data.service'; import { CSSVariableService } from '../shared/sass-helper/sass-helper.service'; import { MenuService } from '../shared/menu/menu.service'; -import { DataBuildService } from './cache/builders/data-build.service'; -import { DSOUpdateComparator } from './data/dso-update-comparator'; +import { NormalizedObjectBuildService } from './cache/builders/normalized-object-build.service'; +import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service'; const IMPORTS = [ CommonModule, @@ -103,7 +103,7 @@ const PROVIDERS = [ ObjectCacheService, PaginationComponentOptions, RegistryService, - DataBuildService, + NormalizedObjectBuildService, RemoteDataBuildService, RequestService, EndpointMapResponseParsingService, @@ -131,7 +131,7 @@ const PROVIDERS = [ UploaderService, UUIDService, DSpaceObjectDataService, - DSOUpdateComparator, + DSOChangeAnalyzer, CSSVariableService, MenuService, // register AuthInterceptor as HttpInterceptor diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index d5c1c58296..925caa495c 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -8,7 +8,7 @@ import { GenericConstructor } from '../shared/generic-constructor'; import { PaginatedList } from './paginated-list'; import { ResourceType } from '../shared/resource-type'; import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; -import { isRestDataObject, isRestPaginatedList } from '../cache/builders/data-build.service'; +import { isRestDataObject, isRestPaginatedList } from '../cache/builders/normalized-object-build.service'; /* tslint:disable:max-classes-per-file */ export abstract class BaseResponseParsingService { diff --git a/src/app/core/data/change-analyzer.ts b/src/app/core/data/change-analyzer.ts new file mode 100644 index 0000000000..caf9e38c7c --- /dev/null +++ b/src/app/core/data/change-analyzer.ts @@ -0,0 +1,20 @@ +import { NormalizedObject } from '../cache/models/normalized-object.model'; +import { Operation } from 'fast-json-patch/lib/core'; + +/** + * An interface to determine what differs between two + * NormalizedObjects + */ +export interface ChangeAnalyzer { + + /** + * Compare two objects and return their differences as a + * JsonPatch Operation Array + * + * @param {NormalizedObject} object1 + * The first object to compare + * @param {NormalizedObject} object2 + * The second object to compare + */ + diff(object1: TNormalized, object2: TNormalized): Operation[]; +} diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index b08b1005b7..e8a682ba0e 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -11,8 +11,8 @@ import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; -import { DataBuildService } from '../cache/builders/data-build.service'; -import { DSOUpdateComparator } from './dso-update-comparator'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; @Injectable() export class CollectionDataService extends ComColDataService { @@ -21,14 +21,14 @@ export class CollectionDataService extends ComColDataService, protected cds: CommunityDataService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, - protected comparator: DSOUpdateComparator + protected comparator: DSOChangeAnalyzer ) { super(); } diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index ca6c8dd855..1b520d83ce 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -16,8 +16,8 @@ import { RequestEntry } from './request.reducer'; import { of as observableOf } from 'rxjs'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; -import { DataBuildService } from '../cache/builders/data-build.service'; -import { DSOUpdateComparator } from './dso-update-comparator'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; const LINK_NAME = 'test'; @@ -30,7 +30,7 @@ class TestService extends ComColDataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: DataBuildService, + protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected EnvConfig: GlobalConfig, protected cds: CommunityDataService, @@ -38,7 +38,7 @@ class TestService extends ComColDataService { protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, - protected comparator: DSOUpdateComparator, + protected comparator: DSOChangeAnalyzer, protected linkPath: string ) { super(); @@ -61,7 +61,7 @@ describe('ComColDataService', () => { const notificationsService = {} as NotificationsService; const http = {} as HttpClient; const comparator = {} as any; - const dataBuildService = {} as DataBuildService; + const dataBuildService = {} as NormalizedObjectBuildService; const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d'; const options = Object.assign(new FindAllOptions(), { diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index 63fbe3a21a..d09a0b9757 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -17,8 +17,8 @@ import { Observable } from 'rxjs'; import { PaginatedList } from './paginated-list'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; -import { DataBuildService } from '../cache/builders/data-build.service'; -import { DSOUpdateComparator } from './dso-update-comparator'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; @Injectable() export class CommunityDataService extends ComColDataService { @@ -29,13 +29,13 @@ export class CommunityDataService extends ComColDataService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, - protected comparator: DSOUpdateComparator + protected comparator: DSOChangeAnalyzer ) { super(); } diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index 9a690e3c4b..9b39f0b68e 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -11,9 +11,9 @@ import { SortDirection, SortOptions } from '../cache/models/sort-options.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { Operation } from '../../../../node_modules/fast-json-patch'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { UpdateComparator } from './update-comparator'; +import { ChangeAnalyzer } from './change-analyzer'; import { HttpClient } from '@angular/common/http'; -import { DataBuildService } from '../cache/builders/data-build.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { compare } from 'fast-json-patch'; @@ -27,14 +27,14 @@ class TestService extends DataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: DataBuildService, + protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected linkPath: string, protected halService: HALEndpointService, protected objectCache: ObjectCacheService, protected notificationsService: NotificationsService, protected http: HttpClient, - protected comparator: UpdateComparator + protected comparator: ChangeAnalyzer ) { super(); } @@ -44,8 +44,8 @@ class TestService extends DataService { } } -class DummyComparator implements UpdateComparator { - compare(object1: NormalizedTestObject, object2: NormalizedTestObject): Operation[] { +class DummyChangeAnalyzer implements ChangeAnalyzer { + diff(object1: NormalizedTestObject, object2: NormalizedTestObject): Operation[] { return compare((object1 as any).metadata, (object2 as any).metadata); } @@ -58,10 +58,10 @@ describe('DataService', () => { const rdbService = {} as RemoteDataBuildService; const notificationsService = {} as NotificationsService; const http = {} as HttpClient; - const comparator = new DummyComparator() as any; + const comparator = new DummyChangeAnalyzer() as any; const dataBuildService = { normalize: (object) => object - } as DataBuildService; + } as NormalizedObjectBuildService; const objectCache = { addPatch: () => { /* empty */ diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 47be86c296..045e82fbb6 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -41,20 +41,20 @@ import { NotificationOptions } from '../../shared/notifications/models/notificat import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; import { CacheableObject } from '../cache/object-cache.reducer'; -import { DataBuildService } from '../cache/builders/data-build.service'; -import { UpdateComparator } from './update-comparator'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { ChangeAnalyzer } from './change-analyzer'; export abstract class DataService { protected abstract requestService: RequestService; protected abstract rdbService: RemoteDataBuildService; - protected abstract dataBuildService: DataBuildService; + protected abstract dataBuildService: NormalizedObjectBuildService; protected abstract store: Store; protected abstract linkPath: string; protected abstract halService: HALEndpointService; protected abstract objectCache: ObjectCacheService; protected abstract notificationsService: NotificationsService; protected abstract http: HttpClient; - protected abstract comparator: UpdateComparator; + protected abstract comparator: ChangeAnalyzer; public abstract getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable @@ -138,7 +138,7 @@ export abstract class DataService { const newVersion = this.dataBuildService.normalize(object); - const operations = this.comparator.compare(oldVersion, newVersion); + const operations = this.comparator.diff(oldVersion, newVersion); if (isNotEmpty(operations)) { this.objectCache.addPatch(object.self, operations); } @@ -148,6 +148,15 @@ export abstract class DataService> { const requestId = this.requestService.generateRequestId(); const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe( diff --git a/src/app/core/data/dso-change-analyzer.service.ts b/src/app/core/data/dso-change-analyzer.service.ts new file mode 100644 index 0000000000..a47359e5c0 --- /dev/null +++ b/src/app/core/data/dso-change-analyzer.service.ts @@ -0,0 +1,26 @@ +import { Operation } from 'fast-json-patch/lib/core'; +import { compare } from 'fast-json-patch'; +import { ChangeAnalyzer } from './change-analyzer'; +import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model'; +import { Injectable } from '@angular/core'; + +/** + * A class to determine what differs between two + * DSpaceObjects + */ +@Injectable() +export class DSOChangeAnalyzer implements ChangeAnalyzer { + + /** + * Compare the metadata of two DSpaceObjects and return the differences as + * a JsonPatch Operation Array + * + * @param {NormalizedDSpaceObject} object1 + * The first object to compare + * @param {NormalizedDSpaceObject} object2 + * The second object to compare + */ + diff(object1: NormalizedDSpaceObject, object2: NormalizedDSpaceObject): Operation[] { + return compare(object1.metadata, object2.metadata).map((operation: Operation) => Object.assign({}, operation, { path: '/metadata' + operation.path })); + } +} diff --git a/src/app/core/data/dso-update-comparator.ts b/src/app/core/data/dso-update-comparator.ts deleted file mode 100644 index 245fbfaef0..0000000000 --- a/src/app/core/data/dso-update-comparator.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Operation } from 'fast-json-patch/lib/core'; -import { compare } from 'fast-json-patch'; -import { UpdateComparator } from './update-comparator'; -import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model'; -import { Injectable } from '@angular/core'; - -@Injectable() -export class DSOUpdateComparator implements UpdateComparator { - compare(object1: NormalizedDSpaceObject, object2: NormalizedDSpaceObject): Operation[] { - return compare(object1.metadata, object2.metadata).map((operation: Operation) => Object.assign({}, operation, { path: '/metadata' + operation.path })); - } -} diff --git a/src/app/core/data/dspace-object-data.service.spec.ts b/src/app/core/data/dspace-object-data.service.spec.ts index 2d478b8f73..7047db6065 100644 --- a/src/app/core/data/dspace-object-data.service.spec.ts +++ b/src/app/core/data/dspace-object-data.service.spec.ts @@ -9,7 +9,7 @@ import { DSpaceObjectDataService } from './dspace-object-data.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; -import { DataBuildService } from '../cache/builders/data-build.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; describe('DSpaceObjectDataService', () => { let scheduler: TestScheduler; @@ -46,7 +46,7 @@ describe('DSpaceObjectDataService', () => { const notificationsService = {} as NotificationsService; const http = {} as HttpClient; const comparator = {} as any; - const dataBuildService = {} as DataBuildService; + const dataBuildService = {} as NormalizedObjectBuildService; service = new DSpaceObjectDataService( requestService, diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index 9a069c4d61..d485fd0bc4 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -13,8 +13,8 @@ import { FindAllOptions } from './request.models'; import { ObjectCacheService } from '../cache/object-cache.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; -import { DataBuildService } from '../cache/builders/data-build.service'; -import { DSOUpdateComparator } from './dso-update-comparator'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; /* tslint:disable:max-classes-per-file */ class DataServiceImpl extends DataService { @@ -23,13 +23,13 @@ class DataServiceImpl extends DataService constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: DataBuildService, + protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, - protected comparator: DSOUpdateComparator) { + protected comparator: DSOChangeAnalyzer) { super(); } @@ -50,12 +50,12 @@ export class DSpaceObjectDataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: DataBuildService, + protected dataBuildService: NormalizedObjectBuildService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, - protected comparator: DSOUpdateComparator) { + protected comparator: DSOChangeAnalyzer) { this.dataService = new DataServiceImpl(requestService, rdbService, dataBuildService, null, objectCache, halService, notificationsService, http, comparator); } diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index 1be361cb9d..0cac6f901d 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -11,7 +11,7 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { FindAllOptions } from './request.models'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; -import { DataBuildService } from '../cache/builders/data-build.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; describe('ItemDataService', () => { let scheduler: TestScheduler; @@ -40,7 +40,7 @@ describe('ItemDataService', () => { const notificationsService = {} as NotificationsService; const http = {} as HttpClient; const comparator = {} as any; - const dataBuildService = {} as DataBuildService; + const dataBuildService = {} as NormalizedObjectBuildService; function initMockBrowseService(isSuccessful: boolean) { const obs = isSuccessful ? diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 2fb2c017dc..32cb00c1a9 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -18,8 +18,8 @@ import { FindAllOptions } from './request.models'; import { ObjectCacheService } from '../cache/object-cache.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; -import { DataBuildService } from '../cache/builders/data-build.service'; -import { DSOUpdateComparator } from './dso-update-comparator'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; @Injectable() export class ItemDataService extends DataService { @@ -28,14 +28,14 @@ export class ItemDataService extends DataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected dataBuildService: DataBuildService, + protected dataBuildService: NormalizedObjectBuildService, protected store: Store, private bs: BrowseService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, - protected comparator: DSOUpdateComparator) { + protected comparator: DSOChangeAnalyzer) { super(); } diff --git a/src/app/core/data/request.reducer.ts b/src/app/core/data/request.reducer.ts index a680de2d6b..27b5a4188a 100644 --- a/src/app/core/data/request.reducer.ts +++ b/src/app/core/data/request.reducer.ts @@ -86,7 +86,17 @@ function completeRequest(state: RequestState, action: RequestCompleteAction): Re }); } -function resetResponseTimestamps(state: RequestState, action: ResetResponseTimestampsAction) { +/** + * Reset the timeAdded property of all responses + * + * @param state + * the current state + * @param action + * a RequestCompleteAction + * @return RequestState + * the new state, with the timeAdded property reset + */ +function resetResponseTimestamps(state: RequestState, action: ResetResponseTimestampsAction): RequestState { const newState = Object.create(null); Object.keys(state).forEach((key) => { newState[key] = Object.assign({}, state[key], diff --git a/src/app/core/data/update-comparator.ts b/src/app/core/data/update-comparator.ts deleted file mode 100644 index f064d7f3f2..0000000000 --- a/src/app/core/data/update-comparator.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { NormalizedObject } from '../cache/models/normalized-object.model'; -import { Operation } from 'fast-json-patch/lib/core'; - -export interface UpdateComparator { - compare(object1: TNormalized, object2: TNormalized): Operation[]; -} diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts index 59038228bb..20d6b1dfb3 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts @@ -79,6 +79,14 @@ export class DSpaceRESTv2Service { })); } + /** + * Create a FormData object from a DSpaceObject + * + * @param {DSpaceObject} dso + * the DSpaceObject + * @return {FormData} + * the result + */ buildFormData(dso: DSpaceObject): FormData { const form: FormData = new FormData(); form.append('name', dso.name); diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index ef1d69b8b5..90c811db7a 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -35,8 +35,8 @@ import { AuthService } from '../auth/auth.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; import { EmptyError } from 'rxjs/internal-compatibility'; -import { DataBuildService } from '../cache/builders/data-build.service'; -import { DSOUpdateComparator } from '../data/dso-update-comparator'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; /* tslint:disable:max-classes-per-file */ @Component({ @@ -119,8 +119,8 @@ describe('MetadataService', () => { { provide: AuthService, useValue: {} }, { provide: NotificationsService, useValue: {} }, { provide: HttpClient, useValue: {} }, - { provide: DataBuildService, useValue: {} }, - { provide: DSOUpdateComparator, useValue: {} }, + { provide: NormalizedObjectBuildService, useValue: {} }, + { provide: DSOChangeAnalyzer, useValue: {} }, Meta, Title, ItemDataService, diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts index 19050e2bc2..17710fd1c6 100644 --- a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts @@ -11,6 +11,9 @@ import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { isNotEmpty } from '../../empty.util'; import { ResourceType } from '../../../core/shared/resource-type'; +/** + * A form for creating and editing Communities or Collections + */ @Component({ selector: 'ds-comcol-form', styleUrls: ['./comcol-form.component.scss'], diff --git a/src/app/shared/testing/utils.ts b/src/app/shared/testing/utils.ts index cd17a1b1f5..770a554439 100644 --- a/src/app/shared/testing/utils.ts +++ b/src/app/shared/testing/utils.ts @@ -31,6 +31,14 @@ export const createTestComponent = (html: string, type: { new(...args: any[]) return fixture as ComponentFixture; }; +/** + * Allows you to spy on a read only property + * + * @param obj + * The object to spy on + * @param prop + * The property to spy on + */ export function spyOnOperator(obj: any, prop: string): any { const oldProp = obj[prop]; Object.defineProperty(obj, prop, { diff --git a/src/config/auto-sync-config.interface.ts b/src/config/auto-sync-config.interface.ts index 5285916b12..90e7ebee80 100644 --- a/src/config/auto-sync-config.interface.ts +++ b/src/config/auto-sync-config.interface.ts @@ -1,11 +1,30 @@ import { RestRequestMethod } from '../app/core/data/rest-request-method'; +/** + * The number of seconds between automatic syncs to the + * server for requests using a certain HTTP Method + */ type TimePerMethod = { [method in RestRequestMethod]: number; }; +/** + * The config that determines how the automatic syncing + * of changed data to the server works + */ export interface AutoSyncConfig { + /** + * The number of seconds between automatic syncs to the server + */ defaultTime: number; + + /** + * HTTP Method specific overrides of defaultTime + */ timePerMethod: TimePerMethod; + + /** + * The max number of requests in the buffer before a sync to the server + */ maxBufferSize: number; }; From c4004eaaa2d3f4f4a530fb6449844fab00922d6e Mon Sep 17 00:00:00 2001 From: lotte Date: Tue, 22 Jan 2019 09:46:55 +0100 Subject: [PATCH 68/69] fixes for conflicts with master --- .../item-delete/item-delete.component.spec.ts | 38 +++++++++---------- .../item-delete/item-delete.component.ts | 10 ++--- .../item-private.component.spec.ts | 2 +- .../item-private/item-private.component.ts | 12 +++--- .../item-public/item-public.component.spec.ts | 36 +++++++++--------- .../item-public/item-public.component.ts | 12 +++--- .../item-reinstate.component.spec.ts | 36 +++++++++--------- .../item-reinstate.component.ts | 12 +++--- .../item-withdraw.component.spec.ts | 36 +++++++++--------- .../item-withdraw/item-withdraw.component.ts | 12 +++--- ...tract-simple-item-action.component.spec.ts | 38 +++++++++---------- .../abstract-simple-item-action.component.ts | 2 +- src/app/core/auth/auth-request.service.ts | 2 - src/app/core/data/item-data.service.spec.ts | 7 ++++ 14 files changed, 130 insertions(+), 125 deletions(-) diff --git a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts index 50c494128a..c03ae7c3fe 100644 --- a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts @@ -1,22 +1,22 @@ -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import {Item} from '../../../core/shared/item.model'; -import {RouterStub} from '../../../shared/testing/router-stub'; -import {of as observableOf} from 'rxjs'; -import {RestResponse} from '../../../core/cache/response-cache.models'; -import {RemoteData} from '../../../core/data/remote-data'; -import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub'; -import {CommonModule} from '@angular/common'; -import {FormsModule} from '@angular/forms'; -import {RouterTestingModule} from '@angular/router/testing'; -import {TranslateModule} from '@ngx-translate/core'; -import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; -import {ActivatedRoute, Router} from '@angular/router'; -import {ItemDataService} from '../../../core/data/item-data.service'; -import {NotificationsService} from '../../../shared/notifications/notifications.service'; -import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; -import {By} from '@angular/platform-browser'; -import {ItemDeleteComponent} from './item-delete.component'; -import {getItemEditPath} from '../../item-page-routing.module'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Item } from '../../../core/shared/item.model'; +import { RouterStub } from '../../../shared/testing/router-stub'; +import { of as observableOf } from 'rxjs'; +import { RemoteData } from '../../../core/data/remote-data'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { ItemDeleteComponent } from './item-delete.component'; +import { getItemEditPath } from '../../item-page-routing.module'; +import { RestResponse } from '../../../core/cache/response.models'; let comp: ItemDeleteComponent; let fixture: ComponentFixture; diff --git a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts index 68c5738f7d..95f25c67bc 100644 --- a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts +++ b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts @@ -1,8 +1,8 @@ -import {Component} from '@angular/core'; -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 {getItemEditPath} from '../../item-page-routing.module'; +import { Component } from '@angular/core'; +import { first } from 'rxjs/operators'; +import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component'; +import { getItemEditPath } from '../../item-page-routing.module'; +import { RestResponse } from '../../../core/cache/response.models'; @Component({ selector: 'ds-item-delete', diff --git a/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts b/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts index 5b99ced743..9f9447704b 100644 --- a/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts @@ -2,7 +2,6 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {Item} from '../../../core/shared/item.model'; import {RouterStub} from '../../../shared/testing/router-stub'; import {of as observableOf} from 'rxjs'; -import {RestResponse} from '../../../core/cache/response-cache.models'; import {RemoteData} from '../../../core/data/remote-data'; import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub'; import {CommonModule} from '@angular/common'; @@ -16,6 +15,7 @@ import {NotificationsService} from '../../../shared/notifications/notifications. import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; import {By} from '@angular/platform-browser'; import {ItemPrivateComponent} from './item-private.component'; +import { RestResponse } from '../../../core/cache/response.models'; let comp: ItemPrivateComponent; let fixture: ComponentFixture; diff --git a/src/app/+item-page/edit-item-page/item-private/item-private.component.ts b/src/app/+item-page/edit-item-page/item-private/item-private.component.ts index f1e7600c18..d949e4fa6e 100644 --- a/src/app/+item-page/edit-item-page/item-private/item-private.component.ts +++ b/src/app/+item-page/edit-item-page/item-private/item-private.component.ts @@ -1,9 +1,9 @@ -import {Component} from '@angular/core'; -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 {RemoteData} from '../../../core/data/remote-data'; -import {Item} from '../../../core/shared/item.model'; +import { Component } from '@angular/core'; +import { first } from 'rxjs/operators'; +import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Item } from '../../../core/shared/item.model'; +import { RestResponse } from '../../../core/cache/response.models'; @Component({ selector: 'ds-item-private', diff --git a/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts b/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts index 182d3ffabe..97c81681d0 100644 --- a/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts @@ -1,21 +1,21 @@ -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import {Item} from '../../../core/shared/item.model'; -import {RouterStub} from '../../../shared/testing/router-stub'; -import {of as observableOf} from 'rxjs'; -import {RestResponse} from '../../../core/cache/response-cache.models'; -import {RemoteData} from '../../../core/data/remote-data'; -import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub'; -import {CommonModule} from '@angular/common'; -import {FormsModule} from '@angular/forms'; -import {RouterTestingModule} from '@angular/router/testing'; -import {TranslateModule} from '@ngx-translate/core'; -import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; -import {ActivatedRoute, Router} from '@angular/router'; -import {ItemDataService} from '../../../core/data/item-data.service'; -import {NotificationsService} from '../../../shared/notifications/notifications.service'; -import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; -import {By} from '@angular/platform-browser'; -import {ItemPublicComponent} from './item-public.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Item } from '../../../core/shared/item.model'; +import { RouterStub } from '../../../shared/testing/router-stub'; +import { of as observableOf } from 'rxjs'; +import { RemoteData } from '../../../core/data/remote-data'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { ItemPublicComponent } from './item-public.component'; +import { RestResponse } from '../../../core/cache/response.models'; let comp: ItemPublicComponent; let fixture: ComponentFixture; diff --git a/src/app/+item-page/edit-item-page/item-public/item-public.component.ts b/src/app/+item-page/edit-item-page/item-public/item-public.component.ts index d65d0f171d..272cf9a96f 100644 --- a/src/app/+item-page/edit-item-page/item-public/item-public.component.ts +++ b/src/app/+item-page/edit-item-page/item-public/item-public.component.ts @@ -1,9 +1,9 @@ -import {Component} from '@angular/core'; -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 {RemoteData} from '../../../core/data/remote-data'; -import {Item} from '../../../core/shared/item.model'; +import { Component } from '@angular/core'; +import { first } from 'rxjs/operators'; +import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Item } from '../../../core/shared/item.model'; +import { RestResponse } from '../../../core/cache/response.models'; @Component({ selector: 'ds-item-public', diff --git a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts index dbea7f3e69..e89eda736f 100644 --- a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts @@ -1,21 +1,21 @@ -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import {Item} from '../../../core/shared/item.model'; -import {RouterStub} from '../../../shared/testing/router-stub'; -import {of as observableOf} from 'rxjs'; -import {RestResponse} from '../../../core/cache/response-cache.models'; -import {RemoteData} from '../../../core/data/remote-data'; -import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub'; -import {CommonModule} from '@angular/common'; -import {FormsModule} from '@angular/forms'; -import {RouterTestingModule} from '@angular/router/testing'; -import {TranslateModule} from '@ngx-translate/core'; -import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; -import {ActivatedRoute, Router} from '@angular/router'; -import {ItemDataService} from '../../../core/data/item-data.service'; -import {NotificationsService} from '../../../shared/notifications/notifications.service'; -import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; -import {By} from '@angular/platform-browser'; -import {ItemReinstateComponent} from './item-reinstate.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Item } from '../../../core/shared/item.model'; +import { RouterStub } from '../../../shared/testing/router-stub'; +import { of as observableOf } from 'rxjs'; +import { RemoteData } from '../../../core/data/remote-data'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { ItemReinstateComponent } from './item-reinstate.component'; +import { RestResponse } from '../../../core/cache/response.models'; let comp: ItemReinstateComponent; let fixture: ComponentFixture; diff --git a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts index 5c710b0a81..9c0e1c8d05 100644 --- a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts +++ b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts @@ -1,9 +1,9 @@ -import {Component} from '@angular/core'; -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 {RemoteData} from '../../../core/data/remote-data'; -import {Item} from '../../../core/shared/item.model'; +import { Component } from '@angular/core'; +import { first } from 'rxjs/operators'; +import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Item } from '../../../core/shared/item.model'; +import { RestResponse } from '../../../core/cache/response.models'; @Component({ selector: 'ds-item-reinstate', diff --git a/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts index e1de52a506..9305459c12 100644 --- a/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts @@ -1,21 +1,21 @@ -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import {Item} from '../../../core/shared/item.model'; -import {RouterStub} from '../../../shared/testing/router-stub'; -import {of as observableOf} from 'rxjs'; -import {RestResponse} from '../../../core/cache/response-cache.models'; -import {RemoteData} from '../../../core/data/remote-data'; -import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub'; -import {CommonModule} from '@angular/common'; -import {FormsModule} from '@angular/forms'; -import {RouterTestingModule} from '@angular/router/testing'; -import {TranslateModule} from '@ngx-translate/core'; -import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; -import {ActivatedRoute, Router} from '@angular/router'; -import {ItemDataService} from '../../../core/data/item-data.service'; -import {NotificationsService} from '../../../shared/notifications/notifications.service'; -import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; -import {ItemWithdrawComponent} from './item-withdraw.component'; -import {By} from '@angular/platform-browser'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Item } from '../../../core/shared/item.model'; +import { RouterStub } from '../../../shared/testing/router-stub'; +import { of as observableOf } from 'rxjs'; +import { RemoteData } from '../../../core/data/remote-data'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ItemWithdrawComponent } from './item-withdraw.component'; +import { By } from '@angular/platform-browser'; +import { RestResponse } from '../../../core/cache/response.models'; let comp: ItemWithdrawComponent; let fixture: ComponentFixture; diff --git a/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts index 6924124efc..1fed1756a4 100644 --- a/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts +++ b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts @@ -1,9 +1,9 @@ -import {Component} from '@angular/core'; -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 {RemoteData} from '../../../core/data/remote-data'; -import {Item} from '../../../core/shared/item.model'; +import { Component } from '@angular/core'; +import { first } from 'rxjs/operators'; +import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Item } from '../../../core/shared/item.model'; +import { RestResponse } from '../../../core/cache/response.models'; @Component({ selector: 'ds-item-withdraw', diff --git a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts index 2da671517c..1c4cae552e 100644 --- a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts +++ b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts @@ -1,22 +1,22 @@ -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import {Item} from '../../../core/shared/item.model'; -import {RouterStub} from '../../../shared/testing/router-stub'; -import {CommonModule} from '@angular/common'; -import {RouterTestingModule} from '@angular/router/testing'; -import {TranslateModule} from '@ngx-translate/core'; -import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; -import {ActivatedRoute, Router} from '@angular/router'; -import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub'; -import {NotificationsService} from '../../../shared/notifications/notifications.service'; -import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; -import {FormsModule} from '@angular/forms'; -import {ItemDataService} from '../../../core/data/item-data.service'; -import {RemoteData} from '../../../core/data/remote-data'; -import {AbstractSimpleItemActionComponent} from './abstract-simple-item-action.component'; -import {By} from '@angular/platform-browser'; -import {RestResponse} from '../../../core/cache/response-cache.models'; -import {of as observableOf} from 'rxjs'; -import {getItemEditPath} from '../../item-page-routing.module'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Item } from '../../../core/shared/item.model'; +import { RouterStub } from '../../../shared/testing/router-stub'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ActivatedRoute, Router } from '@angular/router'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { RemoteData } from '../../../core/data/remote-data'; +import { AbstractSimpleItemActionComponent } from './abstract-simple-item-action.component'; +import { By } from '@angular/platform-browser'; +import { of as observableOf } from 'rxjs'; +import { getItemEditPath } from '../../item-page-routing.module'; +import { RestResponse } from '../../../core/cache/response.models'; /** * Test component that implements the AbstractSimpleItemActionComponent used to test the diff --git a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts index 743b52921f..7773dbb573 100644 --- a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts +++ b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts @@ -8,9 +8,9 @@ import {RemoteData} from '../../../core/data/remote-data'; import {Observable} from 'rxjs'; import {getSucceededRemoteData} from '../../../core/shared/operators'; import {first, map} from 'rxjs/operators'; -import {RestResponse} from '../../../core/cache/response-cache.models'; import {findSuccessfulAccordingTo} from '../edit-item-operators'; 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. diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index f957d807c1..6d782cbbe2 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -25,8 +25,6 @@ export class AuthRequestService { protected fetchRequest(request: RestRequest): Observable { return this.requestService.getByUUID(request.uuid).pipe( getResponseFromEntry(), - // TODO to review when https://github.com/DSpace/dspace-angular/issues/217 will be fixed - // tap(() => this.responseCache.remove(request.href)), mergeMap((response) => { if (response.isSuccessful && isNotEmpty(response)) { return observableOf((response as AuthStatusResponse).response); diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index 93ff3b051b..6cf7e503d3 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -14,6 +14,8 @@ import { RestResponse } from '../cache/response.models'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { HttpClient } from '@angular/common/http'; +import { RequestEntry } from './request.reducer'; +import { of as observableOf } from 'rxjs'; describe('ItemDataService', () => { let scheduler: TestScheduler; @@ -25,6 +27,11 @@ describe('ItemDataService', () => { }, configure(request: RestRequest) { // Do nothing + }, + getByHref(requestHref: string) { + const responseCacheEntry = new RequestEntry(); + responseCacheEntry.response = new RestResponse(true, '200'); + return observableOf(responseCacheEntry); } } as RequestService; const rdbService = {} as RemoteDataBuildService; From af4a6bab798b3dad7d047ce5c5e4d81e39d40514 Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 7 Feb 2019 14:14:35 +0100 Subject: [PATCH 69/69] processed PR feedback --- src/app/core/cache/models/normalized-dspace-object.model.ts | 2 +- src/app/core/data/data.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/core/cache/models/normalized-dspace-object.model.ts b/src/app/core/cache/models/normalized-dspace-object.model.ts index efdfa6dd74..030e17364a 100644 --- a/src/app/core/cache/models/normalized-dspace-object.model.ts +++ b/src/app/core/cache/models/normalized-dspace-object.model.ts @@ -18,7 +18,7 @@ export class NormalizedDSpaceObject extends NormalizedObject { * Repeated here to make the serialization work, * inheritSerialization doesn't seem to work for more than one level */ - @autoserialize + @deserialize self: string; /** diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 045e82fbb6..c164ca8b56 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -162,7 +162,7 @@ export abstract class DataService parentUUID ? `${endpoint}?parentCommunity=${parentUUID}` : endpoint) + map((endpoint: string) => parentUUID ? `${endpoint}?parent=${parentUUID}` : endpoint) ); const normalizedObject: TNormalized = this.dataBuildService.normalize(dso);