mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge remote-tracking branch 'atmire/master' into Browse-by-links
Conflicts: src/app/core/browse/browse.service.spec.ts src/app/core/data/dso-response-parsing.service.ts src/app/shared/shared.module.ts
This commit is contained in:
@@ -18,9 +18,16 @@ module.exports = {
|
||||
// Caching settings
|
||||
cache: {
|
||||
// NOTE: how long should objects be cached for by default
|
||||
msToLive: 15 * 60 * 1000, // 15 minutes
|
||||
msToLive: {
|
||||
default: 15 * 60 * 1000, // 15 minutes
|
||||
},
|
||||
// msToLive: 1000, // 15 minutes
|
||||
control: 'max-age=60' // revalidate browser
|
||||
control: 'max-age=60', // revalidate browser
|
||||
autoSync: {
|
||||
defaultTime: 0,
|
||||
maxBufferSize: 100,
|
||||
timePerMethod: {'PATCH': 3} //time in seconds
|
||||
}
|
||||
},
|
||||
// Form settings
|
||||
form: {
|
||||
|
@@ -96,6 +96,7 @@
|
||||
"core-js": "^2.5.7",
|
||||
"express": "4.16.2",
|
||||
"express-session": "1.15.6",
|
||||
"fast-json-patch": "^2.0.7",
|
||||
"font-awesome": "4.7.0",
|
||||
"fork-ts-checker-webpack-plugin": "^0.4.10",
|
||||
"http-server": "0.11.1",
|
||||
|
@@ -13,6 +13,38 @@
|
||||
"head": "Recent Submissions"
|
||||
}
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"title": "Name",
|
||||
"description": "Introductory text (HTML)",
|
||||
"abstract": "Short Description",
|
||||
"rights": "Copyright text (HTML)",
|
||||
"tableofcontents": "News (HTML)",
|
||||
"license": "License",
|
||||
"provenance": "Provenance",
|
||||
"errors": {
|
||||
"title": {
|
||||
"required": "Please enter a collection name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"edit": {
|
||||
"head": "Edit Collection",
|
||||
"delete": "Delete this collection"
|
||||
},
|
||||
"create": {
|
||||
"head": "Create a Collection",
|
||||
"sub-head": "Create a Collection for Community {{ parent }}"
|
||||
},
|
||||
"delete": {
|
||||
"head": "Delete Collection",
|
||||
"text": "Are you sure you want to delete collection \"{{ dso }}\"",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"notification": {
|
||||
"success": "Successfully deleted collection",
|
||||
"fail": "Collection could not be deleted"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community": {
|
||||
@@ -25,6 +57,36 @@
|
||||
},
|
||||
"sub-community-list": {
|
||||
"head": "Communities of this Community"
|
||||
},
|
||||
"form": {
|
||||
"title": "Name",
|
||||
"description": "Introductory text (HTML)",
|
||||
"abstract": "Short Description",
|
||||
"rights": "Copyright text (HTML)",
|
||||
"tableofcontents": "News (HTML)",
|
||||
"errors": {
|
||||
"title": {
|
||||
"required": "Please enter a community name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"edit": {
|
||||
"head": "Edit Community",
|
||||
"delete": "Delete this community"
|
||||
},
|
||||
"create": {
|
||||
"head": "Create a Community",
|
||||
"sub-head": "Create a Sub-Community for Community {{ parent }}"
|
||||
},
|
||||
"delete": {
|
||||
"head": "Delete Community",
|
||||
"text": "Are you sure you want to delete community \"{{ dso }}\"",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"notification": {
|
||||
"success": "Successfully deleted community",
|
||||
"fail": "Community could not be deleted"
|
||||
}
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
|
@@ -0,0 +1,71 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import {
|
||||
DynamicInputModel,
|
||||
DynamicTextAreaModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
|
||||
import { ResourceType } from '../../core/shared/resource-type';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component';
|
||||
|
||||
/**
|
||||
* Form used for creating and editing collections
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-collection-form',
|
||||
styleUrls: ['../../shared/comcol-forms/comcol-form/comcol-form.component.scss'],
|
||||
templateUrl: '../../shared/comcol-forms/comcol-form/comcol-form.component.html'
|
||||
})
|
||||
export class CollectionFormComponent extends ComColFormComponent<Collection> {
|
||||
/**
|
||||
* @type {Collection} A new collection when a collection is being created, an existing Input collection when a collection is being edited
|
||||
*/
|
||||
@Input() dso: Collection = new Collection();
|
||||
|
||||
/**
|
||||
* @type {ResourceType.Collection} This is a collection-type form
|
||||
*/
|
||||
protected type = ResourceType.Collection;
|
||||
|
||||
/**
|
||||
* The dynamic form fields used for creating/editing a collection
|
||||
* @type {(DynamicInputModel | DynamicTextAreaModel)[]}
|
||||
*/
|
||||
formModel: DynamicFormControlModel[] = [
|
||||
new DynamicInputModel({
|
||||
id: 'title',
|
||||
name: 'dc.title',
|
||||
required: true,
|
||||
validators: {
|
||||
required: null
|
||||
},
|
||||
errorMessages: {
|
||||
required: 'Please enter a name for this title'
|
||||
},
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'description',
|
||||
name: 'dc.description',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'abstract',
|
||||
name: 'dc.description.abstract',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'rights',
|
||||
name: 'dc.rights',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'tableofcontents',
|
||||
name: 'dc.description.tableofcontents',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'license',
|
||||
name: 'dc.rights.license',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'provenance',
|
||||
name: 'dc.description.provenance',
|
||||
}),
|
||||
];
|
||||
}
|
@@ -3,10 +3,38 @@ import { RouterModule } from '@angular/router';
|
||||
|
||||
import { CollectionPageComponent } from './collection-page.component';
|
||||
import { CollectionPageResolver } from './collection-page.resolver';
|
||||
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { EditCollectionPageComponent } from './edit-collection-page/edit-collection-page.component';
|
||||
import { CreateCollectionPageGuard } from './create-collection-page/create-collection-page.guard';
|
||||
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: 'create',
|
||||
component: CreateCollectionPageComponent,
|
||||
canActivate: [AuthenticatedGuard, CreateCollectionPageGuard]
|
||||
},
|
||||
{
|
||||
path: ':id/edit',
|
||||
pathMatch: 'full',
|
||||
component: EditCollectionPageComponent,
|
||||
canActivate: [AuthenticatedGuard],
|
||||
resolve: {
|
||||
dso: CollectionPageResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ':id/delete',
|
||||
pathMatch: 'full',
|
||||
component: DeleteCollectionPageComponent,
|
||||
canActivate: [AuthenticatedGuard],
|
||||
resolve: {
|
||||
dso: CollectionPageResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
component: CollectionPageComponent,
|
||||
@@ -19,6 +47,7 @@ import { CollectionPageResolver } from './collection-page.resolver';
|
||||
],
|
||||
providers: [
|
||||
CollectionPageResolver,
|
||||
CreateCollectionPageGuard
|
||||
]
|
||||
})
|
||||
export class CollectionPageRoutingModule {
|
||||
|
@@ -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, 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';
|
||||
@@ -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<Collection>) => rd.payload),
|
||||
@@ -75,8 +76,8 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
||||
pagination: pagination,
|
||||
sort: this.sortConfig
|
||||
});
|
||||
})
|
||||
);
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
updatePage(searchOptions) {
|
||||
|
@@ -5,7 +5,11 @@ 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';
|
||||
import { SearchPageModule } from '../+search-page/search-page.module';
|
||||
import { EditCollectionPageComponent } from './edit-collection-page/edit-collection-page.component';
|
||||
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -16,6 +20,10 @@ import { SearchPageModule } from '../+search-page/search-page.module';
|
||||
],
|
||||
declarations: [
|
||||
CollectionPageComponent,
|
||||
CreateCollectionPageComponent,
|
||||
EditCollectionPageComponent,
|
||||
DeleteCollectionPageComponent,
|
||||
CollectionFormComponent
|
||||
]
|
||||
})
|
||||
export class CollectionPageModule {
|
||||
|
@@ -0,0 +1,8 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 pb-4">
|
||||
<h2 id="sub-header" class="border-bottom pb-2">{{'collection.create.sub-head' | translate:{ parent: (parentRD$| async)?.payload.name } }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<ds-collection-form (submitForm)="onSubmit($event)"></ds-collection-form>
|
||||
</div>
|
@@ -0,0 +1 @@
|
||||
|
@@ -0,0 +1,46 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { RouteService } from '../../shared/services/route.service';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { CreateCollectionPageComponent } from './create-collection-page.component';
|
||||
|
||||
describe('CreateCollectionPageComponent', () => {
|
||||
let comp: CreateCollectionPageComponent;
|
||||
let fixture: ComponentFixture<CreateCollectionPageComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||
declarations: [CreateCollectionPageComponent],
|
||||
providers: [
|
||||
{ provide: CollectionDataService, useValue: {} },
|
||||
{
|
||||
provide: CommunityDataService,
|
||||
useValue: { findById: () => observableOf({ payload: { name: 'test' } }) }
|
||||
},
|
||||
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } },
|
||||
{ provide: Router, useValue: {} },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CreateCollectionPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('frontendURL', () => {
|
||||
it('should have the right frontendURL set', () => {
|
||||
expect((comp as any).frontendURL).toEqual('/collections/');
|
||||
})
|
||||
});
|
||||
});
|
@@ -0,0 +1,29 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { RouteService } from '../../shared/services/route.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component';
|
||||
import { NormalizedCollection } from '../../core/cache/models/normalized-collection.model';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
|
||||
/**
|
||||
* Component that represents the page where a user can create a new Collection
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-create-collection',
|
||||
styleUrls: ['./create-collection-page.component.scss'],
|
||||
templateUrl: './create-collection-page.component.html'
|
||||
})
|
||||
export class CreateCollectionPageComponent extends CreateComColPageComponent<Collection, NormalizedCollection> {
|
||||
protected frontendURL = '/collections/';
|
||||
|
||||
public constructor(
|
||||
protected communityDataService: CommunityDataService,
|
||||
protected collectionDataService: CollectionDataService,
|
||||
protected routeService: RouteService,
|
||||
protected router: Router
|
||||
) {
|
||||
super(collectionDataService, communityDataService, routeService, router);
|
||||
}
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
import { CreateCollectionPageGuard } from './create-collection-page.guard';
|
||||
import { MockRouter } from '../../shared/mocks/mock-router';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
describe('CreateCollectionPageGuard', () => {
|
||||
describe('canActivate', () => {
|
||||
let guard: CreateCollectionPageGuard;
|
||||
let router;
|
||||
let communityDataServiceStub: any;
|
||||
|
||||
beforeEach(() => {
|
||||
communityDataServiceStub = {
|
||||
findById: (id: string) => {
|
||||
if (id === 'valid-id') {
|
||||
return observableOf(new RemoteData(false, false, true, null, new Community()));
|
||||
} else if (id === 'invalid-id') {
|
||||
return observableOf(new RemoteData(false, false, true, null, undefined));
|
||||
} else if (id === 'error-id') {
|
||||
return observableOf(new RemoteData(false, false, false, null, new Community()));
|
||||
}
|
||||
}
|
||||
};
|
||||
router = new MockRouter();
|
||||
|
||||
guard = new CreateCollectionPageGuard(router, communityDataServiceStub);
|
||||
});
|
||||
|
||||
it('should return true when the parent ID resolves to a community', () => {
|
||||
guard.canActivate({ queryParams: { parent: 'valid-id' } } as any, undefined)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(canActivate) =>
|
||||
expect(canActivate).toEqual(true)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when no parent ID has been provided', () => {
|
||||
guard.canActivate({ queryParams: { } } as any, undefined)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(canActivate) =>
|
||||
expect(canActivate).toEqual(false)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when the parent ID does not resolve to a community', () => {
|
||||
guard.canActivate({ queryParams: { parent: 'invalid-id' } } as any, undefined)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(canActivate) =>
|
||||
expect(canActivate).toEqual(false)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when the parent ID resolves to an error response', () => {
|
||||
guard.canActivate({ queryParams: { parent: 'error-id' } } as any, undefined)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(canActivate) =>
|
||||
expect(canActivate).toEqual(false)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,46 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
|
||||
|
||||
import { hasNoValue, hasValue } from '../../shared/empty.util';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { getFinishedRemoteData } from '../../core/shared/operators';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Prevent creation of a collection without a parent community provided
|
||||
* @class CreateCollectionPageGuard
|
||||
*/
|
||||
@Injectable()
|
||||
export class CreateCollectionPageGuard implements CanActivate {
|
||||
public constructor(private router: Router, private communityService: CommunityDataService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* True when either a parent ID query parameter has been provided and the parent ID resolves to a valid parent community
|
||||
* Reroutes to a 404 page when the page cannot be activated
|
||||
* @method canActivate
|
||||
*/
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||
const parentID = route.queryParams.parent;
|
||||
if (hasNoValue(parentID)) {
|
||||
this.router.navigate(['/404']);
|
||||
return observableOf(false);
|
||||
}
|
||||
const parent: Observable<RemoteData<Community>> = this.communityService.findById(parentID)
|
||||
.pipe(
|
||||
getFinishedRemoteData(),
|
||||
);
|
||||
|
||||
return parent.pipe(
|
||||
map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
|
||||
tap((isValid: boolean) => {
|
||||
if (!isValid) {
|
||||
this.router.navigate(['/404']);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<ng-container *ngVar="(dsoRD$ | async)?.payload as dso">
|
||||
<div class="col-12 pb-4">
|
||||
<h2 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate
|
||||
}}</h2>
|
||||
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p>
|
||||
<button class="btn btn-primary mr-2" (click)="onConfirm(dso)">
|
||||
{{'community.delete.confirm' |
|
||||
translate}}
|
||||
</button>
|
||||
<button class="btn btn-primary" (click)="onCancel(dso)">{{'community.delete.cancel' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
@@ -0,0 +1 @@
|
||||
|
@@ -0,0 +1,41 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { DeleteCollectionPageComponent } from './delete-collection-page.component';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
|
||||
describe('DeleteCollectionPageComponent', () => {
|
||||
let comp: DeleteCollectionPageComponent;
|
||||
let fixture: ComponentFixture<DeleteCollectionPageComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||
declarations: [DeleteCollectionPageComponent],
|
||||
providers: [
|
||||
{ provide: CollectionDataService, useValue: {} },
|
||||
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
||||
{ provide: NotificationsService, useValue: {} },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DeleteCollectionPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('frontendURL', () => {
|
||||
it('should have the right frontendURL set', () => {
|
||||
expect((comp as any).frontendURL).toEqual('/collections/');
|
||||
})
|
||||
});
|
||||
});
|
@@ -0,0 +1,33 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model';
|
||||
import { DeleteComColPageComponent } from '../../shared/comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
import { NormalizedCollection } from '../../core/cache/models/normalized-collection.model';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
/**
|
||||
* Component that represents the page where a user can delete an existing Collection
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-delete-collection',
|
||||
styleUrls: ['./delete-collection-page.component.scss'],
|
||||
templateUrl: './delete-collection-page.component.html'
|
||||
})
|
||||
export class DeleteCollectionPageComponent extends DeleteComColPageComponent<Collection, NormalizedCollection> {
|
||||
protected frontendURL = '/collections/';
|
||||
|
||||
public constructor(
|
||||
protected dsoDataService: CollectionDataService,
|
||||
protected router: Router,
|
||||
protected route: ActivatedRoute,
|
||||
protected notifications: NotificationsService,
|
||||
protected translate: TranslateService
|
||||
) {
|
||||
super(dsoDataService, router, route, notifications, translate);
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 pb-4">
|
||||
<h2 id="header" class="border-bottom pb-2">{{ 'collection.edit.head' | translate }}</h2>
|
||||
<ds-collection-form (submitForm)="onSubmit($event)" [dso]="(dsoRD$ | async)?.payload"></ds-collection-form>
|
||||
<a class="btn btn-danger"
|
||||
[routerLink]="'/collections/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'collection.edit.delete'
|
||||
| translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1 @@
|
||||
|
@@ -0,0 +1,39 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { EditCollectionPageComponent } from './edit-collection-page.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
describe('EditCollectionPageComponent', () => {
|
||||
let comp: EditCollectionPageComponent;
|
||||
let fixture: ComponentFixture<EditCollectionPageComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||
declarations: [EditCollectionPageComponent],
|
||||
providers: [
|
||||
{ provide: CollectionDataService, useValue: {} },
|
||||
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EditCollectionPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('frontendURL', () => {
|
||||
it('should have the right frontendURL set', () => {
|
||||
expect((comp as any).frontendURL).toEqual('/collections/');
|
||||
})
|
||||
});
|
||||
});
|
@@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
||||
import { NormalizedCollection } from '../../core/cache/models/normalized-collection.model';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
|
||||
/**
|
||||
* Component that represents the page where a user can edit an existing Collection
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-edit-collection',
|
||||
styleUrls: ['./edit-collection-page.component.scss'],
|
||||
templateUrl: './edit-collection-page.component.html'
|
||||
})
|
||||
export class EditCollectionPageComponent extends EditComColPageComponent<Collection, NormalizedCollection> {
|
||||
protected frontendURL = '/collections/';
|
||||
|
||||
public constructor(
|
||||
protected collectionDataService: CollectionDataService,
|
||||
protected router: Router,
|
||||
protected route: ActivatedRoute
|
||||
) {
|
||||
super(collectionDataService, router, route);
|
||||
}
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core';
|
||||
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { ResourceType } from '../../core/shared/resource-type';
|
||||
import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component';
|
||||
|
||||
/**
|
||||
* Form used for creating and editing communities
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-community-form',
|
||||
styleUrls: ['../../shared/comcol-forms/comcol-form/comcol-form.component.scss'],
|
||||
templateUrl: '../../shared/comcol-forms/comcol-form/comcol-form.component.html'
|
||||
})
|
||||
export class CommunityFormComponent extends ComColFormComponent<Community> {
|
||||
/**
|
||||
* @type {Community} A new community when a community is being created, an existing Input community when a community is being edited
|
||||
*/
|
||||
@Input() dso: Community = new Community();
|
||||
|
||||
/**
|
||||
* @type {ResourceType.Community} This is a community-type form
|
||||
*/
|
||||
protected type = ResourceType.Community;
|
||||
|
||||
/**
|
||||
* The dynamic form fields used for creating/editing a community
|
||||
* @type {(DynamicInputModel | DynamicTextAreaModel)[]}
|
||||
*/
|
||||
formModel: DynamicFormControlModel[] = [
|
||||
new DynamicInputModel({
|
||||
id: 'title',
|
||||
name: 'dc.title',
|
||||
required: true,
|
||||
validators: {
|
||||
required: null
|
||||
},
|
||||
errorMessages: {
|
||||
required: 'Please enter a name for this title'
|
||||
},
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'description',
|
||||
name: 'dc.description',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'abstract',
|
||||
name: 'dc.description.abstract',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'rights',
|
||||
name: 'dc.rights',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'tableofcontents',
|
||||
name: 'dc.description.tableofcontents',
|
||||
}),
|
||||
];
|
||||
}
|
@@ -3,10 +3,38 @@ import { RouterModule } from '@angular/router';
|
||||
|
||||
import { CommunityPageComponent } from './community-page.component';
|
||||
import { CommunityPageResolver } from './community-page.resolver';
|
||||
import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component';
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { EditCommunityPageComponent } from './edit-community-page/edit-community-page.component';
|
||||
import { CreateCommunityPageGuard } from './create-community-page/create-community-page.guard';
|
||||
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: 'create',
|
||||
component: CreateCommunityPageComponent,
|
||||
canActivate: [AuthenticatedGuard, CreateCommunityPageGuard]
|
||||
},
|
||||
{
|
||||
path: ':id/edit',
|
||||
pathMatch: 'full',
|
||||
component: EditCommunityPageComponent,
|
||||
canActivate: [AuthenticatedGuard],
|
||||
resolve: {
|
||||
dso: CommunityPageResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ':id/delete',
|
||||
pathMatch: 'full',
|
||||
component: DeleteCommunityPageComponent,
|
||||
canActivate: [AuthenticatedGuard],
|
||||
resolve: {
|
||||
dso: CommunityPageResolver
|
||||
}
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
component: CommunityPageComponent,
|
||||
@@ -19,6 +47,7 @@ import { CommunityPageResolver } from './community-page.resolver';
|
||||
],
|
||||
providers: [
|
||||
CommunityPageResolver,
|
||||
CreateCommunityPageGuard
|
||||
]
|
||||
})
|
||||
export class CommunityPageRoutingModule {
|
||||
|
@@ -30,6 +30,7 @@
|
||||
<ds-community-page-sub-collection-list [community]="communityPayload"></ds-community-page-sub-collection-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ds-error *ngIf="communityRD?.hasFailed" message="{{'error.community' | translate}}"></ds-error>
|
||||
<ds-loading *ngIf="communityRD?.isLoading" message="{{'loading.community' | translate}}"></ds-loading>
|
||||
</div>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import {mergeMap, filter, map} from 'rxjs/operators';
|
||||
import { mergeMap, filter, map } from 'rxjs/operators';
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@@ -45,5 +45,4 @@ export class CommunityPageComponent implements OnInit, OnDestroy {
|
||||
ngOnDestroy(): void {
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -7,6 +7,10 @@ import { CommunityPageComponent } from './community-page.component';
|
||||
import { CommunityPageSubCollectionListComponent } from './sub-collection-list/community-page-sub-collection-list.component';
|
||||
import { CommunityPageRoutingModule } from './community-page-routing.module';
|
||||
import {CommunityPageSubCommunityListComponent} from './sub-community-list/community-page-sub-community-list.component';
|
||||
import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component';
|
||||
import { CommunityFormComponent } from './community-form/community-form.component';
|
||||
import { EditCommunityPageComponent } from './edit-community-page/edit-community-page.component';
|
||||
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -18,8 +22,13 @@ import {CommunityPageSubCommunityListComponent} from './sub-community-list/commu
|
||||
CommunityPageComponent,
|
||||
CommunityPageSubCollectionListComponent,
|
||||
CommunityPageSubCommunityListComponent,
|
||||
CreateCommunityPageComponent,
|
||||
EditCommunityPageComponent,
|
||||
DeleteCommunityPageComponent,
|
||||
CommunityFormComponent
|
||||
]
|
||||
})
|
||||
|
||||
export class CommunityPageModule {
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,11 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 pb-4">
|
||||
<ng-container *ngVar="(parentRD$ | async)?.payload as parent">
|
||||
<h2 *ngIf="!parent" id="header" class="border-bottom pb-2">{{ 'community.create.head' | translate }}</h2>
|
||||
<h2 *ngIf="parent" id="sub-header" class="border-bottom pb-2">{{ 'community.create.sub-head' | translate:{ parent: parent.name } }}</h2>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<ds-community-form (submitForm)="onSubmit($event)"></ds-community-form>
|
||||
</div>
|
@@ -0,0 +1 @@
|
||||
|
@@ -0,0 +1,42 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { RouteService } from '../../shared/services/route.service';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { CreateCommunityPageComponent } from './create-community-page.component';
|
||||
|
||||
describe('CreateCommunityPageComponent', () => {
|
||||
let comp: CreateCommunityPageComponent;
|
||||
let fixture: ComponentFixture<CreateCommunityPageComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||
declarations: [CreateCommunityPageComponent],
|
||||
providers: [
|
||||
{ provide: CommunityDataService, useValue: { findById: () => observableOf({}) } },
|
||||
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } },
|
||||
{ provide: Router, useValue: {} },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CreateCommunityPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('frontendURL', () => {
|
||||
it('should have the right frontendURL set', () => {
|
||||
expect((comp as any).frontendURL).toEqual('/communities/');
|
||||
})
|
||||
});
|
||||
});
|
@@ -0,0 +1,27 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { RouteService } from '../../shared/services/route.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component';
|
||||
import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model';
|
||||
|
||||
/**
|
||||
* Component that represents the page where a user can create a new Community
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-create-community',
|
||||
styleUrls: ['./create-community-page.component.scss'],
|
||||
templateUrl: './create-community-page.component.html'
|
||||
})
|
||||
export class CreateCommunityPageComponent extends CreateComColPageComponent<Community, NormalizedCommunity> {
|
||||
protected frontendURL = '/communities/';
|
||||
|
||||
public constructor(
|
||||
protected communityDataService: CommunityDataService,
|
||||
protected routeService: RouteService,
|
||||
protected router: Router
|
||||
) {
|
||||
super(communityDataService, communityDataService, routeService, router);
|
||||
}
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
import { CreateCommunityPageGuard } from './create-community-page.guard';
|
||||
import { MockRouter } from '../../shared/mocks/mock-router';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
describe('CreateCommunityPageGuard', () => {
|
||||
describe('canActivate', () => {
|
||||
let guard: CreateCommunityPageGuard;
|
||||
let router;
|
||||
let communityDataServiceStub: any;
|
||||
|
||||
beforeEach(() => {
|
||||
communityDataServiceStub = {
|
||||
findById: (id: string) => {
|
||||
if (id === 'valid-id') {
|
||||
return observableOf(new RemoteData(false, false, true, null, new Community()));
|
||||
} else if (id === 'invalid-id') {
|
||||
return observableOf(new RemoteData(false, false, true, null, undefined));
|
||||
} else if (id === 'error-id') {
|
||||
return observableOf(new RemoteData(false, false, false, null, new Community()));
|
||||
}
|
||||
}
|
||||
};
|
||||
router = new MockRouter();
|
||||
|
||||
guard = new CreateCommunityPageGuard(router, communityDataServiceStub);
|
||||
});
|
||||
|
||||
it('should return true when the parent ID resolves to a community', () => {
|
||||
guard.canActivate({ queryParams: { parent: 'valid-id' } } as any, undefined)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(canActivate) =>
|
||||
expect(canActivate).toEqual(true)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return true when no parent ID has been provided', () => {
|
||||
guard.canActivate({ queryParams: { } } as any, undefined)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(canActivate) =>
|
||||
expect(canActivate).toEqual(true)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when the parent ID does not resolve to a community', () => {
|
||||
guard.canActivate({ queryParams: { parent: 'invalid-id' } } as any, undefined)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(canActivate) =>
|
||||
expect(canActivate).toEqual(false)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when the parent ID resolves to an error response', () => {
|
||||
guard.canActivate({ queryParams: { parent: 'error-id' } } as any, undefined)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(canActivate) =>
|
||||
expect(canActivate).toEqual(false)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,46 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
|
||||
|
||||
import { hasNoValue, hasValue } from '../../shared/empty.util';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { getFinishedRemoteData } from '../../core/shared/operators';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Prevent creation of a community with an invalid parent community provided
|
||||
* @class CreateCommunityPageGuard
|
||||
*/
|
||||
@Injectable()
|
||||
export class CreateCommunityPageGuard implements CanActivate {
|
||||
public constructor(private router: Router, private communityService: CommunityDataService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* True when either NO parent ID query parameter has been provided, or the parent ID resolves to a valid parent community
|
||||
* Reroutes to a 404 page when the page cannot be activated
|
||||
* @method canActivate
|
||||
*/
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||
const parentID = route.queryParams.parent;
|
||||
if (hasNoValue(parentID)) {
|
||||
return observableOf(true);
|
||||
}
|
||||
|
||||
const parent: Observable<RemoteData<Community>> = this.communityService.findById(parentID)
|
||||
.pipe(
|
||||
getFinishedRemoteData(),
|
||||
);
|
||||
|
||||
return parent.pipe(
|
||||
map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
|
||||
tap((isValid: boolean) => {
|
||||
if (!isValid) {
|
||||
this.router.navigate(['/404']);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<ng-container *ngVar="(dsoRD$ | async)?.payload as dso">
|
||||
<div class="col-12 pb-4">
|
||||
<h2 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate
|
||||
}}</h2>
|
||||
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p>
|
||||
<button class="btn btn-primary mr-2" (click)="onConfirm(dso)">
|
||||
{{'community.delete.confirm' |
|
||||
translate}}
|
||||
</button>
|
||||
<button class="btn btn-primary" (click)="onCancel(dso)">{{'community.delete.cancel' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
@@ -0,0 +1 @@
|
||||
|
@@ -0,0 +1,42 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { RouteService } from '../../shared/services/route.service';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { DeleteCommunityPageComponent } from './delete-community-page.component';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
|
||||
describe('DeleteCommunityPageComponent', () => {
|
||||
let comp: DeleteCommunityPageComponent;
|
||||
let fixture: ComponentFixture<DeleteCommunityPageComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||
declarations: [DeleteCommunityPageComponent],
|
||||
providers: [
|
||||
{ provide: CommunityDataService, useValue: {} },
|
||||
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
||||
{ provide: NotificationsService, useValue: {} },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DeleteCommunityPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('frontendURL', () => {
|
||||
it('should have the right frontendURL set', () => {
|
||||
expect((comp as any).frontendURL).toEqual('/communities/');
|
||||
})
|
||||
});
|
||||
});
|
@@ -0,0 +1,30 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model';
|
||||
import { DeleteComColPageComponent } from '../../shared/comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
/**
|
||||
* Component that represents the page where a user can delete an existing Community
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-delete-community',
|
||||
styleUrls: ['./delete-community-page.component.scss'],
|
||||
templateUrl: './delete-community-page.component.html'
|
||||
})
|
||||
export class DeleteCommunityPageComponent extends DeleteComColPageComponent<Community, NormalizedCommunity> {
|
||||
protected frontendURL = '/communities/';
|
||||
|
||||
public constructor(
|
||||
protected dsoDataService: CommunityDataService,
|
||||
protected router: Router,
|
||||
protected route: ActivatedRoute,
|
||||
protected notifications: NotificationsService,
|
||||
protected translate: TranslateService
|
||||
) {
|
||||
super(dsoDataService, router, route, notifications, translate);
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 pb-4">
|
||||
<h2 id="header" class="border-bottom pb-2">{{ 'community.edit.head' | translate }}</h2>
|
||||
<ds-community-form (submitForm)="onSubmit($event)"
|
||||
[dso]="(dsoRD$ | async)?.payload"></ds-community-form>
|
||||
<a class="btn btn-danger"
|
||||
[routerLink]="'/communities/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'community.edit.delete'
|
||||
| translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1 @@
|
||||
|
@@ -0,0 +1,39 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { EditCommunityPageComponent } from './edit-community-page.component';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
|
||||
describe('EditCommunityPageComponent', () => {
|
||||
let comp: EditCommunityPageComponent;
|
||||
let fixture: ComponentFixture<EditCommunityPageComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||
declarations: [EditCommunityPageComponent],
|
||||
providers: [
|
||||
{ provide: CommunityDataService, useValue: {} },
|
||||
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EditCommunityPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('frontendURL', () => {
|
||||
it('should have the right frontendURL set', () => {
|
||||
expect((comp as any).frontendURL).toEqual('/communities/');
|
||||
})
|
||||
});
|
||||
});
|
@@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model';
|
||||
import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
||||
|
||||
/**
|
||||
* Component that represents the page where a user can edit an existing Community
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-edit-community',
|
||||
styleUrls: ['./edit-community-page.component.scss'],
|
||||
templateUrl: './edit-community-page.component.html'
|
||||
})
|
||||
export class EditCommunityPageComponent extends EditComColPageComponent<Community, NormalizedCommunity> {
|
||||
protected frontendURL = '/communities/';
|
||||
|
||||
public constructor(
|
||||
protected communityDataService: CommunityDataService,
|
||||
protected router: Router,
|
||||
protected route: ActivatedRoute
|
||||
) {
|
||||
super(communityDataService, router, route);
|
||||
}
|
||||
}
|
@@ -17,7 +17,6 @@ describe('SubCommunityList Component', () => {
|
||||
let fixture: ComponentFixture<CommunityPageSubCommunityListComponent>;
|
||||
|
||||
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: [
|
||||
{
|
||||
|
@@ -17,6 +17,7 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [fadeInOut]
|
||||
})
|
||||
|
||||
export class TopLevelCommunityListComponent {
|
||||
communitiesRDObs: Observable<RemoteData<PaginatedList<Community>>>;
|
||||
config: PaginationComponentOptions;
|
||||
|
@@ -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<ItemDeleteComponent>;
|
||||
@@ -27,8 +27,6 @@ let routerStub;
|
||||
let mockItemDataService: ItemDataService;
|
||||
let routeStub;
|
||||
let notificationsServiceStub;
|
||||
let successfulRestResponse;
|
||||
let failRestResponse;
|
||||
|
||||
describe('ItemDeleteComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
@@ -46,14 +44,12 @@ describe('ItemDeleteComponent', () => {
|
||||
});
|
||||
|
||||
mockItemDataService = jasmine.createSpyObj('mockItemDataService', {
|
||||
delete: observableOf(new RestResponse(true, '200'))
|
||||
delete: observableOf(true)
|
||||
});
|
||||
|
||||
routeStub = {
|
||||
data: observableOf({
|
||||
item: new RemoteData(false, false, true, null, {
|
||||
id: 'fake-id'
|
||||
})
|
||||
item: new RemoteData(false, false, true, null, mockItem)
|
||||
})
|
||||
};
|
||||
|
||||
@@ -63,10 +59,10 @@ describe('ItemDeleteComponent', () => {
|
||||
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||
declarations: [ItemDeleteComponent],
|
||||
providers: [
|
||||
{provide: ActivatedRoute, useValue: routeStub},
|
||||
{provide: Router, useValue: routerStub},
|
||||
{provide: ItemDataService, useValue: mockItemDataService},
|
||||
{provide: NotificationsService, useValue: notificationsServiceStub},
|
||||
{ provide: ActivatedRoute, useValue: routeStub },
|
||||
{ provide: Router, useValue: routerStub },
|
||||
{ provide: ItemDataService, useValue: mockItemDataService },
|
||||
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
||||
], schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
]
|
||||
@@ -74,9 +70,6 @@ describe('ItemDeleteComponent', () => {
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
successfulRestResponse = new RestResponse(true, '200');
|
||||
failRestResponse = new RestResponse(false, '500');
|
||||
|
||||
fixture = TestBed.createComponent(ItemDeleteComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
@@ -95,22 +88,21 @@ describe('ItemDeleteComponent', () => {
|
||||
|
||||
describe('performAction', () => {
|
||||
it('should call delete function from the ItemDataService', () => {
|
||||
spyOn(comp, 'processRestResponse');
|
||||
spyOn(comp, 'notify');
|
||||
comp.performAction();
|
||||
|
||||
expect(mockItemDataService.delete).toHaveBeenCalledWith(mockItem.id);
|
||||
expect(comp.processRestResponse).toHaveBeenCalled();
|
||||
expect(mockItemDataService.delete).toHaveBeenCalledWith(mockItem);
|
||||
expect(comp.notify).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('processRestResponse', () => {
|
||||
describe('notify', () => {
|
||||
it('should navigate to the homepage on successful deletion of the item', () => {
|
||||
comp.processRestResponse(successfulRestResponse);
|
||||
comp.notify(true);
|
||||
expect(routerStub.navigate).toHaveBeenCalledWith(['']);
|
||||
});
|
||||
});
|
||||
describe('processRestResponse', () => {
|
||||
describe('notify', () => {
|
||||
it('should navigate to the item edit page on failed deletion of the item', () => {
|
||||
comp.processRestResponse(failRestResponse);
|
||||
comp.notify(false);
|
||||
expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditPath('fake-id')]);
|
||||
});
|
||||
});
|
||||
|
@@ -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',
|
||||
@@ -19,20 +19,19 @@ export class ItemDeleteComponent extends AbstractSimpleItemActionComponent {
|
||||
* Perform the delete action to the item
|
||||
*/
|
||||
performAction() {
|
||||
this.itemDataService.delete(this.item.id).pipe(first()).subscribe(
|
||||
(response: RestResponse) => {
|
||||
this.processRestResponse(response);
|
||||
this.itemDataService.delete(this.item).pipe(first()).subscribe(
|
||||
(succeeded: boolean) => {
|
||||
this.notify(succeeded);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the RestResponse retrieved from the server.
|
||||
* When the item is successfully delete, navigate to the homepage, otherwise navigate back to the item edit page
|
||||
* @param response
|
||||
*/
|
||||
processRestResponse(response: RestResponse) {
|
||||
if (response.isSuccessful) {
|
||||
notify(succeeded: boolean) {
|
||||
if (succeeded) {
|
||||
this.notificationsService.success(this.translateService.get('item.edit.' + this.messageKey + '.success'));
|
||||
this.router.navigate(['']);
|
||||
} else {
|
||||
|
@@ -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<ItemPrivateComponent>;
|
||||
|
@@ -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',
|
||||
|
@@ -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<ItemPublicComponent>;
|
||||
|
@@ -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',
|
||||
|
@@ -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<ItemReinstateComponent>;
|
||||
|
@@ -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',
|
||||
|
@@ -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<ItemWithdrawComponent>;
|
||||
|
@@ -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',
|
||||
|
@@ -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
|
||||
|
@@ -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.
|
||||
|
@@ -6,7 +6,7 @@ import {
|
||||
Subject,
|
||||
Subscription
|
||||
} from 'rxjs';
|
||||
import { switchMap, distinctUntilChanged, first, map } from 'rxjs/operators';
|
||||
import { switchMap, distinctUntilChanged, map, take } from 'rxjs/operators';
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
import { 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(
|
||||
|
@@ -1,5 +1,5 @@
|
||||
|
||||
import {first} 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';
|
||||
@@ -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 {
|
||||
|
@@ -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,
|
||||
|
@@ -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',
|
||||
|
@@ -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,11 +82,10 @@ describe('SearchService', () => {
|
||||
};
|
||||
|
||||
const remoteDataBuildService = {
|
||||
toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, responseCacheObs: Observable<ResponseCacheEntry>, payloadObs: Observable<any>) => {
|
||||
return observableCombineLatest(requestEntryObs,
|
||||
responseCacheObs, payloadObs).pipe(
|
||||
map(([req, res, pay]) => {
|
||||
return { req, res, pay };
|
||||
toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, payloadObs: Observable<any>) => {
|
||||
return observableCombineLatest(requestEntryObs, payloadObs).pipe(
|
||||
map(([req, pay]) => {
|
||||
return { req, 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
|
||||
@@ -183,19 +175,14 @@ 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', () => {
|
||||
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
|
||||
@@ -213,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', () => {
|
||||
@@ -224,10 +208,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
|
||||
@@ -245,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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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 { map, switchMap, tap } 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';
|
||||
@@ -68,7 +70,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,
|
||||
@@ -98,16 +99,12 @@ export class SearchService implements OnDestroy {
|
||||
configureRequest(this.requestService)
|
||||
);
|
||||
const requestEntryObs = requestObs.pipe(
|
||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||
);
|
||||
|
||||
const responseCacheObs = requestObs.pipe(
|
||||
flatMap((request: RestRequest) => this.responseCache.get(request.href))
|
||||
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||
);
|
||||
|
||||
// get search results from response cache
|
||||
const sqrObs: Observable<SearchQueryResponse> = responseCacheObs.pipe(
|
||||
map((entry: ResponseCacheEntry) => entry.response),
|
||||
const sqrObs: Observable<SearchQueryResponse> = requestEntryObs.pipe(
|
||||
getResponseFromEntry(),
|
||||
map((response: SearchSuccessResponse) => response.results)
|
||||
);
|
||||
|
||||
@@ -115,10 +112,11 @@ export class SearchService implements OnDestroy {
|
||||
// Turn list of observable remote data DSO's into observable remote data object with list of DSO
|
||||
const dsoObs: Observable<RemoteData<DSpaceObject[]>> = 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<Observable<RemoteData<DSpaceObject>>>) => this.rdb.aggregate(input))
|
||||
switchMap((input: Array<Observable<RemoteData<DSpaceObject>>>) => this.rdb.aggregate(input)),
|
||||
);
|
||||
|
||||
// Create search results again with the correct dso objects linked to each result
|
||||
@@ -139,8 +137,8 @@ export class SearchService implements OnDestroy {
|
||||
})
|
||||
);
|
||||
|
||||
const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe(
|
||||
map((entry: ResponseCacheEntry) => entry.response),
|
||||
const pageInfoObs: Observable<PageInfo> = 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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -182,21 +180,17 @@ export class SearchService implements OnDestroy {
|
||||
);
|
||||
|
||||
const requestEntryObs = requestObs.pipe(
|
||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||
);
|
||||
|
||||
const responseCacheObs = requestObs.pipe(
|
||||
flatMap((request: RestRequest) => this.responseCache.get(request.href))
|
||||
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||
);
|
||||
|
||||
// get search results from response cache
|
||||
const facetConfigObs: Observable<SearchFilterConfig[]> = responseCacheObs.pipe(
|
||||
map((entry: ResponseCacheEntry) => entry.response),
|
||||
const facetConfigObs: Observable<SearchFilterConfig[]> = 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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,21 +223,17 @@ export class SearchService implements OnDestroy {
|
||||
);
|
||||
|
||||
const requestEntryObs = requestObs.pipe(
|
||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||
);
|
||||
|
||||
const responseCacheObs = requestObs.pipe(
|
||||
flatMap((request: RestRequest) => this.responseCache.get(request.href))
|
||||
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||
);
|
||||
|
||||
// get search results from response cache
|
||||
const facetValueObs: Observable<FacetValue[]> = responseCacheObs.pipe(
|
||||
map((entry: ResponseCacheEntry) => entry.response),
|
||||
const facetValueObs: Observable<FacetValue[]> = requestEntryObs.pipe(
|
||||
getResponseFromEntry(),
|
||||
map((response: FacetValueSuccessResponse) => response.results)
|
||||
);
|
||||
|
||||
const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe(
|
||||
map((entry: ResponseCacheEntry) => entry.response),
|
||||
const pageInfoObs: Observable<PageInfo> = 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { filter, first, map, take } from 'rxjs/operators';
|
||||
import { filter, map, take } from 'rxjs/operators';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
@@ -91,7 +91,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());
|
||||
this.sidebarVisible = this.menuService.isMenuVisible(MenuID.ADMIN);
|
||||
|
@@ -2,15 +2,15 @@ import { Observable, of as observableOf, throwError as observableThrowError } fr
|
||||
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
|
||||
import { 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,15 +19,12 @@ export class AuthRequestService {
|
||||
|
||||
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||
protected halService: HALEndpointService,
|
||||
protected responseCache: ResponseCacheService,
|
||||
protected requestService: RequestService) {
|
||||
}
|
||||
|
||||
protected fetchRequest(request: RestRequest): Observable<any> {
|
||||
return this.responseCache.get(request.href).pipe(
|
||||
map((entry: ResponseCacheEntry) => entry.response),
|
||||
// TODO to review when https://github.com/DSpace/dspace-angular/issues/217 will be fixed
|
||||
tap(() => this.responseCache.remove(request.href)),
|
||||
return this.requestService.getByUUID(request.uuid).pipe(
|
||||
getResponseFromEntry(),
|
||||
mergeMap((response) => {
|
||||
if (response.isSuccessful && isNotEmpty(response)) {
|
||||
return observableOf((response as AuthStatusResponse).response);
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { AuthStatusResponse } from '../cache/response-cache.models';
|
||||
import { AuthStatusResponse } from '../cache/response.models';
|
||||
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { 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<ObjectCacheState>({});
|
||||
const objectCacheService = new ObjectCacheService(store as any);
|
||||
|
||||
|
@@ -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';
|
||||
@@ -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<NormalizedAuthStatus, AuthType>(data.payload, request.href);
|
||||
const response = this.process<NormalizedAuthStatus, AuthType>(data.payload, request.uuid);
|
||||
return new AuthStatusResponse(response, data.statusCode);
|
||||
} else {
|
||||
return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode);
|
||||
|
@@ -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
|
||||
@@ -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())
|
||||
|
@@ -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');
|
||||
});
|
||||
|
@@ -277,7 +277,7 @@ export class AuthService {
|
||||
public isTokenExpiring(): Observable<boolean> {
|
||||
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)) {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { first, map, switchMap } from 'rxjs/operators';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
@@ -45,7 +45,7 @@ export class ServerAuthService extends AuthService {
|
||||
} else {
|
||||
throw(new Error('Not authenticated'));
|
||||
}
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,7 +60,7 @@ export class ServerAuthService extends AuthService {
|
||||
*/
|
||||
public redirectToPreviousUrl() {
|
||||
this.getRedirectUrl().pipe(
|
||||
first())
|
||||
take(1))
|
||||
.subscribe((redirectUrl) => {
|
||||
if (isNotEmpty(redirectUrl)) {
|
||||
// override the route reuse strategy
|
||||
|
@@ -2,20 +2,19 @@ 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';
|
||||
import { BrowseService } from './browse.service';
|
||||
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
|
||||
import { RequestEntry } from '../data/request.reducer';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
describe('BrowseService', () => {
|
||||
let scheduler: TestScheduler;
|
||||
let service: BrowseService;
|
||||
let responseCache: ResponseCacheService;
|
||||
let requestService: RequestService;
|
||||
let rdbService: RemoteDataBuildService;
|
||||
|
||||
@@ -80,22 +79,14 @@ describe('BrowseService', () => {
|
||||
})
|
||||
];
|
||||
|
||||
function initMockResponseCacheService(isSuccessful: boolean) {
|
||||
const rcs = getMockResponseCacheService();
|
||||
(rcs.get as any).and.returnValue(cold('b-', {
|
||||
b: {
|
||||
response: {
|
||||
isSuccessful,
|
||||
payload: browseDefinitions,
|
||||
}
|
||||
}
|
||||
}));
|
||||
return rcs;
|
||||
}
|
||||
const getRequestEntry$ = (successful: boolean) => {
|
||||
return observableOf({
|
||||
response: { isSuccessful: successful, payload: browseDefinitions } as any
|
||||
} as RequestEntry)
|
||||
};
|
||||
|
||||
function initTestService() {
|
||||
return new BrowseService(
|
||||
responseCache,
|
||||
requestService,
|
||||
halService,
|
||||
rdbService
|
||||
@@ -109,8 +100,7 @@ describe('BrowseService', () => {
|
||||
describe('getBrowseDefinitions', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
responseCache = initMockResponseCacheService(true);
|
||||
requestService = getMockRequestService();
|
||||
requestService = getMockRequestService(getRequestEntry$(true));
|
||||
rdbService = getMockRemoteDataBuildService();
|
||||
service = initTestService();
|
||||
spyOn(halService, 'getEndpoint').and
|
||||
@@ -148,8 +138,7 @@ describe('BrowseService', () => {
|
||||
const mockAuthorName = 'Donald Smith';
|
||||
|
||||
beforeEach(() => {
|
||||
responseCache = initMockResponseCacheService(true);
|
||||
requestService = getMockRequestService();
|
||||
requestService = getMockRequestService(getRequestEntry$(true));
|
||||
rdbService = getMockRemoteDataBuildService();
|
||||
service = initTestService();
|
||||
spyOn(service, 'getBrowseDefinitions').and
|
||||
@@ -222,8 +211,7 @@ describe('BrowseService', () => {
|
||||
|
||||
describe('if getBrowseDefinitions fires', () => {
|
||||
beforeEach(() => {
|
||||
responseCache = initMockResponseCacheService(true);
|
||||
requestService = getMockRequestService();
|
||||
requestService = getMockRequestService(getRequestEntry$(true));
|
||||
rdbService = getMockRemoteDataBuildService();
|
||||
service = initTestService();
|
||||
spyOn(service, 'getBrowseDefinitions').and
|
||||
@@ -278,8 +266,7 @@ describe('BrowseService', () => {
|
||||
|
||||
describe('if getBrowseDefinitions doesn\'t fire', () => {
|
||||
it('should return undefined', () => {
|
||||
responseCache = initMockResponseCacheService(true);
|
||||
requestService = getMockRequestService();
|
||||
requestService = getMockRequestService(getRequestEntry$(true));
|
||||
rdbService = getMockRemoteDataBuildService();
|
||||
service = initTestService();
|
||||
spyOn(service, 'getBrowseDefinitions').and
|
||||
|
@@ -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,10 +26,9 @@ import { BrowseEntry } from '../shared/browse-entry.model';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import {
|
||||
configureRequest,
|
||||
filterSuccessfulResponses, getBrowseDefinitionLinks,
|
||||
getRemoteDataPayload,
|
||||
getRequestFromSelflink,
|
||||
getResponseFromSelflink
|
||||
filterSuccessfulResponses,
|
||||
getBrowseDefinitionLinks,
|
||||
getRemoteDataPayload, getRequestFromRequestHref
|
||||
} from '../shared/operators';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import { Item } from '../shared/item.model';
|
||||
@@ -57,7 +53,6 @@ export class BrowseService {
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected responseCache: ResponseCacheService,
|
||||
protected requestService: RequestService,
|
||||
protected halService: HALEndpointService,
|
||||
private rdb: RemoteDataBuildService,
|
||||
@@ -73,11 +68,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 requestEntry$ = href$.pipe(getRequestFromRequestHref(this.requestService));
|
||||
const payload$ = requestEntry$.pipe(
|
||||
filterSuccessfulResponses(),
|
||||
map((entry: ResponseCacheEntry) => entry.response),
|
||||
map((response: GenericSuccessResponse<BrowseDefinition[]>) => response.payload),
|
||||
ensureArrayHasValue(),
|
||||
map((definitions: BrowseDefinition[]) => definitions
|
||||
@@ -85,7 +78,7 @@ export class BrowseService {
|
||||
distinctUntilChanged()
|
||||
);
|
||||
|
||||
return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
||||
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
|
||||
}
|
||||
|
||||
getBrowseEntriesFor(options: BrowseEntrySearchOptions): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
|
||||
@@ -118,12 +111,10 @@ 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 requestEntry$ = href$.pipe(getRequestFromRequestHref(this.requestService));
|
||||
|
||||
const payload$ = responseCache$.pipe(
|
||||
const payload$ = requestEntry$.pipe(
|
||||
filterSuccessfulResponses(),
|
||||
map((entry: ResponseCacheEntry) => entry.response),
|
||||
map((response: GenericSuccessResponse<BrowseEntry[]>) => new PaginatedList(response.pageInfo, response.payload)),
|
||||
map((list: PaginatedList<BrowseEntry>) => Object.assign(list, {
|
||||
page: list.page ? list.page.map((entry: BrowseEntry) => Object.assign(new BrowseEntry(), entry)) : list.page
|
||||
@@ -131,7 +122,7 @@ export class BrowseService {
|
||||
distinctUntilChanged()
|
||||
);
|
||||
|
||||
return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
||||
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,12 +166,10 @@ 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 requestEntry$ = href$.pipe(getRequestFromRequestHref(this.requestService));
|
||||
|
||||
const payload$ = responseCache$.pipe(
|
||||
const payload$ = requestEntry$.pipe(
|
||||
filterSuccessfulResponses(),
|
||||
map((entry: ResponseCacheEntry) => entry.response),
|
||||
map((response: GenericSuccessResponse<Item[]>) => new PaginatedList(response.pageInfo, response.payload)),
|
||||
map((list: PaginatedList<Item>) => Object.assign(list, {
|
||||
page: list.page ? list.page.map((item: DSpaceObject) => Object.assign(new Item(), item)) : list.page
|
||||
@@ -188,7 +177,7 @@ export class BrowseService {
|
||||
distinctUntilChanged()
|
||||
);
|
||||
|
||||
return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
||||
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
|
||||
}
|
||||
|
||||
getBrowseURLFor(metadatumKey: string, linkPath: string): Observable<string> {
|
||||
|
50
src/app/core/cache/builders/normalized-object-build.service.ts
vendored
Normal file
50
src/app/core/cache/builders/normalized-object-build.service.ts
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { NormalizedObject } from '../models/normalized-object.model';
|
||||
import { CacheableObject } from '../object-cache.reducer';
|
||||
import { getRelationships } from './build-decorators';
|
||||
import { NormalizedObjectFactory } from '../models/normalized-object-factory';
|
||||
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* Return true if halObj has a value for `_links.self`
|
||||
*
|
||||
* @param {any} halObj The object to test
|
||||
*/
|
||||
export function isRestDataObject(halObj: any): boolean {
|
||||
return isNotEmpty(halObj._links) && hasValue(halObj._links.self);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if halObj has a value for `page` and `_embedded`
|
||||
*
|
||||
* @param {any} halObj The object to test
|
||||
*/
|
||||
export function isRestPaginatedList(halObj: any): boolean {
|
||||
return hasValue(halObj.page) && hasValue(halObj._embedded);
|
||||
}
|
||||
|
||||
/**
|
||||
* A service to turn domain models in to their normalized
|
||||
* counterparts.
|
||||
*/
|
||||
@Injectable()
|
||||
export class NormalizedObjectBuildService {
|
||||
|
||||
/**
|
||||
* Returns the normalized model that corresponds to the given domain model
|
||||
*
|
||||
* @param {TDomain} domainModel a domain model
|
||||
*/
|
||||
normalize<TDomain extends CacheableObject, TNormalized extends NormalizedObject>(domainModel: TDomain): TNormalized {
|
||||
const normalizedConstructor = NormalizedObjectFactory.getConstructor(domainModel.type);
|
||||
const relationships = getRelationships(normalizedConstructor) || [];
|
||||
|
||||
const normalizedModel = Object.assign({}, domainModel) as any;
|
||||
relationships.forEach((key: string) => {
|
||||
if (hasValue(domainModel[key])) {
|
||||
domainModel[key] = undefined;
|
||||
}
|
||||
});
|
||||
return normalizedModel;
|
||||
}
|
||||
}
|
@@ -32,7 +32,7 @@ describe('RemoteDataBuildService', () => {
|
||||
let service: RemoteDataBuildService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new RemoteDataBuildService(undefined, undefined, undefined);
|
||||
service = new RemoteDataBuildService(undefined, undefined);
|
||||
});
|
||||
|
||||
describe('when toPaginatedList is called', () => {
|
||||
|
@@ -5,7 +5,15 @@ import {
|
||||
race as observableRace
|
||||
} from 'rxjs';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { distinctUntilChanged, flatMap, map, startWith } from 'rxjs/operators';
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
first,
|
||||
flatMap,
|
||||
map,
|
||||
startWith,
|
||||
switchMap,
|
||||
take
|
||||
} from 'rxjs/operators';
|
||||
import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||
import { PaginatedList } from '../../data/paginated-list';
|
||||
import { RemoteData } from '../../data/remote-data';
|
||||
@@ -16,22 +24,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
|
||||
getRequestFromRequestHref, getRequestFromRequestUUID,
|
||||
getResourceLinksFromResponse
|
||||
} from '../../shared/operators';
|
||||
|
||||
@Injectable()
|
||||
export class RemoteDataBuildService {
|
||||
constructor(protected objectCache: ObjectCacheService,
|
||||
protected responseCache: ResponseCacheService,
|
||||
protected requestService: RequestService) {
|
||||
}
|
||||
|
||||
@@ -39,29 +43,24 @@ export class RemoteDataBuildService {
|
||||
if (typeof href$ === 'string') {
|
||||
href$ = observableOf(href$);
|
||||
}
|
||||
const requestHref$ = href$.pipe(flatMap((href: string) =>
|
||||
this.objectCache.getRequestHrefBySelfLink(href)));
|
||||
const requestUUID$ = href$.pipe(
|
||||
switchMap((href: string) =>
|
||||
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)),
|
||||
);
|
||||
|
||||
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(
|
||||
href$.pipe(
|
||||
flatMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href)),
|
||||
startWith(undefined)
|
||||
),
|
||||
responseCache$.pipe(
|
||||
switchMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href)),
|
||||
startWith(undefined)),
|
||||
requestEntry$.pipe(
|
||||
getResourceLinksFromResponse(),
|
||||
flatMap((resourceSelfLinks: string[]) => {
|
||||
switchMap((resourceSelfLinks: string[]) => {
|
||||
if (isNotEmpty(resourceSelfLinks)) {
|
||||
return this.objectCache.getBySelfLink(resourceSelfLinks[0]);
|
||||
} else {
|
||||
@@ -86,21 +85,21 @@ export class RemoteDataBuildService {
|
||||
startWith(undefined),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
return this.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
||||
return this.toRemoteDataObservable(requestEntry$, payload$);
|
||||
}
|
||||
|
||||
toRemoteDataObservable<T>(requestEntry$: Observable<RequestEntry>, responseCache$: Observable<ResponseCacheEntry>, payload$: Observable<T>) {
|
||||
return observableCombineLatest(requestEntry$, responseCache$.pipe(startWith(undefined)), payload$).pipe(
|
||||
map(([reqEntry, resEntry, payload]) => {
|
||||
toRemoteDataObservable<T>(requestEntry$: Observable<RequestEntry>, payload$: Observable<T>) {
|
||||
return observableCombineLatest(requestEntry$, 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(
|
||||
@@ -119,10 +118,8 @@ export class RemoteDataBuildService {
|
||||
href$ = observableOf(href$);
|
||||
}
|
||||
|
||||
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
|
||||
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
|
||||
|
||||
const tDomainList$ = responseCache$.pipe(
|
||||
const requestEntry$ = href$.pipe(getRequestFromRequestHref(this.requestService));
|
||||
const tDomainList$ = requestEntry$.pipe(
|
||||
getResourceLinksFromResponse(),
|
||||
flatMap((resourceUUIDs: string[]) => {
|
||||
return this.objectCache.getList(resourceUUIDs).pipe(
|
||||
@@ -133,14 +130,13 @@ export class RemoteDataBuildService {
|
||||
}));
|
||||
}),
|
||||
startWith([]),
|
||||
distinctUntilChanged()
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
const pageInfo$ = responseCache$.pipe(
|
||||
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,12 +152,11 @@ export class RemoteDataBuildService {
|
||||
})
|
||||
);
|
||||
|
||||
return this.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
||||
return this.toRemoteDataObservable(requestEntry$, payload$);
|
||||
}
|
||||
|
||||
build<TNormalized, TDomain>(normalized: TNormalized): TDomain {
|
||||
const links: any = {};
|
||||
|
||||
const relationships = getRelationships(normalized.constructor) || [];
|
||||
|
||||
relationships.forEach((relationship: string) => {
|
||||
@@ -204,7 +199,6 @@ export class RemoteDataBuildService {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const domainModel = getMapsTo(normalized.constructor);
|
||||
return Object.assign(new domainModel(), normalized, links);
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { autoserialize, inheritSerialization } from 'cerialize';
|
||||
import { autoserialize, deserialize, inheritSerialization, serialize } from 'cerialize';
|
||||
|
||||
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
|
||||
import { 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[];
|
||||
|
||||
|
@@ -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';
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
@@ -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
|
||||
}
|
||||
|
65
src/app/core/cache/object-cache.actions.ts
vendored
65
src/app/core/cache/object-cache.actions.ts
vendored
@@ -2,6 +2,7 @@ import { Action } from '@ngrx/store';
|
||||
|
||||
import { type } from '../../shared/ngrx/type';
|
||||
import { CacheableObject } from './object-cache.reducer';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
|
||||
/**
|
||||
* The list of ObjectCacheAction type definitions
|
||||
@@ -9,7 +10,9 @@ 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'),
|
||||
ADD_PATCH: type('dspace/core/cache/object/ADD_PATCH'),
|
||||
APPLY_PATCH: type('dspace/core/cache/object/APPLY_PATCH')
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
@@ -22,7 +25,7 @@ export class AddToObjectCacheAction implements Action {
|
||||
objectToCache: CacheableObject;
|
||||
timeAdded: number;
|
||||
msToLive: number;
|
||||
requestHref: string;
|
||||
requestUUID: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -39,8 +42,8 @@ export class AddToObjectCacheAction implements Action {
|
||||
* This isn't necessarily the same as the object's self
|
||||
* 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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +82,48 @@ export class ResetObjectCacheTimestampsAction implements Action {
|
||||
this.payload = newTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An ngrx action to add new operations to a specified cached object
|
||||
*/
|
||||
export class AddPatchObjectCacheAction implements Action {
|
||||
type = ObjectCacheActionTypes.ADD_PATCH;
|
||||
payload: {
|
||||
href: string,
|
||||
operations: Operation[]
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new AddPatchObjectCacheAction
|
||||
*
|
||||
* @param href
|
||||
* the unique href of the object that should be updated
|
||||
* @param operations
|
||||
* the list of operations to add
|
||||
*/
|
||||
constructor(href: string, operations: Operation[]) {
|
||||
this.payload = { href, operations };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An ngrx action to apply all existing operations to a specified cached object
|
||||
*/
|
||||
export class ApplyPatchObjectCacheAction implements Action {
|
||||
type = ObjectCacheActionTypes.APPLY_PATCH;
|
||||
payload: string;
|
||||
|
||||
/**
|
||||
* Create a new ApplyPatchObjectCacheAction
|
||||
*
|
||||
* @param href
|
||||
* the unique href of the object that should be updated
|
||||
*/
|
||||
constructor(href: string) {
|
||||
this.payload = href;
|
||||
}
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
@@ -87,4 +132,6 @@ export class ResetObjectCacheTimestampsAction implements Action {
|
||||
export type ObjectCacheAction
|
||||
= AddToObjectCacheAction
|
||||
| RemoveFromObjectCacheAction
|
||||
| ResetObjectCacheTimestampsAction;
|
||||
| ResetObjectCacheTimestampsAction
|
||||
| AddPatchObjectCacheAction
|
||||
| ApplyPatchObjectCacheAction;
|
||||
|
59
src/app/core/cache/object-cache.reducer.spec.ts
vendored
59
src/app/core/cache/object-cache.reducer.spec.ts
vendored
@@ -2,9 +2,13 @@ import * as deepFreeze from 'deep-freeze';
|
||||
|
||||
import { objectCacheReducer } from './object-cache.reducer';
|
||||
import {
|
||||
AddPatchObjectCacheAction,
|
||||
AddToObjectCacheAction,
|
||||
RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction
|
||||
ApplyPatchObjectCacheAction,
|
||||
RemoveFromObjectCacheAction,
|
||||
ResetObjectCacheTimestampsAction
|
||||
} from './object-cache.actions';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
|
||||
class NullAction extends RemoveFromObjectCacheAction {
|
||||
type = null;
|
||||
@@ -16,8 +20,11 @@ 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';
|
||||
const testState = {
|
||||
[selfLink1]: {
|
||||
data: {
|
||||
@@ -26,16 +33,20 @@ 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
|
||||
}
|
||||
};
|
||||
deepFreeze(testState);
|
||||
@@ -59,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);
|
||||
@@ -72,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 */
|
||||
@@ -87,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);
|
||||
@@ -132,4 +143,32 @@ describe('objectCacheReducer', () => {
|
||||
objectCacheReducer(testState, action);
|
||||
});
|
||||
|
||||
it('should perform the ADD_PATCH action without affecting the previous state', () => {
|
||||
const action = new AddPatchObjectCacheAction(selfLink1, [{
|
||||
op: 'replace',
|
||||
path: '/name',
|
||||
value: 'random string'
|
||||
}]);
|
||||
// testState has already been frozen above
|
||||
objectCacheReducer(testState, action);
|
||||
});
|
||||
|
||||
it('should when the ADD_PATCH action dispatched', () => {
|
||||
const patch = [{ op: 'add', path: '/name', value: newName } as Operation];
|
||||
const action = new AddPatchObjectCacheAction(selfLink1, patch);
|
||||
const newState = objectCacheReducer(testState, action);
|
||||
expect(newState[selfLink1].patches.map((p) => p.operations)).toContain(patch);
|
||||
});
|
||||
|
||||
it('should when the APPLY_PATCH action dispatched', () => {
|
||||
const patch = [{ op: 'add', path: '/name', value: newName } as Operation];
|
||||
const addPatchAction = new AddPatchObjectCacheAction(selfLink1, patch);
|
||||
const stateWithPatch = objectCacheReducer(testState, addPatchAction);
|
||||
|
||||
const action = new ApplyPatchObjectCacheAction(selfLink1);
|
||||
const newState = objectCacheReducer(stateWithPatch, action);
|
||||
expect(newState[selfLink1].patches).toEqual([]);
|
||||
expect((newState[selfLink1].data as any).name).toEqual(newName);
|
||||
});
|
||||
|
||||
});
|
||||
|
89
src/app/core/cache/object-cache.reducer.ts
vendored
89
src/app/core/cache/object-cache.reducer.ts
vendored
@@ -1,10 +1,15 @@
|
||||
import {
|
||||
ObjectCacheAction, ObjectCacheActionTypes, AddToObjectCacheAction,
|
||||
RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction
|
||||
ObjectCacheAction,
|
||||
ObjectCacheActionTypes,
|
||||
AddToObjectCacheAction,
|
||||
RemoveFromObjectCacheAction,
|
||||
ResetObjectCacheTimestampsAction,
|
||||
AddPatchObjectCacheAction, ApplyPatchObjectCacheAction
|
||||
} 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 { applyPatch, Operation } from 'fast-json-patch';
|
||||
|
||||
export enum DirtyType {
|
||||
Created = 'Created',
|
||||
@@ -12,6 +17,21 @@ 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[];
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface to represent objects that can be cached
|
||||
*
|
||||
@@ -35,7 +55,9 @@ export class ObjectCacheEntry implements CacheEntry {
|
||||
data: CacheableObject;
|
||||
timeAdded: number;
|
||||
msToLive: number;
|
||||
requestHref: string;
|
||||
requestUUID: string;
|
||||
patches: Patch[] = [];
|
||||
isDirty: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,6 +98,14 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi
|
||||
return resetObjectCacheTimestamps(state, action as ResetObjectCacheTimestampsAction)
|
||||
}
|
||||
|
||||
case ObjectCacheActionTypes.ADD_PATCH: {
|
||||
return addPatchObjectCache(state, action as AddPatchObjectCacheAction);
|
||||
}
|
||||
|
||||
case ObjectCacheActionTypes.APPLY_PATCH: {
|
||||
return applyPatchObjectCache(state, action as ApplyPatchObjectCacheAction);
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
@@ -93,12 +123,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
|
||||
requestUUID: action.payload.requestUUID,
|
||||
isDirty: (hasValue(existing) ? isNotEmpty(existing.patches) : false),
|
||||
patches: (hasValue(existing) ? existing.patches : [])
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -143,3 +176,49 @@ function resetObjectCacheTimestamps(state: ObjectCacheState, action: ResetObject
|
||||
});
|
||||
return newState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the list of patch operations to a cached object
|
||||
*
|
||||
* @param state
|
||||
* the current state
|
||||
* @param action
|
||||
* an AddPatchObjectCacheAction
|
||||
* @return ObjectCacheState
|
||||
* the new state, with the new operations added to the state of the specified ObjectCacheEntry
|
||||
*/
|
||||
function addPatchObjectCache(state: ObjectCacheState, action: AddPatchObjectCacheAction): ObjectCacheState {
|
||||
const uuid = action.payload.href;
|
||||
const operations = action.payload.operations;
|
||||
const newState = Object.assign({}, state);
|
||||
if (hasValue(newState[uuid])) {
|
||||
const patches = newState[uuid].patches;
|
||||
newState[uuid] = Object.assign({}, newState[uuid], {
|
||||
patches: [...patches, { operations } as Patch],
|
||||
isDirty: true
|
||||
});
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the list of patch operations to a cached object
|
||||
*
|
||||
* @param state
|
||||
* the current state
|
||||
* @param action
|
||||
* an ApplyPatchObjectCacheAction
|
||||
* @return ObjectCacheState
|
||||
* the new state, with the new operations applied to the state of the specified ObjectCacheEntry
|
||||
*/
|
||||
function applyPatchObjectCache(state: ObjectCacheState, action: ApplyPatchObjectCacheAction): ObjectCacheState {
|
||||
const uuid = action.payload;
|
||||
const newState = Object.assign({}, state);
|
||||
if (hasValue(newState[uuid])) {
|
||||
// flatten two dimensional array
|
||||
const flatPatch: Operation[] = [].concat(...newState[uuid].patches.map((patch) => patch.operations));
|
||||
const newData = applyPatch(newState[uuid].data, flatPatch, undefined, false);
|
||||
newState[uuid] = Object.assign({}, newState[uuid], { data: newData.newDocument, patches: [] });
|
||||
}
|
||||
return newState;
|
||||
}
|
||||
|
58
src/app/core/cache/object-cache.service.spec.ts
vendored
58
src/app/core/cache/object-cache.service.spec.ts
vendored
@@ -2,32 +2,52 @@ import { Store } from '@ngrx/store';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { 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;
|
||||
let store: Store<CoreState>;
|
||||
|
||||
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;
|
||||
const objectToCache = {
|
||||
let objectToCache = {
|
||||
self: selfLink,
|
||||
type: ResourceType.Item
|
||||
};
|
||||
const cacheEntry = {
|
||||
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
|
||||
};
|
||||
const invalidCacheEntry = Object.assign({}, cacheEntry, { msToLive: -1 });
|
||||
invalidCacheEntry = Object.assign({}, cacheEntry, { msToLive: -1 })
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
init();
|
||||
store = new Store<CoreState>(undefined, undefined, undefined);
|
||||
spyOn(store, 'dispatch');
|
||||
service = new ObjectCacheService(store);
|
||||
@@ -39,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));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,4 +147,30 @@ describe('ObjectCacheService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('patch methods', () => {
|
||||
it('should dispatch the correct actions when addPatch is called', () => {
|
||||
service.addPatch(selfLink, operations);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new AddPatchObjectCacheAction(selfLink, operations));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new AddToSSBAction(selfLink, RestRequestMethod.PATCH));
|
||||
});
|
||||
|
||||
it('isDirty should return true when the patches list in the cache entry is not empty', () => {
|
||||
cacheEntry.patches = [
|
||||
{
|
||||
operations: operations
|
||||
} as Patch];
|
||||
const result = (service as any).isDirty(cacheEntry);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('isDirty should return false when the patches list in the cache entry is empty', () => {
|
||||
cacheEntry.patches = [];
|
||||
const result = (service as any).isDirty(cacheEntry);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
it('should dispatch the correct actions when applyPatchesToCachedObject is called', () => {
|
||||
(service as any).applyPatchesToCachedObject(selfLink);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new ApplyPatchObjectCacheAction(selfLink));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
94
src/app/core/cache/object-cache.service.ts
vendored
94
src/app/core/cache/object-cache.service.ts
vendored
@@ -1,25 +1,33 @@
|
||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
|
||||
import { 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';
|
||||
|
||||
import { CacheableObject, ObjectCacheEntry } from './object-cache.reducer';
|
||||
import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions';
|
||||
import { hasNoValue } from '../../shared/empty.util';
|
||||
import {
|
||||
AddPatchObjectCacheAction,
|
||||
AddToObjectCacheAction,
|
||||
ApplyPatchObjectCacheAction,
|
||||
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';
|
||||
import { AddToSSBAction } from './server-sync-buffer.actions';
|
||||
import { RestRequestMethod } from '../data/rest-request-method';
|
||||
|
||||
function selfLinkFromUuidSelector(uuid: string): MemoizedSelector<CoreState, string> {
|
||||
return pathSelector<CoreState, string>(coreSelector, 'index', IndexName.OBJECT, uuid);
|
||||
}
|
||||
|
||||
function entryFromSelfLinkSelector(selfLink: string): MemoizedSelector<CoreState, ObjectCacheEntry> {
|
||||
return pathSelector<CoreState, ObjectCacheEntry>(coreSelector, 'data/object', selfLink);
|
||||
return pathSelector<CoreState, ObjectCacheEntry>(coreSelector, 'cache/object', selfLink);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,20 +45,18 @@ export class ObjectCacheService {
|
||||
* The object to add
|
||||
* @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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
@@ -82,10 +88,21 @@ export class ObjectCacheService {
|
||||
|
||||
getBySelfLink<T extends NormalizedObject>(selfLink: string): Observable<T> {
|
||||
return this.getEntry(selfLink).pipe(
|
||||
map((entry: ObjectCacheEntry) => {
|
||||
if (isNotEmpty(entry.patches)) {
|
||||
const flatPatch: Operation[] = [].concat(...entry.patches.map((patch) => patch.operations));
|
||||
const patchedData = applyPatch(entry.data, flatPatch, undefined, false).newDocument;
|
||||
return Object.assign({}, entry, { data: patchedData });
|
||||
} else {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
),
|
||||
map((entry: ObjectCacheEntry) => {
|
||||
const type: GenericConstructor<NormalizedObject> = NormalizedObjectFactory.getConstructor(entry.data.type);
|
||||
return Object.assign(new type(), entry.data) as T
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private getEntry(selfLink: string): Observable<ObjectCacheEntry> {
|
||||
@@ -96,16 +113,16 @@ export class ObjectCacheService {
|
||||
);
|
||||
}
|
||||
|
||||
getRequestHrefBySelfLink(selfLink: string): Observable<string> {
|
||||
getRequestUUIDBySelfLink(selfLink: string): Observable<string> {
|
||||
return this.getEntry(selfLink).pipe(
|
||||
map((entry: ObjectCacheEntry) => entry.requestHref),
|
||||
distinctUntilChanged(),);
|
||||
map((entry: ObjectCacheEntry) => entry.requestUUID),
|
||||
distinctUntilChanged());
|
||||
}
|
||||
|
||||
getRequestHrefByUUID(uuid: string): Observable<string> {
|
||||
getRequestUUIDByObjectUUID(uuid: string): Observable<string> {
|
||||
return this.store.pipe(
|
||||
select(selfLinkFromUuidSelector(uuid)),
|
||||
mergeMap((selfLink: string) => this.getRequestHrefBySelfLink(selfLink))
|
||||
mergeMap((selfLink: string) => this.getRequestUUIDBySelfLink(selfLink))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -148,7 +165,7 @@ export class ObjectCacheService {
|
||||
|
||||
this.store.pipe(
|
||||
select(selfLinkFromUuidSelector(uuid)),
|
||||
first()
|
||||
take(1)
|
||||
).subscribe((selfLink: string) => result = this.hasBySelfLink(selfLink));
|
||||
|
||||
return result;
|
||||
@@ -167,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;
|
||||
@@ -195,4 +212,39 @@ export class ObjectCacheService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add operations to the existing list of operations for an ObjectCacheEntry
|
||||
* Makes sure the ServerSyncBuffer for this ObjectCacheEntry is updated
|
||||
* @param {string} uuid
|
||||
* the uuid of the ObjectCacheEntry
|
||||
* @param {Operation[]} patch
|
||||
* list of operations to perform
|
||||
*/
|
||||
public addPatch(selfLink: string, patch: Operation[]) {
|
||||
this.store.dispatch(new AddPatchObjectCacheAction(selfLink, patch));
|
||||
this.store.dispatch(new AddToSSBAction(selfLink, RestRequestMethod.PATCH));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether there are any unperformed operations for an ObjectCacheEntry
|
||||
*
|
||||
* @param entry
|
||||
* the entry to check
|
||||
* @return boolean
|
||||
* false if the entry is there are no operations left in the ObjectCacheEntry, true otherwise
|
||||
*/
|
||||
private isDirty(entry: ObjectCacheEntry): boolean {
|
||||
return isNotEmpty(entry.patches);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the existing operations on an ObjectCacheEntry in the store
|
||||
* NB: this does not make any server side changes
|
||||
* @param {string} uuid
|
||||
* the uuid of the ObjectCacheEntry
|
||||
*/
|
||||
private applyPatchesToCachedObject(selfLink: string) {
|
||||
this.store.dispatch(new ApplyPatchObjectCacheAction(selfLink));
|
||||
}
|
||||
|
||||
}
|
||||
|
72
src/app/core/cache/response-cache.actions.ts
vendored
72
src/app/core/cache/response-cache.actions.ts
vendored
@@ -1,72 +0,0 @@
|
||||
import { Action } from '@ngrx/store';
|
||||
|
||||
import { type } from '../../shared/ngrx/type';
|
||||
import { RestResponse } from './response-cache.models';
|
||||
|
||||
/**
|
||||
* The list of ResponseCacheAction type definitions
|
||||
*/
|
||||
export const ResponseCacheActionTypes = {
|
||||
ADD: type('dspace/core/cache/response/ADD'),
|
||||
REMOVE: type('dspace/core/cache/response/REMOVE'),
|
||||
RESET_TIMESTAMPS: type('dspace/core/cache/response/RESET_TIMESTAMPS')
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
export class ResponseCacheAddAction implements Action {
|
||||
type = ResponseCacheActionTypes.ADD;
|
||||
payload: {
|
||||
key: string,
|
||||
response: RestResponse
|
||||
timeAdded: number;
|
||||
msToLive: number;
|
||||
};
|
||||
|
||||
constructor(key: string, response: RestResponse, timeAdded: number, msToLive: number) {
|
||||
this.payload = { key, response, timeAdded, msToLive };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An ngrx action to remove a request from the cache
|
||||
*/
|
||||
export class ResponseCacheRemoveAction implements Action {
|
||||
type = ResponseCacheActionTypes.REMOVE;
|
||||
payload: string;
|
||||
|
||||
/**
|
||||
* Create a new ResponseCacheRemoveAction
|
||||
* @param key
|
||||
* The key of the request to remove
|
||||
*/
|
||||
constructor(key: string) {
|
||||
this.payload = key;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An ngrx action to reset the timeAdded property of all cached objects
|
||||
*/
|
||||
export class ResetResponseCacheTimestampsAction implements Action {
|
||||
type = ResponseCacheActionTypes.RESET_TIMESTAMPS;
|
||||
payload: number;
|
||||
|
||||
/**
|
||||
* Create a new ResetObjectCacheTimestampsAction
|
||||
*
|
||||
* @param newTimestamp
|
||||
* the new timeAdded all objects should get
|
||||
*/
|
||||
constructor(newTimestamp: number) {
|
||||
this.payload = newTimestamp;
|
||||
}
|
||||
}
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
* A type to encompass all ResponseCacheActions
|
||||
*/
|
||||
export type ResponseCacheAction
|
||||
= ResponseCacheAddAction
|
||||
| ResponseCacheRemoveAction
|
||||
| ResetResponseCacheTimestampsAction;
|
@@ -1,38 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Observable } from 'rxjs';
|
||||
import { provideMockActions } from '@ngrx/effects/testing';
|
||||
import { cold, hot } from 'jasmine-marbles';
|
||||
import { StoreActionTypes } from '../../store.actions';
|
||||
import { ResponseCacheEffects } from './response-cache.effects';
|
||||
import { ResetResponseCacheTimestampsAction } from './response-cache.actions';
|
||||
|
||||
describe('ResponseCacheEffects', () => {
|
||||
let cacheEffects: ResponseCacheEffects;
|
||||
let actions: Observable<any>;
|
||||
const timestamp = 10000;
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
ResponseCacheEffects,
|
||||
provideMockActions(() => actions),
|
||||
// other providers
|
||||
],
|
||||
});
|
||||
|
||||
cacheEffects = TestBed.get(ResponseCacheEffects);
|
||||
});
|
||||
|
||||
describe('fixTimestampsOnRehydrate$', () => {
|
||||
|
||||
it('should return a RESET_TIMESTAMPS action in response to a REHYDRATE action', () => {
|
||||
spyOn(Date.prototype, 'getTime').and.callFake(() => {
|
||||
return timestamp;
|
||||
});
|
||||
actions = hot('--a-', { a: { type: StoreActionTypes.REHYDRATE, payload: {} } });
|
||||
|
||||
const expected = cold('--b-', { b: new ResetResponseCacheTimestampsAction(new Date().getTime()) });
|
||||
|
||||
expect(cacheEffects.fixTimestampsOnRehydrate).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
});
|
27
src/app/core/cache/response-cache.effects.ts
vendored
27
src/app/core/cache/response-cache.effects.ts
vendored
@@ -1,27 +0,0 @@
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||
|
||||
import { ResetResponseCacheTimestampsAction } from './response-cache.actions';
|
||||
import { StoreActionTypes } from '../../store.actions';
|
||||
|
||||
@Injectable()
|
||||
export class ResponseCacheEffects {
|
||||
|
||||
/**
|
||||
* When the store is rehydrated in the browser, set all cache
|
||||
* timestamps to 'now', because the time zone of the server can
|
||||
* differ from the client.
|
||||
*
|
||||
* This assumes that the server cached everything a negligible
|
||||
* time ago, and will likely need to be revisited later
|
||||
*/
|
||||
@Effect() fixTimestampsOnRehydrate = this.actions$
|
||||
.pipe(ofType(StoreActionTypes.REHYDRATE),
|
||||
map(() => new ResetResponseCacheTimestampsAction(new Date().getTime()))
|
||||
);
|
||||
|
||||
constructor(private actions$: Actions,) {
|
||||
}
|
||||
|
||||
}
|
124
src/app/core/cache/response-cache.reducer.spec.ts
vendored
124
src/app/core/cache/response-cache.reducer.spec.ts
vendored
@@ -1,124 +0,0 @@
|
||||
import * as deepFreeze from 'deep-freeze';
|
||||
|
||||
import { responseCacheReducer, ResponseCacheState } from './response-cache.reducer';
|
||||
|
||||
import {
|
||||
ResponseCacheRemoveAction,
|
||||
ResetResponseCacheTimestampsAction, ResponseCacheAddAction
|
||||
} from './response-cache.actions';
|
||||
import { RestResponse } from './response-cache.models';
|
||||
|
||||
class NullAction extends ResponseCacheRemoveAction {
|
||||
type = null;
|
||||
payload = null;
|
||||
|
||||
constructor() {
|
||||
super(null);
|
||||
}
|
||||
}
|
||||
|
||||
describe('responseCacheReducer', () => {
|
||||
const keys = ['125c17f89046283c5f0640722aac9feb', 'a06c3006a41caec5d635af099b0c780c'];
|
||||
const msToLive = 900000;
|
||||
const uuids = [
|
||||
'9e32a2e2-6b91-4236-a361-995ccdc14c60',
|
||||
'598ce822-c357-46f3-ab70-63724d02d6ad',
|
||||
'be8325f7-243b-49f4-8a4b-df2b793ff3b5'
|
||||
];
|
||||
const testState: ResponseCacheState = {
|
||||
[keys[0]]: {
|
||||
key: keys[0],
|
||||
response: new RestResponse(true, '200'),
|
||||
timeAdded: new Date().getTime(),
|
||||
msToLive: msToLive
|
||||
},
|
||||
[keys[1]]: {
|
||||
key: keys[1],
|
||||
response: new RestResponse(true, '200'),
|
||||
timeAdded: new Date().getTime(),
|
||||
msToLive: msToLive
|
||||
}
|
||||
};
|
||||
deepFreeze(testState);
|
||||
const errorState: {} = {
|
||||
[keys[0]]: {
|
||||
errorMessage: 'error',
|
||||
resourceUUIDs: uuids
|
||||
}
|
||||
};
|
||||
deepFreeze(errorState);
|
||||
|
||||
it('should return the current state when no valid actions have been made', () => {
|
||||
const action = new NullAction();
|
||||
const newState = responseCacheReducer(testState, action);
|
||||
|
||||
expect(newState).toEqual(testState);
|
||||
});
|
||||
|
||||
it('should start with an empty cache', () => {
|
||||
const action = new NullAction();
|
||||
const initialState = responseCacheReducer(undefined, action);
|
||||
|
||||
expect(initialState).toEqual(Object.create(null));
|
||||
});
|
||||
|
||||
describe('ADD', () => {
|
||||
const addTimeAdded = new Date().getTime();
|
||||
const addMsToLive = 5;
|
||||
const addResponse = new RestResponse(true, '200');
|
||||
const action = new ResponseCacheAddAction(keys[0], addResponse, addTimeAdded, addMsToLive);
|
||||
|
||||
it('should perform the action without affecting the previous state', () => {
|
||||
// testState has already been frozen above
|
||||
responseCacheReducer(testState, action);
|
||||
});
|
||||
|
||||
it('should add the response to the cached request', () => {
|
||||
const newState = responseCacheReducer(testState, action);
|
||||
expect(newState[keys[0]].timeAdded).toBe(addTimeAdded);
|
||||
expect(newState[keys[0]].msToLive).toBe(addMsToLive);
|
||||
expect(newState[keys[0]].response).toBe(addResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('REMOVE', () => {
|
||||
it('should perform the action without affecting the previous state', () => {
|
||||
const action = new ResponseCacheRemoveAction(keys[0]);
|
||||
// testState has already been frozen above
|
||||
responseCacheReducer(testState, action);
|
||||
});
|
||||
|
||||
it('should remove the specified request from the cache', () => {
|
||||
const action = new ResponseCacheRemoveAction(keys[0]);
|
||||
const newState = responseCacheReducer(testState, action);
|
||||
expect(testState[keys[0]]).not.toBeUndefined();
|
||||
expect(newState[keys[0]]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('shouldn\'t do anything when the specified key isn\'t cached', () => {
|
||||
const wrongKey = 'this isn\'t cached';
|
||||
const action = new ResponseCacheRemoveAction(wrongKey);
|
||||
const newState = responseCacheReducer(testState, action);
|
||||
expect(testState[wrongKey]).toBeUndefined();
|
||||
expect(newState).toEqual(testState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RESET_TIMESTAMPS', () => {
|
||||
const newTimeStamp = new Date().getTime();
|
||||
const action = new ResetResponseCacheTimestampsAction(newTimeStamp);
|
||||
|
||||
it('should perform the action without affecting the previous state', () => {
|
||||
// testState has already been frozen above
|
||||
responseCacheReducer(testState, action);
|
||||
});
|
||||
|
||||
it('should set the timestamp of all requests in the cache', () => {
|
||||
const newState = responseCacheReducer(testState, action);
|
||||
Object.keys(newState).forEach((key) => {
|
||||
expect(newState[key].timeAdded).toEqual(newTimeStamp);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
111
src/app/core/cache/response-cache.reducer.ts
vendored
111
src/app/core/cache/response-cache.reducer.ts
vendored
@@ -1,111 +0,0 @@
|
||||
import {
|
||||
ResponseCacheAction, ResponseCacheActionTypes,
|
||||
ResponseCacheRemoveAction, ResetResponseCacheTimestampsAction,
|
||||
ResponseCacheAddAction
|
||||
} from './response-cache.actions';
|
||||
import { CacheEntry } from './cache-entry';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { RestResponse } from './response-cache.models';
|
||||
|
||||
/**
|
||||
* An entry in the ResponseCache
|
||||
*/
|
||||
export class ResponseCacheEntry implements CacheEntry {
|
||||
key: string;
|
||||
response: RestResponse;
|
||||
timeAdded: number;
|
||||
msToLive: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The ResponseCache State
|
||||
*/
|
||||
export interface ResponseCacheState {
|
||||
[key: string]: ResponseCacheEntry
|
||||
}
|
||||
|
||||
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
||||
const initialState = Object.create(null);
|
||||
|
||||
/**
|
||||
* The ResponseCache Reducer
|
||||
*
|
||||
* @param state
|
||||
* the current state
|
||||
* @param action
|
||||
* the action to perform on the state
|
||||
* @return ResponseCacheState
|
||||
* the new state
|
||||
*/
|
||||
export function responseCacheReducer(state = initialState, action: ResponseCacheAction): ResponseCacheState {
|
||||
switch (action.type) {
|
||||
|
||||
case ResponseCacheActionTypes.ADD: {
|
||||
return addToCache(state, action as ResponseCacheAddAction);
|
||||
}
|
||||
|
||||
case ResponseCacheActionTypes.REMOVE: {
|
||||
return removeFromCache(state, action as ResponseCacheRemoveAction);
|
||||
}
|
||||
|
||||
case ResponseCacheActionTypes.RESET_TIMESTAMPS: {
|
||||
return resetResponseCacheTimestamps(state, action as ResetResponseCacheTimestampsAction)
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addToCache(state: ResponseCacheState, action: ResponseCacheAddAction): ResponseCacheState {
|
||||
return Object.assign({}, state, {
|
||||
[action.payload.key]: {
|
||||
key: action.payload.key,
|
||||
response: action.payload.response,
|
||||
timeAdded: action.payload.timeAdded,
|
||||
msToLive: action.payload.msToLive
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a request from the cache
|
||||
*
|
||||
* @param state
|
||||
* the current state
|
||||
* @param action
|
||||
* an ResponseCacheRemoveAction
|
||||
* @return ResponseCacheState
|
||||
* the new state, with the request removed if it existed.
|
||||
*/
|
||||
function removeFromCache(state: ResponseCacheState, action: ResponseCacheRemoveAction): ResponseCacheState {
|
||||
if (hasValue(state[action.payload])) {
|
||||
const newCache = Object.assign({}, state);
|
||||
delete newCache[action.payload];
|
||||
|
||||
return newCache;
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the timeAdded timestamp of every cached request to the specified value
|
||||
*
|
||||
* @param state
|
||||
* the current state
|
||||
* @param action
|
||||
* a ResetResponseCacheTimestampsAction
|
||||
* @return ResponseCacheState
|
||||
* the new state, with all timeAdded timestamps set to the specified value
|
||||
*/
|
||||
function resetResponseCacheTimestamps(state: ResponseCacheState, action: ResetResponseCacheTimestampsAction): ResponseCacheState {
|
||||
const newState = Object.create(null);
|
||||
Object.keys(state).forEach((key) => {
|
||||
newState[key] = Object.assign({}, state[key], {
|
||||
timeAdded: action.payload
|
||||
});
|
||||
});
|
||||
return newState;
|
||||
}
|
100
src/app/core/cache/response-cache.service.spec.ts
vendored
100
src/app/core/cache/response-cache.service.spec.ts
vendored
@@ -1,100 +0,0 @@
|
||||
import { Store } from '@ngrx/store';
|
||||
|
||||
import { ResponseCacheService } from './response-cache.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { RestResponse } from './response-cache.models';
|
||||
import { ResponseCacheEntry } from './response-cache.reducer';
|
||||
import { first } from 'rxjs/operators';
|
||||
import * as ngrx from '@ngrx/store'
|
||||
import { cold } from 'jasmine-marbles';
|
||||
|
||||
describe('ResponseCacheService', () => {
|
||||
let service: ResponseCacheService;
|
||||
let store: Store<CoreState>;
|
||||
|
||||
const keys = ['125c17f89046283c5f0640722aac9feb', 'a06c3006a41caec5d635af099b0c780c'];
|
||||
const timestamp = new Date().getTime();
|
||||
const validCacheEntry = (key) => {
|
||||
return {
|
||||
key: key,
|
||||
response: new RestResponse(true, '200'),
|
||||
timeAdded: timestamp,
|
||||
msToLive: 24 * 60 * 60 * 1000 // a day
|
||||
}
|
||||
};
|
||||
const invalidCacheEntry = (key) => {
|
||||
return {
|
||||
key: key,
|
||||
response: new RestResponse(true, '200'),
|
||||
timeAdded: 0,
|
||||
msToLive: 0
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store = new Store<CoreState>(undefined, undefined, undefined);
|
||||
spyOn(store, 'dispatch');
|
||||
service = new ResponseCacheService(store);
|
||||
spyOn(Date.prototype, 'getTime').and.callFake(() => {
|
||||
return timestamp;
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should return an observable of the cached request with the specified key', () => {
|
||||
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
||||
return () => {
|
||||
return () => observableOf(validCacheEntry(keys[1]));
|
||||
};
|
||||
});
|
||||
let testObj: ResponseCacheEntry;
|
||||
service.get(keys[1]).pipe(first()).subscribe((entry) => {
|
||||
testObj = entry;
|
||||
});
|
||||
expect(testObj.key).toEqual(keys[1]);
|
||||
});
|
||||
|
||||
it('should not return a cached request that has exceeded its time to live', () => {
|
||||
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
||||
return () => {
|
||||
return () => observableOf(invalidCacheEntry(keys[1]));
|
||||
};
|
||||
});
|
||||
|
||||
let getObsHasFired = false;
|
||||
const subscription = service.get(keys[1]).subscribe((entry) => getObsHasFired = true);
|
||||
expect(getObsHasFired).toBe(false);
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
});
|
||||
|
||||
describe('has', () => {
|
||||
it('should return true if the request with the supplied key is cached and still valid', () => {
|
||||
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
||||
return () => {
|
||||
return () => observableOf(validCacheEntry(keys[1]));
|
||||
};
|
||||
});
|
||||
expect(service.has(keys[1])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the request with the supplied key isn\'t cached', () => {
|
||||
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
||||
return () => {
|
||||
return () => observableOf(undefined);
|
||||
};
|
||||
});
|
||||
expect(service.has(keys[1])).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if the request with the supplied key is cached but has exceeded its time to live', () => {
|
||||
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
||||
return () => {
|
||||
return () => observableOf(invalidCacheEntry(keys[1]));
|
||||
};
|
||||
});
|
||||
expect(service.has(keys[1])).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
100
src/app/core/cache/response-cache.service.ts
vendored
100
src/app/core/cache/response-cache.service.ts
vendored
@@ -1,100 +0,0 @@
|
||||
import { filter, take, distinctUntilChanged, first } from 'rxjs/operators';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { ResponseCacheEntry } from './response-cache.reducer';
|
||||
import { hasNoValue } from '../../shared/empty.util';
|
||||
import { ResponseCacheRemoveAction, ResponseCacheAddAction } from './response-cache.actions';
|
||||
import { RestResponse } from './response-cache.models';
|
||||
import { coreSelector, CoreState } from '../core.reducers';
|
||||
import { pathSelector } from '../shared/selectors';
|
||||
|
||||
function entryFromKeySelector(key: string): MemoizedSelector<CoreState, ResponseCacheEntry> {
|
||||
return pathSelector<CoreState, ResponseCacheEntry>(coreSelector, 'data/response', key);
|
||||
}
|
||||
|
||||
/**
|
||||
* A service to interact with the response cache
|
||||
*/
|
||||
@Injectable()
|
||||
export class ResponseCacheService {
|
||||
constructor(
|
||||
private store: Store<CoreState>
|
||||
) {
|
||||
}
|
||||
|
||||
add(key: string, response: RestResponse, msToLive: number): Observable<ResponseCacheEntry> {
|
||||
if (!this.has(key)) {
|
||||
this.store.dispatch(new ResponseCacheAddAction(key, response, new Date().getTime(), msToLive));
|
||||
}
|
||||
return this.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an observable of the response with the specified key
|
||||
*
|
||||
* @param key
|
||||
* the key of the response to get
|
||||
* @return Observable<ResponseCacheEntry>
|
||||
* an observable of the ResponseCacheEntry with the specified key
|
||||
*/
|
||||
get(key: string): Observable<ResponseCacheEntry> {
|
||||
return this.store.pipe(
|
||||
select(entryFromKeySelector(key)),
|
||||
filter((entry: ResponseCacheEntry) => this.isValid(entry)),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the response with the specified key is cached
|
||||
*
|
||||
* @param key
|
||||
* the key of the response to check
|
||||
* @return boolean
|
||||
* true if the response with the specified key is cached,
|
||||
* false otherwise
|
||||
*/
|
||||
has(key: string): boolean {
|
||||
let result: boolean;
|
||||
|
||||
this.store.pipe(select(entryFromKeySelector(key)),
|
||||
first()
|
||||
).subscribe((entry: ResponseCacheEntry) => {
|
||||
result = this.isValid(entry);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
remove(key: string): void {
|
||||
if (this.has(key)) {
|
||||
this.store.dispatch(new ResponseCacheRemoveAction(key));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a ResponseCacheEntry should still be cached
|
||||
*
|
||||
* @param entry
|
||||
* the entry to check
|
||||
* @return boolean
|
||||
* false if the entry is null, undefined, or its time to
|
||||
* live has been exceeded, true otherwise
|
||||
*/
|
||||
private isValid(entry: ResponseCacheEntry): boolean {
|
||||
if (hasNoValue(entry)) {
|
||||
return false;
|
||||
} else {
|
||||
const timeOutdated = entry.timeAdded + entry.msToLive;
|
||||
const isOutDated = new Date().getTime() > timeOutdated;
|
||||
if (isOutDated) {
|
||||
this.store.dispatch(new ResponseCacheRemoveAction(entry.key));
|
||||
}
|
||||
return !isOutDated;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -10,10 +10,11 @@ import { MetadataSchema } from '../metadata/metadataschema.model';
|
||||
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
|
||||
import { 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 {
|
||||
public toCache = true;
|
||||
public timeAdded: number;
|
||||
|
||||
constructor(
|
||||
public isSuccessful: boolean,
|
82
src/app/core/cache/server-sync-buffer.actions.ts
vendored
Normal file
82
src/app/core/cache/server-sync-buffer.actions.ts
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Action } from '@ngrx/store';
|
||||
|
||||
import { type } from '../../shared/ngrx/type';
|
||||
import { RestRequestMethod } from '../data/rest-request-method';
|
||||
|
||||
/**
|
||||
* The list of ServerSyncBufferAction type definitions
|
||||
*/
|
||||
export const ServerSyncBufferActionTypes = {
|
||||
ADD: type('dspace/core/cache/syncbuffer/ADD'),
|
||||
COMMIT: type('dspace/core/cache/syncbuffer/COMMIT'),
|
||||
EMPTY: type('dspace/core/cache/syncbuffer/EMPTY'),
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
* An ngrx action to add a new cached object to the server sync buffer
|
||||
*/
|
||||
export class AddToSSBAction implements Action {
|
||||
type = ServerSyncBufferActionTypes.ADD;
|
||||
payload: {
|
||||
href: string,
|
||||
method: RestRequestMethod
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new AddToSSBAction
|
||||
*
|
||||
* @param href
|
||||
* the unique href of the cached object entry that should be updated
|
||||
*/
|
||||
constructor(href: string, method: RestRequestMethod) {
|
||||
this.payload = { href, method: method };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An ngrx action to commit everything (for a certain method, when specified) in the ServerSyncBuffer to the server
|
||||
*/
|
||||
export class CommitSSBAction implements Action {
|
||||
type = ServerSyncBufferActionTypes.COMMIT;
|
||||
payload?: RestRequestMethod;
|
||||
|
||||
/**
|
||||
* Create a new CommitSSBAction
|
||||
*
|
||||
* @param method
|
||||
* an optional method for which the ServerSyncBuffer should send its entries to the server
|
||||
*/
|
||||
constructor(method?: RestRequestMethod) {
|
||||
this.payload = method;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* An ngrx action to remove everything (for a certain method, when specified) from the ServerSyncBuffer to the server
|
||||
*/
|
||||
export class EmptySSBAction implements Action {
|
||||
type = ServerSyncBufferActionTypes.EMPTY;
|
||||
payload?: RestRequestMethod;
|
||||
|
||||
/**
|
||||
* Create a new EmptySSBAction
|
||||
*
|
||||
* @param method
|
||||
* an optional method for which the ServerSyncBuffer should remove its entries
|
||||
* if this parameter is omitted, the buffer will be emptied as a whole
|
||||
*/
|
||||
constructor(method?: RestRequestMethod) {
|
||||
this.payload = method;
|
||||
}
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
* A type to encompass all ServerSyncBufferActions
|
||||
*/
|
||||
export type ServerSyncBufferAction
|
||||
= AddToSSBAction
|
||||
| CommitSSBAction
|
||||
| EmptySSBAction
|
139
src/app/core/cache/server-sync-buffer.effects.spec.ts
vendored
Normal file
139
src/app/core/cache/server-sync-buffer.effects.spec.ts
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { provideMockActions } from '@ngrx/effects/testing';
|
||||
import { cold, hot } from 'jasmine-marbles';
|
||||
import { ServerSyncBufferEffects } from './server-sync-buffer.effects';
|
||||
import { GLOBAL_CONFIG } from '../../../config';
|
||||
import {
|
||||
CommitSSBAction,
|
||||
EmptySSBAction,
|
||||
ServerSyncBufferActionTypes
|
||||
} from './server-sync-buffer.actions';
|
||||
import { RestRequestMethod } from '../data/rest-request-method';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { ObjectCacheService } from './object-cache.service';
|
||||
import { MockStore } from '../../shared/testing/mock-store';
|
||||
import { ObjectCacheState } from './object-cache.reducer';
|
||||
import * as operators from 'rxjs/operators';
|
||||
import { spyOnOperator } from '../../shared/testing/utils';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||
import { ApplyPatchObjectCacheAction } from './object-cache.actions';
|
||||
|
||||
describe('ServerSyncBufferEffects', () => {
|
||||
let ssbEffects: ServerSyncBufferEffects;
|
||||
let actions: Observable<any>;
|
||||
const testConfig = {
|
||||
cache:
|
||||
{
|
||||
autoSync:
|
||||
{
|
||||
timePerMethod: {},
|
||||
defaultTime: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
const selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new MockStore<ObjectCacheState>({});
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
ServerSyncBufferEffects,
|
||||
provideMockActions(() => actions),
|
||||
{ provide: GLOBAL_CONFIG, useValue: testConfig },
|
||||
{ provide: RequestService, useValue: getMockRequestService() },
|
||||
{
|
||||
provide: ObjectCacheService, useValue: {
|
||||
getBySelfLink: (link) => {
|
||||
const object = new DSpaceObject();
|
||||
object.self = link;
|
||||
return observableOf(object);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ provide: Store, useValue: store }
|
||||
// other providers
|
||||
],
|
||||
});
|
||||
|
||||
ssbEffects = TestBed.get(ServerSyncBufferEffects);
|
||||
});
|
||||
|
||||
describe('setTimeoutForServerSync', () => {
|
||||
beforeEach(() => {
|
||||
spyOnOperator(operators, 'delay').and.returnValue((v) => v);
|
||||
});
|
||||
|
||||
it('should return a COMMIT action in response to an ADD action', () => {
|
||||
actions = hot('a', {
|
||||
a: {
|
||||
type: ServerSyncBufferActionTypes.ADD,
|
||||
payload: { href: selfLink, method: RestRequestMethod.PUT }
|
||||
}
|
||||
});
|
||||
|
||||
const expected = cold('b', { b: new CommitSSBAction(RestRequestMethod.PUT) });
|
||||
|
||||
expect(ssbEffects.setTimeoutForServerSync).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('commitServerSyncBuffer', () => {
|
||||
describe('when the buffer is not empty', () => {
|
||||
beforeEach(() => {
|
||||
store
|
||||
.subscribe((state) => {
|
||||
(state as any).core = Object({});
|
||||
(state as any).core['cache/syncbuffer'] = {
|
||||
buffer: [{
|
||||
href: selfLink,
|
||||
method: RestRequestMethod.PATCH
|
||||
}]
|
||||
};
|
||||
});
|
||||
});
|
||||
it('should return a list of actions in response to a COMMIT action', () => {
|
||||
actions = hot('a', {
|
||||
a: {
|
||||
type: ServerSyncBufferActionTypes.COMMIT,
|
||||
payload: RestRequestMethod.PATCH
|
||||
}
|
||||
});
|
||||
|
||||
const expected = cold('(bc)', {
|
||||
b: new ApplyPatchObjectCacheAction(selfLink),
|
||||
c: new EmptySSBAction(RestRequestMethod.PATCH)
|
||||
});
|
||||
|
||||
expect(ssbEffects.commitServerSyncBuffer).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the buffer is empty', () => {
|
||||
beforeEach(() => {
|
||||
store
|
||||
.subscribe((state) => {
|
||||
(state as any).core = Object({});
|
||||
(state as any).core['cache/syncbuffer'] = {
|
||||
buffer: []
|
||||
};
|
||||
});
|
||||
});
|
||||
it('should return a placeholder action in response to a COMMIT action', () => {
|
||||
store.subscribe();
|
||||
actions = hot('a', {
|
||||
a: {
|
||||
type: ServerSyncBufferActionTypes.COMMIT,
|
||||
payload: { method: RestRequestMethod.PATCH }
|
||||
}
|
||||
});
|
||||
const expected = cold('b', { b: { type: 'NO_ACTION' } });
|
||||
|
||||
expect(ssbEffects.commitServerSyncBuffer).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
122
src/app/core/cache/server-sync-buffer.effects.ts
vendored
Normal file
122
src/app/core/cache/server-sync-buffer.effects.ts
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
import { delay, exhaustMap, map, switchMap, take } from 'rxjs/operators';
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||
import {
|
||||
AddToSSBAction,
|
||||
CommitSSBAction,
|
||||
EmptySSBAction,
|
||||
ServerSyncBufferActionTypes
|
||||
} from './server-sync-buffer.actions';
|
||||
import { GLOBAL_CONFIG } from '../../../config';
|
||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||
import { coreSelector, CoreState } from '../core.reducers';
|
||||
import { Action, createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer';
|
||||
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { PutRequest } from '../data/request.models';
|
||||
import { ObjectCacheService } from './object-cache.service';
|
||||
import { ApplyPatchObjectCacheAction } from './object-cache.actions';
|
||||
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||
import { GenericConstructor } from '../shared/generic-constructor';
|
||||
import { hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RestRequestMethod } from '../data/rest-request-method';
|
||||
|
||||
@Injectable()
|
||||
export class ServerSyncBufferEffects {
|
||||
|
||||
/**
|
||||
* When an ADDToSSBAction is dispatched
|
||||
* Set a time out (configurable per method type)
|
||||
* Then dispatch a CommitSSBAction
|
||||
* When the delay is running, no new AddToSSBActions are processed in this effect
|
||||
*/
|
||||
@Effect() setTimeoutForServerSync = this.actions$
|
||||
.pipe(
|
||||
ofType(ServerSyncBufferActionTypes.ADD),
|
||||
exhaustMap((action: AddToSSBAction) => {
|
||||
const autoSyncConfig = this.EnvConfig.cache.autoSync;
|
||||
const timeoutInSeconds = autoSyncConfig.timePerMethod[action.payload.method] || autoSyncConfig.defaultTime;
|
||||
return observableOf(new CommitSSBAction(action.payload.method)).pipe(
|
||||
delay(timeoutInSeconds * 1000),
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* When a CommitSSBAction is dispatched
|
||||
* Create a list of actions for each entry in the current buffer state to be dispatched
|
||||
* When the list of actions is not empty, also dispatch an EmptySSBAction
|
||||
* When the list is empty dispatch a NO_ACTION placeholder action
|
||||
*/
|
||||
@Effect() commitServerSyncBuffer = this.actions$
|
||||
.pipe(
|
||||
ofType(ServerSyncBufferActionTypes.COMMIT),
|
||||
switchMap((action: CommitSSBAction) => {
|
||||
return this.store.pipe(
|
||||
select(serverSyncBufferSelector()),
|
||||
take(1), /* necessary, otherwise delay will not have any effect after the first run */
|
||||
switchMap((bufferState: ServerSyncBufferState) => {
|
||||
const actions: Array<Observable<Action>> = bufferState.buffer
|
||||
.filter((entry: ServerSyncBufferEntry) => {
|
||||
/* If there's a request method, filter
|
||||
If there's no filter, commit everything */
|
||||
if (hasValue(action.payload)) {
|
||||
return entry.method === action.payload;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((entry: ServerSyncBufferEntry) => {
|
||||
if (entry.method === RestRequestMethod.PATCH) {
|
||||
return this.applyPatch(entry.href);
|
||||
} else {
|
||||
/* TODO implement for other request method types */
|
||||
}
|
||||
});
|
||||
|
||||
/* Add extra action to array, to make sure the ServerSyncBuffer is emptied afterwards */
|
||||
if (isNotEmpty(actions) && isNotUndefined(actions[0])) {
|
||||
return observableCombineLatest(...actions).pipe(
|
||||
switchMap((array) => [...array, new EmptySSBAction(action.payload)])
|
||||
);
|
||||
} else {
|
||||
return observableOf({ type: 'NO_ACTION' });
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* private method to create an ApplyPatchObjectCacheAction based on a cache entry
|
||||
* and to do the actual patch request to the server
|
||||
* @param {string} href The self link of the cache entry
|
||||
* @returns {Observable<Action>} ApplyPatchObjectCacheAction to be dispatched
|
||||
*/
|
||||
private applyPatch(href: string): Observable<Action> {
|
||||
const patchObject = this.objectCache.getBySelfLink(href).pipe(take(1));
|
||||
|
||||
return patchObject.pipe(
|
||||
map((object) => {
|
||||
const serializedObject = new DSpaceRESTv2Serializer(object.constructor as GenericConstructor<{}>).serialize(object);
|
||||
|
||||
this.requestService.configure(new PutRequest(this.requestService.generateRequestId(), href, serializedObject));
|
||||
|
||||
return new ApplyPatchObjectCacheAction(href)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
constructor(private actions$: Actions,
|
||||
private store: Store<CoreState>,
|
||||
private requestService: RequestService,
|
||||
private objectCache: ObjectCacheService,
|
||||
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export function serverSyncBufferSelector(): MemoizedSelector<CoreState, ServerSyncBufferState> {
|
||||
return createSelector(coreSelector, (state: CoreState) => state['cache/syncbuffer']);
|
||||
}
|
85
src/app/core/cache/server-sync-buffer.reducer.spec.ts
vendored
Normal file
85
src/app/core/cache/server-sync-buffer.reducer.spec.ts
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
import * as deepFreeze from 'deep-freeze';
|
||||
import { RemoveFromObjectCacheAction } from './object-cache.actions';
|
||||
import { serverSyncBufferReducer } from './server-sync-buffer.reducer';
|
||||
import { RestRequestMethod } from '../data/rest-request-method';
|
||||
import { AddToSSBAction, EmptySSBAction } from './server-sync-buffer.actions';
|
||||
|
||||
class NullAction extends RemoveFromObjectCacheAction {
|
||||
type = null;
|
||||
payload = null;
|
||||
|
||||
constructor() {
|
||||
super(null);
|
||||
}
|
||||
}
|
||||
|
||||
describe('serverSyncBufferReducer', () => {
|
||||
const selfLink1 = 'https://localhost:8080/api/core/items/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||
const selfLink2 = 'https://localhost:8080/api/core/items/28b04544-1766-4e82-9728-c4e93544ecd3';
|
||||
const testState = {
|
||||
buffer:
|
||||
[
|
||||
{
|
||||
href: selfLink1,
|
||||
method: RestRequestMethod.PATCH,
|
||||
},
|
||||
{
|
||||
href: selfLink2,
|
||||
method: RestRequestMethod.GET,
|
||||
}
|
||||
]
|
||||
};
|
||||
const newSelfLink = 'https://localhost:8080/api/core/items/1ce6b5ae-97e1-4e5a-b4b0-f9029bad10c0';
|
||||
|
||||
deepFreeze(testState);
|
||||
|
||||
it('should return the current state when no valid actions have been made', () => {
|
||||
const action = new NullAction();
|
||||
const newState = serverSyncBufferReducer(testState, action);
|
||||
|
||||
expect(newState).toEqual(testState);
|
||||
});
|
||||
|
||||
it('should start with an empty buffer array', () => {
|
||||
const action = new NullAction();
|
||||
const initialState = serverSyncBufferReducer(undefined, action);
|
||||
|
||||
expect(initialState).toEqual({ buffer: [] });
|
||||
});
|
||||
|
||||
it('should perform the ADD action without affecting the previous state', () => {
|
||||
const action = new AddToSSBAction(selfLink1, RestRequestMethod.POST);
|
||||
// testState has already been frozen above
|
||||
serverSyncBufferReducer(testState, action);
|
||||
});
|
||||
|
||||
it('should perform the EMPTY action without affecting the previous state', () => {
|
||||
const action = new EmptySSBAction();
|
||||
// testState has already been frozen above
|
||||
serverSyncBufferReducer(testState, action);
|
||||
});
|
||||
|
||||
it('should empty the buffer if the EmptySSBAction is dispatched without a payload', () => {
|
||||
const action = new EmptySSBAction();
|
||||
// testState has already been frozen above
|
||||
const emptyState = serverSyncBufferReducer(testState, action);
|
||||
expect(emptyState).toEqual({ buffer: [] });
|
||||
});
|
||||
|
||||
it('should empty the buffer partially if the EmptySSBAction is dispatched with a payload', () => {
|
||||
const action = new EmptySSBAction(RestRequestMethod.PATCH);
|
||||
// testState has already been frozen above
|
||||
const emptyState = serverSyncBufferReducer(testState, action);
|
||||
expect(emptyState).toEqual({ buffer: testState.buffer.filter((entry) => entry.method !== RestRequestMethod.PATCH) });
|
||||
});
|
||||
|
||||
it('should add an entry to the buffer if the AddSSBAction is dispatched', () => {
|
||||
const action = new AddToSSBAction(newSelfLink, RestRequestMethod.PUT);
|
||||
// testState has already been frozen above
|
||||
const newState = serverSyncBufferReducer(testState, action);
|
||||
expect(newState.buffer).toContain({
|
||||
href: newSelfLink, method: RestRequestMethod.PUT
|
||||
})
|
||||
;
|
||||
})
|
||||
});
|
92
src/app/core/cache/server-sync-buffer.reducer.ts
vendored
Normal file
92
src/app/core/cache/server-sync-buffer.reducer.ts
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
import { hasNoValue, hasValue } from '../../shared/empty.util';
|
||||
import {
|
||||
AddToSSBAction,
|
||||
EmptySSBAction,
|
||||
ServerSyncBufferAction,
|
||||
ServerSyncBufferActionTypes
|
||||
} from './server-sync-buffer.actions';
|
||||
import { RestRequestMethod } from '../data/rest-request-method';
|
||||
|
||||
/**
|
||||
* An entry in the ServerSyncBufferState
|
||||
* href: unique href of an ObjectCacheEntry
|
||||
* method: RestRequestMethod type
|
||||
*/
|
||||
export class ServerSyncBufferEntry {
|
||||
href: string;
|
||||
method: RestRequestMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* The ServerSyncBuffer State
|
||||
*
|
||||
* Consists list of ServerSyncBufferState
|
||||
*/
|
||||
export interface ServerSyncBufferState {
|
||||
buffer: ServerSyncBufferEntry[];
|
||||
}
|
||||
|
||||
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
||||
const initialState: ServerSyncBufferState = { buffer: [] };
|
||||
|
||||
/**
|
||||
* The ServerSyncBuffer Reducer
|
||||
*
|
||||
* @param state
|
||||
* the current state
|
||||
* @param action
|
||||
* the action to perform on the state
|
||||
* @return ServerSyncBufferState
|
||||
* the new state
|
||||
*/
|
||||
export function serverSyncBufferReducer(state = initialState, action: ServerSyncBufferAction): ServerSyncBufferState {
|
||||
switch (action.type) {
|
||||
|
||||
case ServerSyncBufferActionTypes.ADD: {
|
||||
return addToServerSyncQueue(state, action as AddToSSBAction)
|
||||
}
|
||||
|
||||
case ServerSyncBufferActionTypes.EMPTY: {
|
||||
return emptyServerSyncQueue(state, action as EmptySSBAction);
|
||||
}
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new entry to the buffer with a specified method
|
||||
*
|
||||
* @param state
|
||||
* the current state
|
||||
* @param action
|
||||
* an AddToSSBAction
|
||||
* @return ServerSyncBufferState
|
||||
* the new state, with a new entry added to the buffer
|
||||
*/
|
||||
function addToServerSyncQueue(state: ServerSyncBufferState, action: AddToSSBAction): ServerSyncBufferState {
|
||||
const actionEntry = action.payload as ServerSyncBufferEntry;
|
||||
if (hasNoValue(state.buffer.find((entry) => entry.href === actionEntry.href && entry.method === actionEntry.method))) {
|
||||
return Object.assign({}, state, { buffer: state.buffer.concat(actionEntry) });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all ServerSyncBuffers entry from the buffer with a specified method
|
||||
* If no method is specified, empty the whole buffer
|
||||
*
|
||||
* @param state
|
||||
* the current state
|
||||
* @param action
|
||||
* an AddToSSBAction
|
||||
* @return ServerSyncBufferState
|
||||
* the new state, with a new entry added to the buffer
|
||||
*/
|
||||
function emptyServerSyncQueue(state: ServerSyncBufferState, action: EmptySSBAction): ServerSyncBufferState {
|
||||
let newBuffer = [];
|
||||
if (hasValue(action.payload)) {
|
||||
newBuffer = state.buffer.filter((entry) => entry.method !== action.payload);
|
||||
}
|
||||
return Object.assign({}, state, { buffer: newBuffer });
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||
import { 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,8 @@ 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 +45,6 @@ describe('ConfigService', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
responseCache = initMockResponseCacheService(true);
|
||||
requestService = getMockRequestService();
|
||||
halService = new HALEndpointServiceStub(configEndpoint);
|
||||
service = initTestService();
|
||||
|
@@ -1,24 +1,25 @@
|
||||
import { Observable, of as observableOf, throwError as observableThrowError, merge as observableMerge } from 'rxjs';
|
||||
import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
|
||||
import { 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<ConfigData> {
|
||||
const responses = this.responseCache.get(request.href).pipe(map((entry: ResponseCacheEntry) => entry.response));
|
||||
const responses = this.requestService.getByHref(request.href).pipe(
|
||||
getResponseFromEntry()
|
||||
);
|
||||
const errorResponses = responses.pipe(
|
||||
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<ConfigData> {
|
||||
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)),
|
||||
|
@@ -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();
|
||||
|
@@ -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();
|
||||
|
@@ -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();
|
||||
|
@@ -1,14 +1,14 @@
|
||||
|
||||
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,
|
||||
AuthEffects
|
||||
AuthEffects,
|
||||
ServerSyncBufferEffects
|
||||
];
|
||||
|
@@ -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';
|
||||
@@ -66,6 +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 { NormalizedObjectBuildService } from './cache/builders/normalized-object-build.service';
|
||||
import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service';
|
||||
|
||||
const IMPORTS = [
|
||||
CommonModule,
|
||||
@@ -102,9 +103,9 @@ const PROVIDERS = [
|
||||
ObjectCacheService,
|
||||
PaginationComponentOptions,
|
||||
RegistryService,
|
||||
NormalizedObjectBuildService,
|
||||
RemoteDataBuildService,
|
||||
RequestService,
|
||||
ResponseCacheService,
|
||||
EndpointMapResponseParsingService,
|
||||
FacetValueResponseParsingService,
|
||||
FacetValueMapResponseParsingService,
|
||||
@@ -130,6 +131,7 @@ const PROVIDERS = [
|
||||
UploaderService,
|
||||
UUIDService,
|
||||
DSpaceObjectDataService,
|
||||
DSOChangeAnalyzer,
|
||||
CSSVariableService,
|
||||
MenuService,
|
||||
// register AuthInterceptor as HttpInterceptor
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user