diff --git a/config/environment.default.js b/config/environment.default.js index 20d324562d..d46dc10dee 100644 --- a/config/environment.default.js +++ b/config/environment.default.js @@ -20,7 +20,6 @@ module.exports = { // NOTE: how long should objects be cached for by default msToLive: { default: 15 * 60 * 1000, // 15 minutes - exportToZip: 5 * 1000 // 5 seconds }, // msToLive: 1000, // 15 minutes control: 'max-age=60', // revalidate browser diff --git a/resources/i18n/en.json b/resources/i18n/en.json index f8aa8eddca..a87d36ef9b 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -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": { diff --git a/src/app/+collection-page/collection-form/collection-form.component.ts b/src/app/+collection-page/collection-form/collection-form.component.ts new file mode 100644 index 0000000000..22f2f1271d --- /dev/null +++ b/src/app/+collection-page/collection-form/collection-form.component.ts @@ -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 { + /** + * @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', + }), + ]; +} diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index ca56bca2cd..ddcf36a0cc 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -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 { diff --git a/src/app/+collection-page/collection-page.component.ts b/src/app/+collection-page/collection-page.component.ts index b76c0a7520..7c4f2b92ac 100644 --- a/src/app/+collection-page/collection-page.component.ts +++ b/src/app/+collection-page/collection-page.component.ts @@ -55,7 +55,8 @@ export class CollectionPageComponent implements OnInit, OnDestroy { ngOnInit(): void { this.collectionRD$ = this.route.data.pipe( - map((data) => data.collection) + map((data) => data.collection), + tap((data) => this.collectionId = data.payload.id) ); this.logoRD$ = this.collectionRD$.pipe( map((rd: RemoteData) => rd.payload), diff --git a/src/app/+collection-page/collection-page.module.ts b/src/app/+collection-page/collection-page.module.ts index 85462e67a3..8424cc02a4 100644 --- a/src/app/+collection-page/collection-page.module.ts +++ b/src/app/+collection-page/collection-page.module.ts @@ -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 { diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.html b/src/app/+collection-page/create-collection-page/create-collection-page.component.html new file mode 100644 index 0000000000..b3f4361bc6 --- /dev/null +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.html @@ -0,0 +1,8 @@ +
+
+
+

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

+
+
+ +
diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.scss b/src/app/+collection-page/create-collection-page/create-collection-page.component.scss new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.scss @@ -0,0 +1 @@ + diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts new file mode 100644 index 0000000000..29350a83e0 --- /dev/null +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts @@ -0,0 +1,46 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { RouteService } from '../../shared/services/route.service'; +import { SharedModule } from '../../shared/shared.module'; +import { CollectionDataService } from '../../core/data/collection-data.service'; +import { of as observableOf } from 'rxjs'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { CreateCollectionPageComponent } from './create-collection-page.component'; + +describe('CreateCollectionPageComponent', () => { + let comp: CreateCollectionPageComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + declarations: [CreateCollectionPageComponent], + providers: [ + { provide: CollectionDataService, useValue: {} }, + { + provide: CommunityDataService, + useValue: { findById: () => observableOf({ payload: { name: 'test' } }) } + }, + { provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } }, + { provide: Router, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CreateCollectionPageComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe('frontendURL', () => { + it('should have the right frontendURL set', () => { + expect((comp as any).frontendURL).toEqual('/collections/'); + }) + }); +}); diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts new file mode 100644 index 0000000000..52497694b9 --- /dev/null +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -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 { + protected frontendURL = '/collections/'; + + public constructor( + protected communityDataService: CommunityDataService, + protected collectionDataService: CollectionDataService, + protected routeService: RouteService, + protected router: Router + ) { + super(collectionDataService, communityDataService, routeService, router); + } +} diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.guard.spec.ts b/src/app/+collection-page/create-collection-page/create-collection-page.guard.spec.ts new file mode 100644 index 0000000000..5d21ae36b3 --- /dev/null +++ b/src/app/+collection-page/create-collection-page/create-collection-page.guard.spec.ts @@ -0,0 +1,67 @@ +import { CreateCollectionPageGuard } from './create-collection-page.guard'; +import { MockRouter } from '../../shared/mocks/mock-router'; +import { RemoteData } from '../../core/data/remote-data'; +import { Community } from '../../core/shared/community.model'; +import { of as observableOf } from 'rxjs'; +import { first } from 'rxjs/operators'; + +describe('CreateCollectionPageGuard', () => { + describe('canActivate', () => { + let guard: CreateCollectionPageGuard; + let router; + let communityDataServiceStub: any; + + beforeEach(() => { + communityDataServiceStub = { + findById: (id: string) => { + if (id === 'valid-id') { + return observableOf(new RemoteData(false, false, true, null, new Community())); + } else if (id === 'invalid-id') { + return observableOf(new RemoteData(false, false, true, null, undefined)); + } else if (id === 'error-id') { + return observableOf(new RemoteData(false, false, false, null, new Community())); + } + } + }; + router = new MockRouter(); + + guard = new CreateCollectionPageGuard(router, communityDataServiceStub); + }); + + it('should return true when the parent ID resolves to a community', () => { + guard.canActivate({ queryParams: { parent: 'valid-id' } } as any, undefined) + .pipe(first()) + .subscribe( + (canActivate) => + expect(canActivate).toEqual(true) + ); + }); + + it('should return false when no parent ID has been provided', () => { + guard.canActivate({ queryParams: { } } as any, undefined) + .pipe(first()) + .subscribe( + (canActivate) => + expect(canActivate).toEqual(false) + ); + }); + + it('should return false when the parent ID does not resolve to a community', () => { + guard.canActivate({ queryParams: { parent: 'invalid-id' } } as any, undefined) + .pipe(first()) + .subscribe( + (canActivate) => + expect(canActivate).toEqual(false) + ); + }); + + it('should return false when the parent ID resolves to an error response', () => { + guard.canActivate({ queryParams: { parent: 'error-id' } } as any, undefined) + .pipe(first()) + .subscribe( + (canActivate) => + expect(canActivate).toEqual(false) + ); + }); + }); +}); diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.guard.ts b/src/app/+collection-page/create-collection-page/create-collection-page.guard.ts new file mode 100644 index 0000000000..4cd842e926 --- /dev/null +++ b/src/app/+collection-page/create-collection-page/create-collection-page.guard.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; + +import { hasNoValue, hasValue } from '../../shared/empty.util'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { RemoteData } from '../../core/data/remote-data'; +import { Community } from '../../core/shared/community.model'; +import { getFinishedRemoteData } from '../../core/shared/operators'; +import { map, tap } from 'rxjs/operators'; +import { Observable, of as observableOf } from 'rxjs'; + +/** + * Prevent creation of a collection without a parent community provided + * @class CreateCollectionPageGuard + */ +@Injectable() +export class CreateCollectionPageGuard implements CanActivate { + public constructor(private router: Router, private communityService: CommunityDataService) { + } + + /** + * True when either a parent ID query parameter has been provided and the parent ID resolves to a valid parent community + * Reroutes to a 404 page when the page cannot be activated + * @method canActivate + */ + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + const parentID = route.queryParams.parent; + if (hasNoValue(parentID)) { + this.router.navigate(['/404']); + return observableOf(false); + } + const parent: Observable> = this.communityService.findById(parentID) + .pipe( + getFinishedRemoteData(), + ); + + return parent.pipe( + map((communityRD: RemoteData) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)), + tap((isValid: boolean) => { + if (!isValid) { + this.router.navigate(['/404']); + } + }) + ); + } +} diff --git a/src/app/+collection-page/delete-collection-page/delete-collection-page.component.html b/src/app/+collection-page/delete-collection-page/delete-collection-page.component.html new file mode 100644 index 0000000000..cfd09f2bbd --- /dev/null +++ b/src/app/+collection-page/delete-collection-page/delete-collection-page.component.html @@ -0,0 +1,19 @@ +
+
+ +
+ +

{{ 'community.delete.text' | translate:{ dso: dso.name } }}

+ + +
+
+ +
+ +
diff --git a/src/app/+collection-page/delete-collection-page/delete-collection-page.component.scss b/src/app/+collection-page/delete-collection-page/delete-collection-page.component.scss new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/app/+collection-page/delete-collection-page/delete-collection-page.component.scss @@ -0,0 +1 @@ + diff --git a/src/app/+collection-page/delete-collection-page/delete-collection-page.component.spec.ts b/src/app/+collection-page/delete-collection-page/delete-collection-page.component.spec.ts new file mode 100644 index 0000000000..d64c1d1915 --- /dev/null +++ b/src/app/+collection-page/delete-collection-page/delete-collection-page.component.spec.ts @@ -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; + + 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/'); + }) + }); +}); diff --git a/src/app/+collection-page/delete-collection-page/delete-collection-page.component.ts b/src/app/+collection-page/delete-collection-page/delete-collection-page.component.ts new file mode 100644 index 0000000000..80abb83694 --- /dev/null +++ b/src/app/+collection-page/delete-collection-page/delete-collection-page.component.ts @@ -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 { + 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); + } +} diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.html b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.html new file mode 100644 index 0000000000..c389c681ce --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.html @@ -0,0 +1,11 @@ +
+
+
+ + + {{'collection.edit.delete' + | translate}} +
+
+
diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.scss b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.scss new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.scss @@ -0,0 +1 @@ + diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.spec.ts b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.spec.ts new file mode 100644 index 0000000000..193cb293e4 --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.spec.ts @@ -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; + + 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/'); + }) + }); +}); diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.ts b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.ts new file mode 100644 index 0000000000..9bbdbfb9a1 --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.ts @@ -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 { + protected frontendURL = '/collections/'; + + public constructor( + protected collectionDataService: CollectionDataService, + protected router: Router, + protected route: ActivatedRoute + ) { + super(collectionDataService, router, route); + } +} diff --git a/src/app/+community-page/community-form/community-form.component.ts b/src/app/+community-page/community-form/community-form.component.ts new file mode 100644 index 0000000000..9ae6f0955d --- /dev/null +++ b/src/app/+community-page/community-form/community-form.component.ts @@ -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 { + /** + * @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', + }), + ]; +} diff --git a/src/app/+community-page/community-page-routing.module.ts b/src/app/+community-page/community-page-routing.module.ts index 4cc927d341..02f28f6375 100644 --- a/src/app/+community-page/community-page-routing.module.ts +++ b/src/app/+community-page/community-page-routing.module.ts @@ -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 { diff --git a/src/app/+community-page/community-page.component.html b/src/app/+community-page/community-page.component.html index a86a86c3da..b3a2f90bb3 100644 --- a/src/app/+community-page/community-page.component.html +++ b/src/app/+community-page/community-page.component.html @@ -28,6 +28,7 @@ + diff --git a/src/app/+community-page/community-page.component.ts b/src/app/+community-page/community-page.component.ts index 9a143f9a02..b9119b4139 100644 --- a/src/app/+community-page/community-page.component.ts +++ b/src/app/+community-page/community-page.component.ts @@ -1,4 +1,4 @@ -import { mergeMap, filter, map, first, tap } from 'rxjs/operators'; +import { mergeMap, filter, map } from 'rxjs/operators'; import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; diff --git a/src/app/+community-page/community-page.module.ts b/src/app/+community-page/community-page.module.ts index d7f97755c2..6d63cadcc8 100644 --- a/src/app/+community-page/community-page.module.ts +++ b/src/app/+community-page/community-page.module.ts @@ -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 { } diff --git a/src/app/+community-page/create-community-page/create-community-page.component.html b/src/app/+community-page/create-community-page/create-community-page.component.html new file mode 100644 index 0000000000..55a080d2a1 --- /dev/null +++ b/src/app/+community-page/create-community-page/create-community-page.component.html @@ -0,0 +1,11 @@ +
+
+
+ + +

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

+
+
+
+ +
diff --git a/src/app/+community-page/create-community-page/create-community-page.component.scss b/src/app/+community-page/create-community-page/create-community-page.component.scss new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/app/+community-page/create-community-page/create-community-page.component.scss @@ -0,0 +1 @@ + diff --git a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts new file mode 100644 index 0000000000..dba15dbe88 --- /dev/null +++ b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts @@ -0,0 +1,42 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { RouteService } from '../../shared/services/route.service'; +import { SharedModule } from '../../shared/shared.module'; +import { CollectionDataService } from '../../core/data/collection-data.service'; +import { of as observableOf } from 'rxjs'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { CreateCommunityPageComponent } from './create-community-page.component'; + +describe('CreateCommunityPageComponent', () => { + let comp: CreateCommunityPageComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + declarations: [CreateCommunityPageComponent], + providers: [ + { provide: CommunityDataService, useValue: { findById: () => observableOf({}) } }, + { provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } }, + { provide: Router, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CreateCommunityPageComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe('frontendURL', () => { + it('should have the right frontendURL set', () => { + expect((comp as any).frontendURL).toEqual('/communities/'); + }) + }); +}); diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts new file mode 100644 index 0000000000..47fb065038 --- /dev/null +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -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 { + protected frontendURL = '/communities/'; + + public constructor( + protected communityDataService: CommunityDataService, + protected routeService: RouteService, + protected router: Router + ) { + super(communityDataService, communityDataService, routeService, router); + } +} diff --git a/src/app/+community-page/create-community-page/create-community-page.guard.spec.ts b/src/app/+community-page/create-community-page/create-community-page.guard.spec.ts new file mode 100644 index 0000000000..0cc7232871 --- /dev/null +++ b/src/app/+community-page/create-community-page/create-community-page.guard.spec.ts @@ -0,0 +1,67 @@ +import { CreateCommunityPageGuard } from './create-community-page.guard'; +import { MockRouter } from '../../shared/mocks/mock-router'; +import { RemoteData } from '../../core/data/remote-data'; +import { Community } from '../../core/shared/community.model'; +import { of as observableOf } from 'rxjs'; +import { first } from 'rxjs/operators'; + +describe('CreateCommunityPageGuard', () => { + describe('canActivate', () => { + let guard: CreateCommunityPageGuard; + let router; + let communityDataServiceStub: any; + + beforeEach(() => { + communityDataServiceStub = { + findById: (id: string) => { + if (id === 'valid-id') { + return observableOf(new RemoteData(false, false, true, null, new Community())); + } else if (id === 'invalid-id') { + return observableOf(new RemoteData(false, false, true, null, undefined)); + } else if (id === 'error-id') { + return observableOf(new RemoteData(false, false, false, null, new Community())); + } + } + }; + router = new MockRouter(); + + guard = new CreateCommunityPageGuard(router, communityDataServiceStub); + }); + + it('should return true when the parent ID resolves to a community', () => { + guard.canActivate({ queryParams: { parent: 'valid-id' } } as any, undefined) + .pipe(first()) + .subscribe( + (canActivate) => + expect(canActivate).toEqual(true) + ); + }); + + it('should return true when no parent ID has been provided', () => { + guard.canActivate({ queryParams: { } } as any, undefined) + .pipe(first()) + .subscribe( + (canActivate) => + expect(canActivate).toEqual(true) + ); + }); + + it('should return false when the parent ID does not resolve to a community', () => { + guard.canActivate({ queryParams: { parent: 'invalid-id' } } as any, undefined) + .pipe(first()) + .subscribe( + (canActivate) => + expect(canActivate).toEqual(false) + ); + }); + + it('should return false when the parent ID resolves to an error response', () => { + guard.canActivate({ queryParams: { parent: 'error-id' } } as any, undefined) + .pipe(first()) + .subscribe( + (canActivate) => + expect(canActivate).toEqual(false) + ); + }); + }); +}); diff --git a/src/app/+community-page/create-community-page/create-community-page.guard.ts b/src/app/+community-page/create-community-page/create-community-page.guard.ts new file mode 100644 index 0000000000..2ee5cb6064 --- /dev/null +++ b/src/app/+community-page/create-community-page/create-community-page.guard.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; + +import { hasNoValue, hasValue } from '../../shared/empty.util'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { RemoteData } from '../../core/data/remote-data'; +import { Community } from '../../core/shared/community.model'; +import { getFinishedRemoteData } from '../../core/shared/operators'; +import { map, tap } from 'rxjs/operators'; +import { Observable, of as observableOf } from 'rxjs'; + +/** + * Prevent creation of a community with an invalid parent community provided + * @class CreateCommunityPageGuard + */ +@Injectable() +export class CreateCommunityPageGuard implements CanActivate { + public constructor(private router: Router, private communityService: CommunityDataService) { + } + + /** + * True when either NO parent ID query parameter has been provided, or the parent ID resolves to a valid parent community + * Reroutes to a 404 page when the page cannot be activated + * @method canActivate + */ + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + const parentID = route.queryParams.parent; + if (hasNoValue(parentID)) { + return observableOf(true); + } + + const parent: Observable> = this.communityService.findById(parentID) + .pipe( + getFinishedRemoteData(), + ); + + return parent.pipe( + map((communityRD: RemoteData) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)), + tap((isValid: boolean) => { + if (!isValid) { + this.router.navigate(['/404']); + } + }) + ); + } +} diff --git a/src/app/+community-page/delete-community-page/delete-community-page.component.html b/src/app/+community-page/delete-community-page/delete-community-page.component.html new file mode 100644 index 0000000000..cfd09f2bbd --- /dev/null +++ b/src/app/+community-page/delete-community-page/delete-community-page.component.html @@ -0,0 +1,19 @@ +
+
+ +
+ +

{{ 'community.delete.text' | translate:{ dso: dso.name } }}

+ + +
+
+ +
+ +
diff --git a/src/app/+community-page/delete-community-page/delete-community-page.component.scss b/src/app/+community-page/delete-community-page/delete-community-page.component.scss new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/app/+community-page/delete-community-page/delete-community-page.component.scss @@ -0,0 +1 @@ + diff --git a/src/app/+community-page/delete-community-page/delete-community-page.component.spec.ts b/src/app/+community-page/delete-community-page/delete-community-page.component.spec.ts new file mode 100644 index 0000000000..f18c4fb1f1 --- /dev/null +++ b/src/app/+community-page/delete-community-page/delete-community-page.component.spec.ts @@ -0,0 +1,42 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { RouteService } from '../../shared/services/route.service'; +import { SharedModule } from '../../shared/shared.module'; +import { of as observableOf } from 'rxjs'; +import { 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; + + 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/'); + }) + }); +}); diff --git a/src/app/+community-page/delete-community-page/delete-community-page.component.ts b/src/app/+community-page/delete-community-page/delete-community-page.component.ts new file mode 100644 index 0000000000..01741a7577 --- /dev/null +++ b/src/app/+community-page/delete-community-page/delete-community-page.component.ts @@ -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 { + 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); + } +} diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.html b/src/app/+community-page/edit-community-page/edit-community-page.component.html new file mode 100644 index 0000000000..cedb771c14 --- /dev/null +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.html @@ -0,0 +1,12 @@ +
+
+
+ + + {{'community.edit.delete' + | translate}} +
+
+
diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.scss b/src/app/+community-page/edit-community-page/edit-community-page.component.scss new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.scss @@ -0,0 +1 @@ + diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts b/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts new file mode 100644 index 0000000000..54f2133ce7 --- /dev/null +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts @@ -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; + + 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/'); + }) + }); +}); diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.ts b/src/app/+community-page/edit-community-page/edit-community-page.component.ts new file mode 100644 index 0000000000..68f092e915 --- /dev/null +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.ts @@ -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 { + protected frontendURL = '/communities/'; + + public constructor( + protected communityDataService: CommunityDataService, + protected router: Router, + protected route: ActivatedRoute + ) { + super(communityDataService, router, route); + } +} diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts index 0c985e37f9..ace748c7de 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts @@ -17,7 +17,6 @@ describe('SubCommunityList Component', () => { let fixture: ComponentFixture; const subcommunities = [Object.assign(new Community(), { - name: 'SubCommunity 1', id: '123456789-1', metadata: [ { @@ -27,7 +26,6 @@ describe('SubCommunityList Component', () => { }] }), Object.assign(new Community(), { - name: 'SubCommunity 2', id: '123456789-2', metadata: [ { diff --git a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts index f27dc95bac..6d435c8de8 100644 --- a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts @@ -1,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 {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; @@ -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')]); }); }); diff --git a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts index cc09565655..2700b45475 100644 --- a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts +++ b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts @@ -1,7 +1,7 @@ -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 { 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({ @@ -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 { diff --git a/src/app/+item-page/edit-item-page/item-private/item-private.component.ts b/src/app/+item-page/edit-item-page/item-private/item-private.component.ts index 4947f920d0..d949e4fa6e 100644 --- a/src/app/+item-page/edit-item-page/item-private/item-private.component.ts +++ b/src/app/+item-page/edit-item-page/item-private/item-private.component.ts @@ -1,8 +1,8 @@ -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 { 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({ diff --git a/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts b/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts index 3b9f8ed00a..97c81681d0 100644 --- a/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts @@ -1,20 +1,20 @@ -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 { 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; diff --git a/src/app/+item-page/edit-item-page/item-public/item-public.component.ts b/src/app/+item-page/edit-item-page/item-public/item-public.component.ts index 3d8d2755d8..272cf9a96f 100644 --- a/src/app/+item-page/edit-item-page/item-public/item-public.component.ts +++ b/src/app/+item-page/edit-item-page/item-public/item-public.component.ts @@ -1,8 +1,8 @@ -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 { 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({ diff --git a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts index 98897cf2ae..e89eda736f 100644 --- a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts @@ -1,20 +1,20 @@ -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 { 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; diff --git a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts index 94f03d10bd..9c0e1c8d05 100644 --- a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts +++ b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts @@ -1,8 +1,8 @@ -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 { 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({ diff --git a/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts index 130cdd0d25..9305459c12 100644 --- a/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts @@ -1,20 +1,20 @@ -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 { 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; diff --git a/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts index 7681fa68b5..1fed1756a4 100644 --- a/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts +++ b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts @@ -1,8 +1,8 @@ -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 { 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({ diff --git a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts index 7d3b75c97e..1c4cae552e 100644 --- a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts +++ b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts @@ -1,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 {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 { 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'; /** diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts index faaf3b9fb5..fd5a75e7d1 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts @@ -6,7 +6,7 @@ import { Subject, Subscription } from 'rxjs'; -import { switchMap, distinctUntilChanged, first, map, take } from 'rxjs/operators'; +import { switchMap, distinctUntilChanged, map, take } from 'rxjs/operators'; import { animate, state, style, transition, trigger } from '@angular/animations'; import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { Router } from '@angular/router'; diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts index ec239e3628..dcc01f2b46 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts @@ -1,5 +1,5 @@ -import { first, take } from 'rxjs/operators'; +import { take } from 'rxjs/operators'; import { Component, Input, OnInit } from '@angular/core'; import { SearchFilterConfig } from '../../search-service/search-filter-config.model'; import { SearchFilterService } from './search-filter.service'; diff --git a/src/app/+search-page/search-results/search-results.component.spec.ts b/src/app/+search-page/search-results/search-results.component.spec.ts index 54463d916d..b7ac11553a 100644 --- a/src/app/+search-page/search-results/search-results.component.spec.ts +++ b/src/app/+search-page/search-results/search-results.component.spec.ts @@ -111,7 +111,6 @@ export const objects = [ id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', type: ResourceType.Community, - name: 'OR2017 - Demonstration', metadata: [ { key: 'dc.description', @@ -161,7 +160,6 @@ export const objects = [ id: '9076bd16-e69a-48d6-9e41-0238cb40d863', uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863', type: ResourceType.Community, - name: 'Sample Community', metadata: [ { key: 'dc.description', diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 020dfcad98..10c6643fbb 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { filter, first, map, take } from 'rxjs/operators'; +import { filter, map, take } from 'rxjs/operators'; import { AfterViewInit, ChangeDetectionStrategy, diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index f957d807c1..6d782cbbe2 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -25,8 +25,6 @@ export class AuthRequestService { protected fetchRequest(request: RestRequest): Observable { return this.requestService.getByUUID(request.uuid).pipe( getResponseFromEntry(), - // TODO to review when https://github.com/DSpace/dspace-angular/issues/217 will be fixed - // tap(() => this.responseCache.remove(request.href)), mergeMap((response) => { if (response.isSuccessful && isNotEmpty(response)) { return observableOf((response as AuthStatusResponse).response); diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index 56a5411ef2..1e68802af8 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -1,6 +1,6 @@ import { of as observableOf, Observable } from 'rxjs'; -import { filter, debounceTime, switchMap, take, tap, catchError, map, first } from 'rxjs/operators'; +import { filter, debounceTime, switchMap, take, tap, catchError, map } from 'rxjs/operators'; import { Injectable } from '@angular/core'; // import @ngrx diff --git a/src/app/core/auth/server-auth.service.ts b/src/app/core/auth/server-auth.service.ts index 903926fbcf..25ec1156ee 100644 --- a/src/app/core/auth/server-auth.service.ts +++ b/src/app/core/auth/server-auth.service.ts @@ -1,4 +1,4 @@ -import { first, map, switchMap, take } from 'rxjs/operators'; +import { map, switchMap, take } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; diff --git a/src/app/core/cache/builders/normalized-object-build.service.ts b/src/app/core/cache/builders/normalized-object-build.service.ts new file mode 100644 index 0000000000..9d97ccda75 --- /dev/null +++ b/src/app/core/cache/builders/normalized-object-build.service.ts @@ -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(domainModel: TDomain): TNormalized { + const normalizedConstructor = NormalizedObjectFactory.getConstructor(domainModel.type); + const relationships = getRelationships(normalizedConstructor) || []; + + const normalizedModel = Object.assign({}, domainModel) as any; + relationships.forEach((key: string) => { + if (hasValue(domainModel[key])) { + domainModel[key] = undefined; + } + }); + return normalizedModel; + } +} diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 96945cea81..f9707fc723 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -45,7 +45,6 @@ export class RemoteDataBuildService { href$.pipe(getRequestFromRequestHref(this.requestService)), requestUUID$.pipe(getRequestFromRequestUUID(this.requestService)), ); - // always use self link if that is cached, only if it isn't, get it via the response. const payload$ = observableCombineLatest( diff --git a/src/app/core/cache/models/normalized-community.model.ts b/src/app/core/cache/models/normalized-community.model.ts index 4ab2408a53..e915d2f50a 100644 --- a/src/app/core/cache/models/normalized-community.model.ts +++ b/src/app/core/cache/models/normalized-community.model.ts @@ -1,4 +1,4 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; +import { autoserialize, deserialize, inheritSerialization, serialize } from 'cerialize'; import { NormalizedDSpaceObject } from './normalized-dspace-object.model'; import { Community } from '../../shared/community.model'; @@ -21,32 +21,32 @@ export class NormalizedCommunity extends NormalizedDSpaceObject { /** * The Bitstream that represents the logo of this Community */ - @autoserialize + @deserialize @relationship(ResourceType.Bitstream, false) logo: string; /** * An array of Communities that are direct parents of this Community */ - @autoserialize + @deserialize @relationship(ResourceType.Community, true) parents: string[]; /** * The Community that owns this Community */ - @autoserialize + @deserialize @relationship(ResourceType.Community, false) owner: string; /** * List of Collections that are owned by this Community */ - @autoserialize + @deserialize @relationship(ResourceType.Collection, true) collections: string[]; - @autoserialize + @deserialize @relationship(ResourceType.Community, true) subcommunities: string[]; diff --git a/src/app/core/cache/models/normalized-dspace-object.model.ts b/src/app/core/cache/models/normalized-dspace-object.model.ts index 92174c40f7..efdfa6dd74 100644 --- a/src/app/core/cache/models/normalized-dspace-object.model.ts +++ b/src/app/core/cache/models/normalized-dspace-object.model.ts @@ -1,4 +1,4 @@ -import { autoserialize, autoserializeAs } from 'cerialize'; +import { autoserialize, autoserializeAs, deserialize, serialize } from 'cerialize'; import { DSpaceObject } from '../../shared/dspace-object.model'; import { Metadatum } from '../../shared/metadatum.model'; @@ -45,12 +45,6 @@ export class NormalizedDSpaceObject extends NormalizedObject { @autoserialize type: ResourceType; - /** - * The name for this DSpaceObject - */ - @autoserialize - name: string; - /** * An array containing all metadata of this DSpaceObject */ @@ -60,13 +54,13 @@ export class NormalizedDSpaceObject extends NormalizedObject { /** * An array of DSpaceObjects that are direct parents of this DSpaceObject */ - @autoserialize + @deserialize parents: string[]; /** * The DSpaceObject that owns this DSpaceObject */ - @autoserialize + @deserialize owner: string; /** @@ -75,7 +69,7 @@ export class NormalizedDSpaceObject extends NormalizedObject { * Repeated here to make the serialization work, * inheritSerialization doesn't seem to work for more than one level */ - @autoserialize + @deserialize _links: { [name: string]: string } diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index 867f31e1bb..982c77341e 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -17,12 +17,22 @@ 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[]; } -/**conca +/** * An interface to represent objects that can be cached * * A cacheable object should have a self link diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 40f41be14d..af30646f53 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -1,6 +1,6 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { distinctUntilChanged, filter, first, map, mergeMap, take, } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { MemoizedSelector, select, Store } from '@ngrx/store'; import { IndexName } from '../index/index.reducer'; diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts index d0a194705b..0d7392e555 100644 --- a/src/app/core/cache/server-sync-buffer.effects.ts +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -1,4 +1,4 @@ -import { delay, exhaustMap, first, map, switchMap, take, tap } from 'rxjs/operators'; +import { delay, exhaustMap, map, switchMap, take } from 'rxjs/operators'; import { Inject, Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 7b6516f18a..aba53e2a7f 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -65,6 +65,8 @@ import { DSpaceObjectDataService } from './data/dspace-object-data.service'; import { MetadataschemaParsingService } from './data/metadataschema-parsing.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, @@ -101,6 +103,7 @@ const PROVIDERS = [ ObjectCacheService, PaginationComponentOptions, RegistryService, + NormalizedObjectBuildService, RemoteDataBuildService, RequestService, EndpointMapResponseParsingService, @@ -128,6 +131,7 @@ const PROVIDERS = [ UploaderService, UUIDService, DSpaceObjectDataService, + DSOChangeAnalyzer, CSSVariableService, MenuService, // register AuthInterceptor as HttpInterceptor diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index eada156ce9..925caa495c 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -8,15 +8,7 @@ import { GenericConstructor } from '../shared/generic-constructor'; import { PaginatedList } from './paginated-list'; import { ResourceType } from '../shared/resource-type'; import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; - -function isObjectLevel(halObj: any) { - return isNotEmpty(halObj._links) && hasValue(halObj._links.self); -} - -function isPaginatedResponse(halObj: any) { - return hasValue(halObj.page) && hasValue(halObj._embedded); -} - +import { isRestDataObject, isRestPaginatedList } from '../cache/builders/normalized-object-build.service'; /* tslint:disable:max-classes-per-file */ export abstract class BaseResponseParsingService { @@ -29,11 +21,11 @@ export abstract class BaseResponseParsingService { if (isNotEmpty(data)) { if (hasNoValue(data) || (typeof data !== 'object')) { return data; - } else if (isPaginatedResponse(data)) { + } else if (isRestPaginatedList(data)) { return this.processPaginatedList(data, requestUUID); } else if (Array.isArray(data)) { return this.processArray(data, requestUUID); - } else if (isObjectLevel(data)) { + } else if (isRestDataObject(data)) { data = this.fixBadEPersonRestResponse(data); const object = this.deserialize(data); if (isNotEmpty(data._embedded)) { @@ -43,10 +35,10 @@ export abstract class BaseResponseParsingService { .forEach((property) => { const parsedObj = this.process(data._embedded[property], requestUUID); if (isNotEmpty(parsedObj)) { - if (isPaginatedResponse(data._embedded[property])) { + if (isRestPaginatedList(data._embedded[property])) { object[property] = parsedObj; object[property].page = parsedObj.page.map((obj) => obj.self); - } else if (isObjectLevel(data._embedded[property])) { + } else if (isRestDataObject(data._embedded[property])) { object[property] = parsedObj.self; } else if (Array.isArray(parsedObj)) { object[property] = parsedObj.map((obj) => obj.self) @@ -80,7 +72,7 @@ export abstract class BaseResponseParsingService { list = this.flattenSingleKeyObject(list); } const page: ObjectDomain[] = this.processArray(list, requestUUID); - return new PaginatedList(pageInfo, page); + return new PaginatedList(pageInfo, page, ); } protected processArray(data: any, requestUUID: string): ObjectDomain[] { diff --git a/src/app/core/data/change-analyzer.ts b/src/app/core/data/change-analyzer.ts new file mode 100644 index 0000000000..caf9e38c7c --- /dev/null +++ b/src/app/core/data/change-analyzer.ts @@ -0,0 +1,20 @@ +import { NormalizedObject } from '../cache/models/normalized-object.model'; +import { Operation } from 'fast-json-patch/lib/core'; + +/** + * An interface to determine what differs between two + * NormalizedObjects + */ +export interface ChangeAnalyzer { + + /** + * Compare two objects and return their differences as a + * JsonPatch Operation Array + * + * @param {NormalizedObject} object1 + * The first object to compare + * @param {NormalizedObject} object2 + * The second object to compare + */ + diff(object1: TNormalized, object2: TNormalized): Operation[]; +} diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 74c73e37f3..e8a682ba0e 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NormalizedCollection } from '../cache/models/normalized-collection.model'; @@ -9,6 +9,10 @@ import { ComColDataService } from './comcol-data.service'; import { CommunityDataService } from './community-data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; @Injectable() export class CollectionDataService extends ComColDataService { @@ -17,11 +21,16 @@ export class CollectionDataService extends ComColDataService, protected cds: CommunityDataService, + protected objectCache: ObjectCacheService, protected halService: HALEndpointService, - protected objectCache: ObjectCacheService + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer ) { super(); } + } diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index 867d559c70..4c20a4cfeb 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -14,6 +14,10 @@ import { NormalizedObject } from '../cache/models/normalized-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RequestEntry } from './request.reducer'; import { of as observableOf } from 'rxjs'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; const LINK_NAME = 'test'; @@ -26,11 +30,15 @@ class TestService extends ComColDataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected EnvConfig: GlobalConfig, protected cds: CommunityDataService, - protected halService: HALEndpointService, protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer, protected linkPath: string ) { super(); @@ -45,11 +53,15 @@ describe('ComColDataService', () => { let requestService: RequestService; let cds: CommunityDataService; let objectCache: ObjectCacheService; - const halService: any = {}; + let halService: any = {}; const rdbService = {} as RemoteDataBuildService; const store = {} as Store; const EnvConfig = {} as GlobalConfig; + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = {} as any; + const dataBuildService = {} as NormalizedObjectBuildService; const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d'; const options = Object.assign(new FindAllOptions(), { @@ -65,11 +77,16 @@ describe('ComColDataService', () => { const communityEndpoint = `${communitiesEndpoint}/${scopeID}`; const scopedEndpoint = `${communityEndpoint}/${LINK_NAME}`; const serviceEndpoint = `https://rest.api/core/${LINK_NAME}`; + const authHeader = 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJlaWQiOiJhNjA4NmIzNC0zOTE4LTQ1YjctOGRkZC05MzI5YTcwMmEyNmEiLCJzZyI6W10sImV4cCI6MTUzNDk0MDcyNX0.RV5GAtiX6cpwBN77P_v16iG9ipeyiO7faNYSNMzq_sQ'; + + const mockHalService = { + getEndpoint: (linkPath) => observableOf(communitiesEndpoint) + }; function initMockCommunityDataService(): CommunityDataService { return jasmine.createSpyObj('responseCache', { getEndpoint: hot('--a-', { a: communitiesEndpoint }), - getFindByIDHref: cold('b-', { b: communityEndpoint }) + getIDHref: cold('b-', { b: communityEndpoint }) }); } @@ -89,15 +106,27 @@ describe('ComColDataService', () => { return new TestService( requestService, rdbService, + dataBuildService, store, EnvConfig, cds, - halService, objectCache, + halService, + notificationsService, + http, + comparator, LINK_NAME ); } + beforeEach(() => { + cds = initMockCommunityDataService(); + requestService = getMockRequestService(); + objectCache = initMockObjectCacheService(); + halService = mockHalService; + service = initTestService(); + }); + describe('getBrowseEndpoint', () => { beforeEach(() => { scheduler = getTestScheduler(); @@ -156,4 +185,5 @@ describe('ComColDataService', () => { }); }); + }); diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index d3eed88ffd..8a1ea51bb3 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -20,8 +20,9 @@ import { NormalizedObject } from '../cache/models/normalized-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RequestEntry } from './request.reducer'; import { getResponseFromEntry } from '../shared/operators'; +import { CacheableObject } from '../cache/object-cache.reducer'; -export abstract class ComColDataService extends DataService { +export abstract class ComColDataService extends DataService { protected abstract cds: CommunityDataService; protected abstract objectCache: ObjectCacheService; protected abstract halService: HALEndpointService; @@ -41,7 +42,7 @@ export abstract class ComColDataService this.cds.getFindByIDHref(endpoint, options.scopeID)), + mergeMap((endpoint: string) => this.cds.getIDHref(endpoint, options.scopeID)), filter((href: string) => isNotEmpty(href)), take(1), tap((href: string) => { diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index a037936202..d09a0b9757 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -15,6 +15,10 @@ import { RemoteData } from './remote-data'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { Observable } from 'rxjs'; import { PaginatedList } from './paginated-list'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; @Injectable() export class CommunityDataService extends ComColDataService { @@ -25,9 +29,13 @@ export class CommunityDataService extends ComColDataService, + protected objectCache: ObjectCacheService, protected halService: HALEndpointService, - protected objectCache: ObjectCacheService + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer ) { super(); } diff --git a/src/app/core/data/config-response-parsing.service.spec.ts b/src/app/core/data/config-response-parsing.service.spec.ts index a33c5cf5b5..a2c5cbbadc 100644 --- a/src/app/core/data/config-response-parsing.service.spec.ts +++ b/src/app/core/data/config-response-parsing.service.spec.ts @@ -161,7 +161,13 @@ describe('ConfigResponseParsingService', () => { page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } }, statusCode: '500' }; - const pageinfo = Object.assign(new PageInfo(), { elementsPerPage: 4, totalElements: 4, totalPages: 1, currentPage: 1 }); + const pageinfo = Object.assign(new PageInfo(), { + elementsPerPage: 4, + totalElements: 4, + totalPages: 1, + currentPage: 1, + self: 'https://rest.api/config/submissiondefinitions/traditional/sections' + }); const definitions = Object.assign(new SubmissionDefinitionsModel(), { isDefault: true, diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index 7da709abd5..9b39f0b68e 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -5,13 +5,17 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv import { CoreState } from '../core.reducers'; import { Store } from '@ngrx/store'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { Observable } from 'rxjs'; +import { Observable, of as observableOf } from 'rxjs'; import { FindAllOptions } from './request.models'; -import { SortOptions, SortDirection } from '../cache/models/sort-options.model'; -import { of as observableOf } from 'rxjs'; +import { SortDirection, SortOptions } from '../cache/models/sort-options.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { Operation } from '../../../../node_modules/fast-json-patch'; import { DSpaceObject } from '../shared/dspace-object.model'; +import { ChangeAnalyzer } from './change-analyzer'; +import { HttpClient } from '@angular/common/http'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { compare } from 'fast-json-patch'; const endpoint = 'https://rest.api/core'; @@ -23,10 +27,14 @@ class TestService extends DataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, protected store: Store, protected linkPath: string, protected halService: HALEndpointService, - protected objectCache: ObjectCacheService + protected objectCache: ObjectCacheService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: ChangeAnalyzer ) { super(); } @@ -36,12 +44,24 @@ class TestService extends DataService { } } +class DummyChangeAnalyzer implements ChangeAnalyzer { + diff(object1: NormalizedTestObject, object2: NormalizedTestObject): Operation[] { + return compare((object1 as any).metadata, (object2 as any).metadata); + } + +} describe('DataService', () => { let service: TestService; let options: FindAllOptions; const requestService = {} as RequestService; const halService = {} as HALEndpointService; const rdbService = {} as RemoteDataBuildService; + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = new DummyChangeAnalyzer() as any; + const dataBuildService = { + normalize: (object) => object + } as NormalizedObjectBuildService; const objectCache = { addPatch: () => { /* empty */ @@ -56,13 +76,16 @@ describe('DataService', () => { return new TestService( requestService, rdbService, + dataBuildService, store, endpoint, halService, - objectCache + objectCache, + notificationsService, + http, + comparator, ); } - service = initTestService(); describe('getFindAllHref', () => { @@ -120,7 +143,7 @@ describe('DataService', () => { elementsPerPage: 10, sort: sortOptions, startsWith: 'ab' - } + }; const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` + `&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`; @@ -134,7 +157,7 @@ describe('DataService', () => { let selfLink; beforeEach(() => { - operations = [{ op: 'replace', path: '/name', value: 'random string' } as Operation]; + operations = [{ op: 'replace', path: '/metadata/dc.title', value: 'random string' } as Operation]; selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; spyOn(objectCache, 'addPatch'); }); @@ -153,28 +176,29 @@ describe('DataService', () => { const name1 = 'random string'; const name2 = 'another random string'; beforeEach(() => { - operations = [{ op: 'replace', path: '/name', value: name2 } as Operation]; + operations = [{ op: 'replace', path: '/0/value', value: name2 } as Operation]; selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; dso = new DSpaceObject(); dso.self = selfLink; - dso.name = name1; + dso.metadata = [{ key: 'dc.title', value: name1 }]; dso2 = new DSpaceObject(); dso2.self = selfLink; - dso2.name = name2; + dso2.metadata = [{ key: 'dc.title', value: name2 }]; - spyOn(objectCache, 'getBySelfLink').and.returnValue(dso); + spyOn(service, 'findById').and.returnValues(observableOf(dso)); + spyOn(objectCache, 'getBySelfLink').and.returnValues(observableOf(dso)); spyOn(objectCache, 'addPatch'); }); it('should call addPatch on the object cache with the right parameters when there are differences', () => { - service.update(dso2); + service.update(dso2).subscribe(); expect(objectCache.addPatch).toHaveBeenCalledWith(selfLink, operations); }); it('should not call addPatch on the object cache with the right parameters when there are no differences', () => { - service.update(dso); + service.update(dso).subscribe(); expect(objectCache.addPatch).not.toHaveBeenCalled(); }); }); diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 6afc84df5a..440013c31c 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -1,28 +1,58 @@ -import { delay, distinctUntilChanged, filter, find, first, map, take, tap } from 'rxjs/operators'; +import { + distinctUntilChanged, + filter, + find, + first, + map, + mergeMap, + switchMap, + take +} from 'rxjs/operators'; import { Observable } from 'rxjs'; import { Store } from '@ngrx/store'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { CoreState } from '../core.reducers'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { URLCombiner } from '../url-combiner/url-combiner'; import { PaginatedList } from './paginated-list'; import { RemoteData } from './remote-data'; -import { FindAllOptions, FindAllRequest, FindByIDRequest, GetRequest } from './request.models'; +import { + CreateRequest, + DeleteByIDRequest, + FindAllOptions, + FindAllRequest, + FindByIDRequest, + GetRequest +} from './request.models'; import { RequestService } from './request.service'; import { NormalizedObject } from '../cache/models/normalized-object.model'; -import { compare, Operation } from 'fast-json-patch'; +import { Operation } from 'fast-json-patch'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { of } from 'rxjs/internal/observable/of'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { configureRequest, getResponseFromEntry } from '../shared/operators'; +import { ErrorResponse, RestResponse } from '../cache/response.models'; +import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; +import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; +import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; +import { CacheableObject } from '../cache/object-cache.reducer'; +import { RequestEntry } from './request.reducer'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { ChangeAnalyzer } from './change-analyzer'; -export abstract class DataService { +export abstract class DataService { protected abstract requestService: RequestService; protected abstract rdbService: RemoteDataBuildService; + protected abstract dataBuildService: NormalizedObjectBuildService; protected abstract store: Store; protected abstract linkPath: string; protected abstract halService: HALEndpointService; protected abstract objectCache: ObjectCacheService; + protected abstract notificationsService: NotificationsService; + protected abstract http: HttpClient; + protected abstract comparator: ChangeAnalyzer; public abstract getBrowseEndpoint(options: FindAllOptions, linkPath?: string): Observable @@ -65,13 +95,18 @@ export abstract class DataService return this.rdbService.buildList(hrefObs) as Observable>>; } - getFindByIDHref(endpoint, resourceID): string { + /** + * Create the HREF for a specific object based on its identifier + * @param endpoint The base endpoint for the type of object + * @param resourceID The identifier for the object + */ + getIDHref(endpoint, resourceID): string { return `${endpoint}/${resourceID}`; } findById(id: string): Observable> { const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( - map((endpoint: string) => this.getFindByIDHref(endpoint, id))); + map((endpoint: string) => this.getIDHref(endpoint, id))); hrefObs.pipe( find((href: string) => hasValue(href))) @@ -102,29 +137,96 @@ export abstract class DataService * The patch is derived from the differences between the given object and its version in the object cache * @param {DSpaceObject} object The given object */ - update(object: DSpaceObject) { - const oldVersion = this.objectCache.getBySelfLink(object.self); - const operations = compare(oldVersion, object); - if (isNotEmpty(operations)) { - this.objectCache.addPatch(object.self, operations); - } + update(object: TDomain): Observable> { + const oldVersion$ = this.objectCache.getBySelfLink(object.self); + return oldVersion$.pipe(first(), mergeMap((oldVersion: TNormalized) => { + const newVersion = this.dataBuildService.normalize(object); + const operations = this.comparator.diff(oldVersion, newVersion); + if (isNotEmpty(operations)) { + this.objectCache.addPatch(object.self, operations); + } + return this.findById(object.uuid); + } + )); + + } + + /** + * Create a new DSpaceObject on the server, and store the response + * in the object cache + * + * @param {DSpaceObject} dso + * The object to create + * @param {string} parentUUID + * The UUID of the parent to create the new object under + */ + create(dso: TDomain, parentUUID: string): Observable> { + const requestId = this.requestService.generateRequestId(); + const endpoint$ = this.halService.getEndpoint(this.linkPath).pipe( + isNotEmptyOperator(), + distinctUntilChanged(), + map((endpoint: string) => parentUUID ? `${endpoint}?parentCommunity=${parentUUID}` : endpoint) + ); + + const normalizedObject: TNormalized = this.dataBuildService.normalize(dso); + const serializedDso = new DSpaceRESTv2Serializer(NormalizedObjectFactory.getConstructor(dso.type)).serialize(normalizedObject); + + const request$ = endpoint$.pipe( + take(1), + map((endpoint: string) => new CreateRequest(requestId, endpoint, JSON.stringify(serializedDso))) + ); + + // Execute the post request + request$.pipe( + configureRequest(this.requestService) + ).subscribe(); + + // Resolve self link for new object + const selfLink$ = this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry(), + map((response: RestResponse) => { + if (!response.isSuccessful && response instanceof ErrorResponse) { + this.notificationsService.error('Server Error:', response.errorMessage, new NotificationOptions(-1)); + } else { + return response; + } + }), + map((response: any) => { + if (isNotEmpty(response.resourceSelfLinks)) { + return response.resourceSelfLinks[0]; + } + }), + distinctUntilChanged() + ) as Observable; + + return selfLink$.pipe( + switchMap((selfLink: string) => this.findByHref(selfLink)), + ) + } + + /** + * Delete an existing DSpace Object on the server + * @param dso The DSpace Object to be removed + * Return an observable that emits true when the deletion was successful, false when it failed + */ + delete(dso: TDomain): Observable { + const requestId = this.requestService.generateRequestId(); + + const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( + map((endpoint: string) => this.getIDHref(endpoint, dso.uuid))); + + hrefObs.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + const request = new DeleteByIDRequest(requestId, href, dso.uuid); + this.requestService.configure(request); + }) + ).subscribe(); + + return this.requestService.getByUUID(requestId).pipe( + find((request: RequestEntry) => request.completed), + map((request: RequestEntry) => request.response.isSuccessful) + ); } - // TODO implement, after the structure of the REST server's POST response is finalized - // create(dso: DSpaceObject): Observable> { - // const postHrefObs = this.getEndpoint(); - // - // // TODO ID is unknown at this point - // const idHrefObs = postHrefObs.map((href: string) => this.getFindByIDHref(href, dso.id)); - // - // postHrefObs - // .filter((href: string) => hasValue(href)) - // .take(1) - // .subscribe((href: string) => { - // const request = new RestRequest(this.requestService.generateRequestId(), href, RestRequestMethod.POST, dso); - // this.requestService.configure(request); - // }); - // - // return this.rdbService.buildSingle(idHrefObs, this.normalizedResourceType); - // } } diff --git a/src/app/core/data/dso-change-analyzer.service.ts b/src/app/core/data/dso-change-analyzer.service.ts new file mode 100644 index 0000000000..a47359e5c0 --- /dev/null +++ b/src/app/core/data/dso-change-analyzer.service.ts @@ -0,0 +1,26 @@ +import { Operation } from 'fast-json-patch/lib/core'; +import { compare } from 'fast-json-patch'; +import { ChangeAnalyzer } from './change-analyzer'; +import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model'; +import { Injectable } from '@angular/core'; + +/** + * A class to determine what differs between two + * DSpaceObjects + */ +@Injectable() +export class DSOChangeAnalyzer implements ChangeAnalyzer { + + /** + * Compare the metadata of two DSpaceObjects and return the differences as + * a JsonPatch Operation Array + * + * @param {NormalizedDSpaceObject} object1 + * The first object to compare + * @param {NormalizedDSpaceObject} object2 + * The second object to compare + */ + diff(object1: NormalizedDSpaceObject, object2: NormalizedDSpaceObject): Operation[] { + return compare(object1.metadata, object2.metadata).map((operation: Operation) => Object.assign({}, operation, { path: '/metadata' + operation.path })); + } +} diff --git a/src/app/core/data/dspace-object-data.service.spec.ts b/src/app/core/data/dspace-object-data.service.spec.ts index cdddcb7ce6..7047db6065 100644 --- a/src/app/core/data/dspace-object-data.service.spec.ts +++ b/src/app/core/data/dspace-object-data.service.spec.ts @@ -7,6 +7,9 @@ import { FindByIDRequest } from './request.models'; import { RequestService } from './request.service'; import { DSpaceObjectDataService } from './dspace-object-data.service'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; describe('DSpaceObjectDataService', () => { let scheduler: TestScheduler; @@ -40,12 +43,20 @@ describe('DSpaceObjectDataService', () => { }) }); objectCache = {} as ObjectCacheService; + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = {} as any; + const dataBuildService = {} as NormalizedObjectBuildService; service = new DSpaceObjectDataService( requestService, rdbService, + dataBuildService, + objectCache, halService, - objectCache + notificationsService, + http, + comparator ) }); diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index 324692c676..86ce9be7a9 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -11,6 +11,10 @@ import { RemoteData } from './remote-data'; import { RequestService } from './request.service'; import { FindAllOptions } from './request.models'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; /* tslint:disable:max-classes-per-file */ class DataServiceImpl extends DataService { @@ -19,9 +23,13 @@ class DataServiceImpl extends DataService constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, protected store: Store, + protected objectCache: ObjectCacheService, protected halService: HALEndpointService, - protected objectCache: ObjectCacheService) { + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer) { super(); } @@ -29,7 +37,7 @@ class DataServiceImpl extends DataService return this.halService.getEndpoint(linkPath); } - getFindByIDHref(endpoint, resourceID): string { + getIDHref(endpoint, resourceID): string { return endpoint.replace(/\{\?uuid\}/,`?uuid=${resourceID}`); } } @@ -42,9 +50,13 @@ export class DSpaceObjectDataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected objectCache: ObjectCacheService, protected halService: HALEndpointService, - protected objectCache: ObjectCacheService) { - this.dataService = new DataServiceImpl(requestService, rdbService, null, halService, objectCache); + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer) { + this.dataService = new DataServiceImpl(requestService, rdbService, dataBuildService, null, objectCache, halService, notificationsService, http, comparator); } findById(uuid: string): Observable> { diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index dcea3bbd71..02c70791b5 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -7,10 +7,15 @@ import { CoreState } from '../core.reducers'; import { ItemDataService } from './item-data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; import { FindAllOptions, RestRequest } from './request.models'; -import { Observable } from 'rxjs/internal/Observable'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { Observable } from 'rxjs'; import { RestResponse } from '../cache/response.models'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { HttpClient } from '@angular/common/http'; +import { RequestEntry } from './request.reducer'; +import { of as observableOf } from 'rxjs'; describe('ItemDataService', () => { let scheduler: TestScheduler; @@ -22,12 +27,17 @@ describe('ItemDataService', () => { }, configure(request: RestRequest) { // Do nothing + }, + getByHref(requestHref: string) { + const responseCacheEntry = new RequestEntry(); + responseCacheEntry.response = new RestResponse(true, '200'); + return observableOf(responseCacheEntry); } } as RequestService; - const rdbService = {} as RemoteDataBuildService; - const objectCache = {} as ObjectCacheService; + const store = {} as Store; + const objectCache = {} as ObjectCacheService; const halEndpointService = { getEndpoint(linkPath: string): Observable { return cold('a', {a: itemEndpoint}); @@ -48,12 +58,16 @@ describe('ItemDataService', () => { const scopedEndpoint = `${itemBrowseEndpoint}?scope=${scopeID}`; const serviceEndpoint = `https://rest.api/core/items`; const browseError = new Error('getBrowseURL failed'); + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = {} as any; + const dataBuildService = {} as NormalizedObjectBuildService; const itemEndpoint = 'https://rest.api/core/items'; const ScopedItemEndpoint = `https://rest.api/core/items/${scopeID}`; function initMockBrowseService(isSuccessful: boolean) { const obs = isSuccessful ? - cold('--a-', {a: itemBrowseEndpoint}) : + cold('--a-', { a: itemBrowseEndpoint }) : cold('--#-', undefined, browseError); return jasmine.createSpyObj('bs', { getBrowseURLFor: obs @@ -64,10 +78,14 @@ describe('ItemDataService', () => { return new ItemDataService( requestService, rdbService, + dataBuildService, store, bs, + objectCache, halEndpointService, - objectCache + notificationsService, + http, + comparator ); } @@ -81,7 +99,7 @@ describe('ItemDataService', () => { service = initTestService(); const result = service.getBrowseEndpoint(options); - const expected = cold('--b-', {b: scopedEndpoint}); + const expected = cold('--b-', { b: scopedEndpoint }); expect(result).toBeObservable(expected); }); @@ -144,25 +162,4 @@ describe('ItemDataService', () => { }); }); - describe('getItemDeleteEndpoint', () => { - beforeEach(() => { - scheduler = getTestScheduler(); - service = initTestService(); - }); - - it('should return the endpoint to make an item private or public', () => { - const result = service.getItemDeleteEndpoint(scopeID); - const expected = cold('a', {a: ScopedItemEndpoint}); - - expect(result).toBeObservable(expected); - }); - - it('should delete the item', () => { - const expected = new RestResponse(true, '200'); - const result = service.delete(scopeID); - result.subscribe((v) => expect(v).toEqual(expected)); - - }); - }); - }); diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index aea565ad49..bd3c42a67c 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -14,9 +14,14 @@ import { URLCombiner } from '../url-combiner/url-combiner'; import { DataService } from './data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { DeleteRequest, FindAllOptions, PatchRequest, RestRequest } from './request.models'; +import { FindAllOptions, PatchRequest, RestRequest } from './request.models'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { configureRequest, getResponseFromEntry } from '../shared/operators'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { configureRequest, getRequestFromRequestHref } from '../shared/operators'; +import { RequestEntry } from './request.reducer'; @Injectable() export class ItemDataService extends DataService { @@ -25,10 +30,14 @@ export class ItemDataService extends DataService { constructor( protected requestService: RequestService, protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, protected store: Store, private bs: BrowseService, + protected objectCache: ObjectCacheService, protected halService: HALEndpointService, - protected objectCache: ObjectCacheService) { + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer) { super(); } @@ -55,7 +64,7 @@ export class ItemDataService extends DataService { */ public getItemWithdrawEndpoint(itemId: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( - map((endpoint: string) => this.getFindByIDHref(endpoint, itemId)) + map((endpoint: string) => this.getIDHref(endpoint, itemId)) ); } @@ -65,17 +74,7 @@ export class ItemDataService extends DataService { */ public getItemDiscoverableEndpoint(itemId: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( - map((endpoint: string) => this.getFindByIDHref(endpoint, itemId)) - ); - } - - /** - * Get the endpoint to delete the item - * @param itemId - */ - public getItemDeleteEndpoint(itemId: string): Observable { - return this.halService.getEndpoint(this.linkPath).pipe( - map((endpoint: string) => this.getFindByIDHref(endpoint, itemId)) + map((endpoint: string) => this.getIDHref(endpoint, itemId)) ); } @@ -94,7 +93,9 @@ export class ItemDataService extends DataService { new PatchRequest(this.requestService.generateRequestId(), endpointURL, patchOperation) ), configureRequest(this.requestService), - getResponseFromEntry() + map((request: RestRequest) => request.href), + getRequestFromRequestHref(this.requestService), + map((requestEntry: RequestEntry) => requestEntry.response) ); } @@ -113,23 +114,9 @@ export class ItemDataService extends DataService { new PatchRequest(this.requestService.generateRequestId(), endpointURL, patchOperation) ), configureRequest(this.requestService), - getResponseFromEntry() + map((request: RestRequest) => request.href), + getRequestFromRequestHref(this.requestService), + map((requestEntry: RequestEntry) => requestEntry.response) ); } - - /** - * Delete the item - * @param itemId - */ - public delete(itemId: string) { - return this.getItemDeleteEndpoint(itemId).pipe( - distinctUntilChanged(), - map((endpointURL: string) => - new DeleteRequest(this.requestService.generateRequestId(), endpointURL) - ), - configureRequest(this.requestService), - getResponseFromEntry() - ); - } - } diff --git a/src/app/core/data/paginated-list.ts b/src/app/core/data/paginated-list.ts index 07d53739d0..8efdccd75d 100644 --- a/src/app/core/data/paginated-list.ts +++ b/src/app/core/data/paginated-list.ts @@ -81,4 +81,12 @@ export class PaginatedList { set last(last: string) { this.pageInfo.last = last; } + + get self(): string { + return this.pageInfo.self; + } + + set self(self: string) { + this.pageInfo.self = self; + } } diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index 2e357cdc1d..1126899279 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -260,6 +260,29 @@ export class UpdateMetadataFieldRequest extends PutRequest { } } +export class CreateRequest extends PostRequest { + constructor(uuid: string, href: string, public body?: any, public options?: HttpOptions) { + super(uuid, href, body, options); + } + + getResponseParser(): GenericConstructor { + return DSOResponseParsingService; + } +} + +/** + * Request to delete an object based on its identifier + */ +export class DeleteByIDRequest extends DeleteRequest { + constructor( + uuid: string, + href: string, + public resourceID: string + ) { + super(uuid, href); + } +} + export class RequestError extends Error { statusText: string; } diff --git a/src/app/core/data/request.reducer.ts b/src/app/core/data/request.reducer.ts index 322ac46727..7158b1818b 100644 --- a/src/app/core/data/request.reducer.ts +++ b/src/app/core/data/request.reducer.ts @@ -90,7 +90,17 @@ function completeRequest(state: RequestState, action: RequestCompleteAction): Re }); } -function resetResponseTimestamps(state: RequestState, action: ResetResponseTimestampsAction) { +/** + * Reset the timeAdded property of all responses + * + * @param state + * the current state + * @param action + * a RequestCompleteAction + * @return RequestState + * the new state, with the timeAdded property reset + */ +function resetResponseTimestamps(state: RequestState, action: ResetResponseTimestampsAction): RequestState { const newState = Object.create(null); Object.keys(state).forEach((key) => { newState[key] = Object.assign({}, state[key], diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 922f035139..56a90d185f 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -100,7 +100,7 @@ export class RequestService { this.store.pipe(select(this.entryFromUUIDSelector(uuid))), this.store.pipe( select(this.originalUUIDFromUUIDSelector(uuid)), - switchMap((originalUUID) => { + mergeMap((originalUUID) => { return this.store.pipe(select(this.entryFromUUIDSelector(originalUUID))) }, )) diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts index 4893908627..26bd1ba5de 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.spec.ts @@ -2,6 +2,7 @@ import { TestBed, inject } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { DSpaceRESTv2Service } from './dspace-rest-v2.service'; +import { DSpaceObject } from '../shared/dspace-object.model'; describe('DSpaceRESTv2Service', () => { let dSpaceRESTv2Service: DSpaceRESTv2Service; @@ -65,4 +66,15 @@ describe('DSpaceRESTv2Service', () => { expect(req.request.method).toBe('GET'); req.error(mockError); }); + + describe('buildFormData', () => { + it('should return the correct data', () => { + const name = 'testname'; + const dso: DSpaceObject = { + name: name + } as DSpaceObject; + const formdata = dSpaceRESTv2Service.buildFormData(dso); + expect(formdata.get('name')).toBe(name); + }); + }); }); diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts index 7173e5ba0d..20d6b1dfb3 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts @@ -6,6 +6,8 @@ import { HttpClient, HttpHeaders, HttpParams, HttpResponse } from '@angular/comm import { DSpaceRESTV2Response } from './dspace-rest-v2-response.model'; import { HttpObserve } from '@angular/common/http/src/client'; import { RestRequestMethod } from '../data/rest-request-method'; +import { isNotEmpty } from '../../shared/empty.util'; +import { DSpaceObject } from '../shared/dspace-object.model'; export interface HttpOptions { body?: any; @@ -59,6 +61,9 @@ export class DSpaceRESTv2Service { request(method: RestRequestMethod, url: string, body?: any, options?: HttpOptions): Observable { const requestOptions: HttpOptions = {}; requestOptions.body = body; + if (method === RestRequestMethod.POST && isNotEmpty(body) && isNotEmpty(body.name)) { + requestOptions.body = this.buildFormData(body); + } requestOptions.observe = 'response'; if (options && options.headers) { requestOptions.headers = Object.assign(new HttpHeaders(), options.headers); @@ -74,4 +79,25 @@ export class DSpaceRESTv2Service { })); } + /** + * Create a FormData object from a DSpaceObject + * + * @param {DSpaceObject} dso + * the DSpaceObject + * @return {FormData} + * the result + */ + buildFormData(dso: DSpaceObject): FormData { + const form: FormData = new FormData(); + form.append('name', dso.name); + if (dso.metadata) { + for (const i of Object.keys(dso.metadata)) { + if (isNotEmpty(dso.metadata[i].value)) { + form.append(dso.metadata[i].key, dso.metadata[i].value); + } + } + } + return form; + } + } diff --git a/src/app/core/index/index.effects.ts b/src/app/core/index/index.effects.ts index dccee3c914..61cf313ab1 100644 --- a/src/app/core/index/index.effects.ts +++ b/src/app/core/index/index.effects.ts @@ -1,4 +1,4 @@ -import { filter, map, tap } from 'rxjs/operators'; +import { filter, map } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; @@ -12,11 +12,6 @@ import { AddToIndexAction, RemoveFromIndexByValueAction } from './index.actions' import { hasValue } from '../../shared/empty.util'; import { IndexName } from './index.reducer'; import { RestRequestMethod } from '../data/rest-request-method'; -import { - AddMenuSectionAction, - MenuActionTypes, - RemoveMenuSectionAction -} from '../../shared/menu/menu.actions'; @Injectable() export class UUIDIndexEffects { diff --git a/src/app/core/integration/integration-response-parsing.service.spec.ts b/src/app/core/integration/integration-response-parsing.service.spec.ts index 38741da4e2..baa4343724 100644 --- a/src/app/core/integration/integration-response-parsing.service.spec.ts +++ b/src/app/core/integration/integration-response-parsing.service.spec.ts @@ -23,15 +23,57 @@ describe('IntegrationResponseParsingService', () => { const uuid = 'd9d30c0c-69b7-4369-8397-ca67c888974d'; const integrationEndpoint = 'https://rest.api/integration/authorities'; const entriesEndpoint = `${integrationEndpoint}/${name}/entries?query=${query}&metadata=${metadata}&uuid=${uuid}`; + let validRequest; - beforeEach(() => { - service = new IntegrationResponseParsingService(EnvConfig, objectCacheService); - }); + let validResponse; - describe('parse', () => { - const validRequest = new IntegrationRequest('69f375b5-19f4-4453-8c7a-7dc5c55aafbb', entriesEndpoint); + let invalidResponse1; + let invalidResponse2; + let pageInfo; + let definitions; - const validResponse = { + function initVars() { + pageInfo = Object.assign(new PageInfo(), { elementsPerPage: 5, totalElements: 5, totalPages: 1, currentPage: 1, self: 'https://rest.api/integration/authorities/type/entries'}); + definitions = new PaginatedList(pageInfo,[ + Object.assign({}, new AuthorityValueModel(), { + type: 'authority', + display: 'One', + id: 'One', + otherInformation: undefined, + value: 'One' + }), + Object.assign({}, new AuthorityValueModel(), { + type: 'authority', + display: 'Two', + id: 'Two', + otherInformation: undefined, + value: 'Two' + }), + Object.assign({}, new AuthorityValueModel(), { + type: 'authority', + display: 'Three', + id: 'Three', + otherInformation: undefined, + value: 'Three' + }), + Object.assign({}, new AuthorityValueModel(), { + type: 'authority', + display: 'Four', + id: 'Four', + otherInformation: undefined, + value: 'Four' + }), + Object.assign({}, new AuthorityValueModel(), { + type: 'authority', + display: 'Five', + id: 'Five', + otherInformation: undefined, + value: 'Five' + }) + ]); + validRequest = new IntegrationRequest('69f375b5-19f4-4453-8c7a-7dc5c55aafbb', entriesEndpoint); + + validResponse = { payload: { page: { number: 0, @@ -86,12 +128,12 @@ describe('IntegrationResponseParsingService', () => { statusCode: '200' }; - const invalidResponse1 = { + invalidResponse1 = { payload: {}, statusCode: '200' }; - const invalidResponse2 = { + invalidResponse2 = { payload: { page: { number: 0, @@ -143,45 +185,13 @@ describe('IntegrationResponseParsingService', () => { }, statusCode: '200' }; - const pageinfo = Object.assign(new PageInfo(), { elementsPerPage: 5, totalElements: 5, totalPages: 1, currentPage: 1 }); - const definitions = new PaginatedList(pageinfo,[ - Object.assign({}, new AuthorityValueModel(), { - type: 'authority', - display: 'One', - id: 'One', - otherInformation: undefined, - value: 'One' - }), - Object.assign({}, new AuthorityValueModel(), { - type: 'authority', - display: 'Two', - id: 'Two', - otherInformation: undefined, - value: 'Two' - }), - Object.assign({}, new AuthorityValueModel(), { - type: 'authority', - display: 'Three', - id: 'Three', - otherInformation: undefined, - value: 'Three' - }), - Object.assign({}, new AuthorityValueModel(), { - type: 'authority', - display: 'Four', - id: 'Four', - otherInformation: undefined, - value: 'Four' - }), - Object.assign({}, new AuthorityValueModel(), { - type: 'authority', - display: 'Five', - id: 'Five', - otherInformation: undefined, - value: 'Five' - }) - ]); + } + beforeEach(() => { + initVars(); + service = new IntegrationResponseParsingService(EnvConfig, objectCacheService); + }); + describe('parse', () => { it('should return a IntegrationSuccessResponse if data contains a valid endpoint response', () => { const response = service.parse(validRequest, validResponse); expect(response.constructor).toBe(IntegrationSuccessResponse); diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 254316aaec..1841fba6a0 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -31,9 +31,12 @@ import { MockItem } from '../../shared/mocks/mock-item'; import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; import { BrowseService } from '../browse/browse.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { AuthService } from '../auth/auth.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; import { EmptyError } from 'rxjs/internal-compatibility'; -import { MockStore } from '../../shared/testing/mock-store'; -import { IndexState } from '../index/index.reducer'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; /* tslint:disable:max-classes-per-file */ @Component({ @@ -68,6 +71,7 @@ describe('MetadataService', () => { let uuidService: UUIDService; let remoteDataBuildService: RemoteDataBuildService; let itemDataService: ItemDataService; + let authService: AuthService; let location: Location; let router: Router; @@ -112,6 +116,11 @@ describe('MetadataService', () => { { provide: RemoteDataBuildService, useValue: remoteDataBuildService }, { provide: GLOBAL_CONFIG, useValue: ENV_CONFIG }, { provide: HALEndpointService, useValue: {}}, + { provide: AuthService, useValue: {} }, + { provide: NotificationsService, useValue: {} }, + { provide: HttpClient, useValue: {} }, + { provide: NormalizedObjectBuildService, useValue: {} }, + { provide: DSOChangeAnalyzer, useValue: {} }, Meta, Title, ItemDataService, @@ -124,6 +133,7 @@ describe('MetadataService', () => { title = TestBed.get(Title); itemDataService = TestBed.get(ItemDataService); metadataService = TestBed.get(MetadataService); + authService = TestBed.get(AuthService); envConfig = TestBed.get(GLOBAL_CONFIG); diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 9a74de992e..9dbfae3f90 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -67,7 +67,7 @@ export class MetadataService { public processRemoteData(remoteData: Observable>): void { remoteData.pipe(map((rd: RemoteData) => rd.payload), filter((co: CacheableObject) => hasValue(co)), - take(1),) + take(1)) .subscribe((dspaceObject: DSpaceObject) => { if (!this.initialized) { this.initialize(dspaceObject); @@ -277,7 +277,7 @@ export class MetadataService { .subscribe((bitstreams: Bitstream[]) => { for (const bitstream of bitstreams) { bitstream.format.pipe( - take(1), + first(), catchError((error: Error) => { console.debug(error.message); return [] diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 68338143ba..3e08da151c 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -34,14 +34,15 @@ export class DSpaceObject implements CacheableObject, ListableObject { /** * The name for this DSpaceObject */ - @autoserialize - name: string; + get name(): string { + return this.findMetadata('dc.title'); + } /** * An array containing all metadata of this DSpaceObject */ @autoserialize - metadata: Metadatum[]; + metadata: Metadatum[] = []; /** * An array of DSpaceObjects that are direct parents of this DSpaceObject diff --git a/src/app/core/shared/operators.spec.ts b/src/app/core/shared/operators.spec.ts index 7476aab838..41ed49b185 100644 --- a/src/app/core/shared/operators.spec.ts +++ b/src/app/core/shared/operators.spec.ts @@ -8,11 +8,14 @@ import { configureRequest, filterSuccessfulResponses, getAllSucceededRemoteData, - getRemoteDataPayload, getRequestFromRequestHref, getRequestFromRequestUUID, - getResourceLinksFromResponse, getResponseFromEntry, + getRemoteDataPayload, + getRequestFromRequestHref, + getRequestFromRequestUUID, + getResourceLinksFromResponse, + getResponseFromEntry, getSucceededRemoteData } from './operators'; -import {RemoteData} from '../data/remote-data'; +import { RemoteData } from '../data/remote-data'; describe('Core Module - RxJS Operators', () => { let scheduler: TestScheduler; @@ -188,8 +191,24 @@ describe('Core Module - RxJS Operators', () => { .toEqual(new RemoteData(false, false, true, null, 'd'))); }); - }); + + describe('getResponseFromEntry', () => { + it('should return the response for all not empty request entries, when they have a value', () => { + const source = hot('abcdefg', testRCEs); + const result = source.pipe(getResponseFromEntry()); + const expected = cold('abcde--', { + a: testRCEs.a.response, + b: testRCEs.b.response, + c: testRCEs.c.response, + d: testRCEs.d.response, + e: testRCEs.e.response + }); + + expect(result).toBeObservable(expected) + }); + }); + describe('getAllSucceededRemoteData', () => { it('should return all hasSucceeded RemoteData Observables', () => { const testRD = { diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index 4540bed0c4..3b92c71433 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -1,5 +1,5 @@ import { Observable } from 'rxjs'; -import { filter, find, first, flatMap, map, tap } from 'rxjs/operators'; +import { filter, find, flatMap, map, tap } from 'rxjs/operators'; import { hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util'; import { DSOSuccessResponse, RestResponse } from '../cache/response.models'; import { RemoteData } from '../data/remote-data'; @@ -62,6 +62,10 @@ export const getSucceededRemoteData = () => (source: Observable>): Observable> => source.pipe(find((rd: RemoteData) => rd.hasSucceeded)); +export const getFinishedRemoteData = () => + (source: Observable>): Observable> => + source.pipe(find((rd: RemoteData) => !rd.isLoading)); + export const getAllSucceededRemoteData = () => (source: Observable>): Observable> => source.pipe(filter((rd: RemoteData) => rd.hasSucceeded)); diff --git a/src/app/core/shared/page-info.model.ts b/src/app/core/shared/page-info.model.ts index ba2af24dce..4ed281657d 100644 --- a/src/app/core/shared/page-info.model.ts +++ b/src/app/core/shared/page-info.model.ts @@ -39,4 +39,7 @@ export class PageInfo { @autoserialize first: string; + + @autoserialize + self: string; } diff --git a/src/app/header/header.component.spec.ts b/src/app/header/header.component.spec.ts index c365119074..c46eef75e2 100644 --- a/src/app/header/header.component.spec.ts +++ b/src/app/header/header.component.spec.ts @@ -3,12 +3,13 @@ import { By } from '@angular/platform-browser'; import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; + +import { HeaderComponent } from './header.component'; import { ReactiveFormsModule } from '@angular/forms'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { MenuService } from '../shared/menu/menu.service'; import { MenuServiceStub } from '../shared/testing/menu-service-stub'; -import { HeaderComponent } from './header.component'; let comp: HeaderComponent; let fixture: ComponentFixture; diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.html b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.html new file mode 100644 index 0000000000..720ad0c1cf --- /dev/null +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.html @@ -0,0 +1,3 @@ + diff --git a/src/app/shared/mocks/mock-response-cache.service.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.scss similarity index 100% rename from src/app/shared/mocks/mock-response-cache.service.ts rename to src/app/shared/comcol-forms/comcol-form/comcol-form.component.scss diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts new file mode 100644 index 0000000000..a6f5e0d45a --- /dev/null +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts @@ -0,0 +1,115 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { Location } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { DynamicFormService, DynamicInputModel } from '@ng-dynamic-forms/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model'; +import { Community } from '../../../core/shared/community.model'; +import { ResourceType } from '../../../core/shared/resource-type'; +import { ComColFormComponent } from './comcol-form.component'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { hasValue } from '../../empty.util'; +import { Metadatum } from '../../../core/shared/metadatum.model'; + +describe('ComColFormComponent', () => { + let comp: ComColFormComponent; + let fixture: ComponentFixture>; + let location: Location; + const formServiceStub: any = { + createFormGroup: (fModel: DynamicFormControlModel[]) => { + const controls = {}; + if (hasValue(fModel)) { + fModel.forEach((controlModel) => { + controls[controlModel.id] = new FormControl((controlModel as any).value); + }); + return new FormGroup(controls); + } + return undefined; + } + }; + const titleMD = { key: 'dc.title', value: 'Community Title' } as Metadatum; + const randomMD = { key: 'dc.random', value: 'Random metadata excluded from form' } as Metadatum; + const abstractMD = { + key: 'dc.description.abstract', + value: 'Community description' + } as Metadatum; + const newTitleMD = { key: 'dc.title', value: 'New Community Title' } as Metadatum; + const formModel = [ + new DynamicInputModel({ + id: 'title', + name: newTitleMD.key, + value: 'New Community Title' + }), + new DynamicInputModel({ + id: 'abstract', + name: abstractMD.key, + value: abstractMD.value + }) + ]; + + /* tslint:disable:no-empty */ + const locationStub = { + back: () => { + } + }; + /* tslint:enable:no-empty */ + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), RouterTestingModule], + declarations: [ComColFormComponent], + providers: [ + { provide: Location, useValue: locationStub }, + { provide: DynamicFormService, useValue: formServiceStub } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ComColFormComponent); + comp = fixture.componentInstance; + comp.formModel = []; + comp.dso = new Community(); + fixture.detectChanges(); + location = (comp as any).location; + }); + + describe('onSubmit', () => { + beforeEach(() => { + spyOn(comp.submitForm, 'emit'); + comp.formModel = formModel; + }); + + it('should emit the new version of the community', () => { + comp.dso = Object.assign( + new Community(), + { + metadata: [ + titleMD, + randomMD + ] + } + ); + + comp.onSubmit(); + + expect(comp.submitForm.emit).toHaveBeenCalledWith( + Object.assign( + {}, + new Community(), + { + metadata: [ + randomMD, + newTitleMD, + abstractMD + ], + type: ResourceType.Community + }, + ) + ); + }) + }); +}); diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts new file mode 100644 index 0000000000..17710fd1c6 --- /dev/null +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts @@ -0,0 +1,115 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Location } from '@angular/common'; +import { + DynamicFormService, + DynamicInputModel +} from '@ng-dynamic-forms/core'; +import { FormGroup } from '@angular/forms'; +import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model'; +import { TranslateService } from '@ngx-translate/core'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { isNotEmpty } from '../../empty.util'; +import { ResourceType } from '../../../core/shared/resource-type'; + +/** + * A form for creating and editing Communities or Collections + */ +@Component({ + selector: 'ds-comcol-form', + styleUrls: ['./comcol-form.component.scss'], + templateUrl: './comcol-form.component.html' +}) +export class ComColFormComponent implements OnInit { + /** + * DSpaceObject that the form represents + */ + @Input() dso: T; + + /** + * Type of DSpaceObject that the form represents + */ + protected type; + + /** + * @type {string} Key prefix used to generate form labels + */ + LABEL_KEY_PREFIX = '.form.'; + + /** + * @type {string} Key prefix used to generate form error messages + */ + ERROR_KEY_PREFIX = '.form.errors.'; + + /** + * The form model that represents the fields in the form + */ + formModel: DynamicFormControlModel[]; + + /** + * The form group of this form + */ + formGroup: FormGroup; + + /** + * Emits DSO when the form is submitted + * @type {EventEmitter} + */ + @Output() submitForm: EventEmitter = new EventEmitter(); + + public constructor(private location: Location, + private formService: DynamicFormService, + private translate: TranslateService) { + } + + ngOnInit(): void { + this.formModel.forEach( + (fieldModel: DynamicInputModel) => { + fieldModel.value = this.dso.findMetadata(fieldModel.name); + } + ); + this.formGroup = this.formService.createFormGroup(this.formModel); + + this.updateFieldTranslations(); + this.translate.onLangChange + .subscribe(() => { + this.updateFieldTranslations(); + }); + } + + /** + * Checks which new fields where added and sends the updated version of the DSO to the parent component + */ + onSubmit() { + const metadata = this.formModel.map( + (fieldModel: DynamicInputModel) => { + return { key: fieldModel.name, value: fieldModel.value } + } + ); + const filteredOldMetadata = this.dso.metadata.filter((filter) => !metadata.map((md) => md.key).includes(filter.key)); + const filteredNewMetadata = metadata.filter((md) => isNotEmpty(md.value)); + + const newMetadata = [...filteredOldMetadata, ...filteredNewMetadata]; + const updatedDSO = Object.assign({}, this.dso, { + metadata: newMetadata, + type: ResourceType.Community + }); + this.submitForm.emit(updatedDSO); + } + + /** + * Used the update translations of errors and labels on init and on language change + */ + private updateFieldTranslations() { + this.formModel.forEach( + (fieldModel: DynamicInputModel) => { + fieldModel.label = this.translate.instant(this.type + this.LABEL_KEY_PREFIX + fieldModel.id); + if (isNotEmpty(fieldModel.validators)) { + fieldModel.errorMessages = {}; + Object.keys(fieldModel.validators).forEach((key) => { + fieldModel.errorMessages[key] = this.translate.instant(this.type + this.ERROR_KEY_PREFIX + fieldModel.id + '.' + key); + }); + } + } + ); + } +} diff --git a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts new file mode 100644 index 0000000000..fd3464ba5e --- /dev/null +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts @@ -0,0 +1,119 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { CommunityDataService } from '../../../core/data/community-data.service'; +import { RouteService } from '../../services/route.service'; +import { Router } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Community } from '../../../core/shared/community.model'; +import { SharedModule } from '../../shared.module'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { NormalizedDSpaceObject } from '../../../core/cache/models/normalized-dspace-object.model'; +import { CreateComColPageComponent } from './create-comcol-page.component'; +import { DataService } from '../../../core/data/data.service'; + +describe('CreateComColPageComponent', () => { + let comp: CreateComColPageComponent; + let fixture: ComponentFixture>; + let communityDataService: CommunityDataService; + let dsoDataService: CommunityDataService; + let routeService: RouteService; + let router: Router; + + let community; + let newCommunity; + let communityDataServiceStub; + let routeServiceStub; + let routerStub; + + function initializeVars() { + community = Object.assign(new Community(), { + uuid: 'a20da287-e174-466a-9926-f66b9300d347', + metadata: [{ + key: 'dc.title', + value: 'test community' + }] + }); + + newCommunity = Object.assign(new Community(), { + uuid: '1ff59938-a69a-4e62-b9a4-718569c55d48', + metadata: [{ + key: 'dc.title', + value: 'new community' + }] + }); + + communityDataServiceStub = { + findById: (uuid) => observableOf(new RemoteData(false, false, true, null, Object.assign(new Community(), { + uuid: uuid, + metadata: [{ + key: 'dc.title', + value: community.name + }] + }))), + create: (com, uuid?) => observableOf(new RemoteData(false, false, true, undefined, newCommunity)) + + }; + + routeServiceStub = { + getQueryParameterValue: (param) => observableOf(community.uuid) + }; + routerStub = { + navigate: (commands) => commands + }; + + } + + beforeEach(async(() => { + initializeVars(); + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + providers: [ + { provide: DataService, useValue: communityDataServiceStub }, + { provide: CommunityDataService, useValue: communityDataServiceStub }, + { provide: RouteService, useValue: routeServiceStub }, + { provide: Router, useValue: routerStub }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CreateComColPageComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + dsoDataService = (comp as any).dsoDataService; + communityDataService = (comp as any).communityDataService; + routeService = (comp as any).routeService; + router = (comp as any).router; + }); + + describe('onSubmit', () => { + let data; + beforeEach(() => { + data = Object.assign(new Community(), { + metadata: [{ + key: 'dc.title', + value: 'test' + }] + }); + }); + it('should navigate when successful', () => { + spyOn(router, 'navigate'); + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should not navigate on failure', () => { + spyOn(router, 'navigate'); + spyOn(dsoDataService, 'create').and.returnValue(observableOf(new RemoteData(true, true, false, undefined, newCommunity))); + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigate).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts new file mode 100644 index 0000000000..fc7ee3ee70 --- /dev/null +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts @@ -0,0 +1,73 @@ +import { Component, OnInit } from '@angular/core'; +import { Community } from '../../../core/shared/community.model'; +import { CommunityDataService } from '../../../core/data/community-data.service'; +import { Observable } from 'rxjs'; +import { RouteService } from '../../services/route.service'; +import { Router } from '@angular/router'; +import { RemoteData } from '../../../core/data/remote-data'; +import { isNotEmpty, isNotUndefined } from '../../empty.util'; +import { take } from 'rxjs/operators'; +import { getSucceededRemoteData } from '../../../core/shared/operators'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { DataService } from '../../../core/data/data.service'; +import { NormalizedDSpaceObject } from '../../../core/cache/models/normalized-dspace-object.model'; + +/** + * Component representing the create page for communities and collections + */ +@Component({ + selector: 'ds-create-comcol', + template: '' +}) +export class CreateComColPageComponent implements OnInit { + /** + * Frontend endpoint for this type of DSO + */ + protected frontendURL: string; + + /** + * The provided UUID for the parent community + */ + public parentUUID$: Observable; + + /** + * The parent community of the object that is to be created + */ + public parentRD$: Observable>; + + public constructor( + protected dsoDataService: DataService, + protected parentDataService: CommunityDataService, + protected routeService: RouteService, + protected router: Router + ) { + + } + + ngOnInit(): void { + this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); + this.parentUUID$.pipe(take(1)).subscribe((parentID: string) => { + if (isNotEmpty(parentID)) { + this.parentRD$ = this.parentDataService.findById(parentID); + } + }); + } + + /** + * @param {TDomain} dso The updated version of the DSO + * Creates a new DSO based on the submitted user data and navigates to the new object's home page + */ + onSubmit(dso: TDomain) { + this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { + this.dsoDataService.create(dso, uuid) + .pipe(getSucceededRemoteData()) + .subscribe((dsoRD: RemoteData) => { + if (isNotUndefined(dsoRD)) { + const newUUID = dsoRD.payload.uuid; + this.router.navigate([this.frontendURL + newUUID]); + } + }); + }); + } + +} diff --git a/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts b/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts new file mode 100644 index 0000000000..81ec9c47a0 --- /dev/null +++ b/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts @@ -0,0 +1,155 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { CommunityDataService } from '../../../core/data/community-data.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; +import { Community } from '../../../core/shared/community.model'; +import { SharedModule } from '../../shared.module'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { NormalizedDSpaceObject } from '../../../core/cache/models/normalized-dspace-object.model'; +import { DataService } from '../../../core/data/data.service'; +import { DeleteComColPageComponent } from './delete-comcol-page.component'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { NotificationsServiceStub } from '../../testing/notifications-service-stub'; + +describe('DeleteComColPageComponent', () => { + let comp: DeleteComColPageComponent; + let fixture: ComponentFixture>; + let dsoDataService: CommunityDataService; + let router: Router; + + let community; + let newCommunity; + let routerStub; + let routeStub; + let notificationsService; + const validUUID = 'valid-uuid'; + const invalidUUID = 'invalid-uuid'; + const frontendURL = '/testType'; + function initializeVars() { + community = Object.assign(new Community(), { + uuid: 'a20da287-e174-466a-9926-f66b9300d347', + metadata: [{ + key: 'dc.title', + value: 'test community' + }] + }); + + newCommunity = Object.assign(new Community(), { + uuid: '1ff59938-a69a-4e62-b9a4-718569c55d48', + metadata: [{ + key: 'dc.title', + value: 'new community' + }] + }); + + dsoDataService = jasmine.createSpyObj( + 'dsoDataService', + { + delete: observableOf(true) + }); + + routerStub = { + navigate: (commands) => commands + }; + + routeStub = { + data: observableOf(community) + }; + + } + + beforeEach(async(() => { + initializeVars(); + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + providers: [ + { provide: DataService, useValue: dsoDataService }, + { provide: Router, useValue: routerStub }, + { provide: ActivatedRoute, useValue: routeStub }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DeleteComColPageComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + notificationsService = (comp as any).notifications; + (comp as any).frontendURL = frontendURL; + router = (comp as any).router; + }); + + describe('onConfirm', () => { + let data1; + let data2; + beforeEach(() => { + data1 = Object.assign(new Community(), { + uuid: validUUID, + metadata: [{ + key: 'dc.title', + value: 'test' + }] + }); + + data2 = Object.assign(new Community(), { + uuid: invalidUUID, + metadata: [{ + key: 'dc.title', + value: 'test' + }] + }); + }); + + it('should show an error notification on failure', () => { + (dsoDataService.delete as any).and.returnValue(observableOf(false)); + spyOn(notificationsService, 'error'); + spyOn(router, 'navigate'); + comp.onConfirm(data2); + fixture.detectChanges(); + expect(notificationsService.error).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should show a success notification on success and navigate', () => { + spyOn(notificationsService, 'success'); + spyOn(router, 'navigate'); + comp.onConfirm(data1); + fixture.detectChanges(); + expect(notificationsService.success).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should call delete on the data service', () => { + comp.onConfirm(data1); + fixture.detectChanges(); + expect(dsoDataService.delete).toHaveBeenCalledWith(data1); + }); + }); + + describe('onCancel', () => { + let data1; + beforeEach(() => { + data1 = Object.assign(new Community(), { + uuid: validUUID, + metadata: [{ + key: 'dc.title', + value: 'test' + }] + }); + }); + + it('should redirect to the edit page', () => { + const redirectURL = frontendURL + '/' + validUUID + '/edit'; + spyOn(router, 'navigate'); + comp.onCancel(data1); + fixture.detectChanges(); + expect(router.navigate).toHaveBeenCalledWith([redirectURL]); + }); + }); +}); diff --git a/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.ts b/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.ts new file mode 100644 index 0000000000..6e3a826e87 --- /dev/null +++ b/src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.ts @@ -0,0 +1,71 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { RouteService } from '../../services/route.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { RemoteData } from '../../../core/data/remote-data'; +import { isNotUndefined } from '../../empty.util'; +import { first, map } from 'rxjs/operators'; +import { getSucceededRemoteData } from '../../../core/shared/operators'; +import { DataService } from '../../../core/data/data.service'; +import { NormalizedDSpaceObject } from '../../../core/cache/models/normalized-dspace-object.model'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; + +/** + * Component representing the delete page for communities and collections + */ +@Component({ + selector: 'ds-delete-comcol', + template: '' +}) +export class DeleteComColPageComponent implements OnInit { + /** + * Frontend endpoint for this type of DSO + */ + protected frontendURL: string; + /** + * The initial DSO object + */ + public dsoRD$: Observable>; + + public constructor( + protected dsoDataService: DataService, + protected router: Router, + protected route: ActivatedRoute, + protected notifications: NotificationsService, + protected translate: TranslateService + ) { + } + + ngOnInit(): void { + this.dsoRD$ = this.route.data.pipe(first(), map((data) => data.dso)); + } + + /** + * @param {TDomain} dso The DSO to delete + * Deletes an existing DSO and redirects to the home page afterwards, showing a notification that states whether or not the deletion was successful + */ + onConfirm(dso: TDomain) { + this.dsoDataService.delete(dso) + .pipe(first()) + .subscribe((success: boolean) => { + if (success) { + const successMessage = this.translate.instant(dso.type + '.delete.notification.success'); + this.notifications.success(successMessage) + } else { + const errorMessage = this.translate.instant(dso.type + '.delete.notification.fail'); + this.notifications.error(errorMessage) + } + this.router.navigate(['/']); + }); + } + + /** + * @param {TDomain} dso The DSO for which the delete action was canceled + * When a delete is canceled, the user is redirected to the DSO's edit page + */ + onCancel(dso: TDomain) { + this.router.navigate([this.frontendURL + '/' + dso.uuid + '/edit']); + } +} diff --git a/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.spec.ts b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.spec.ts new file mode 100644 index 0000000000..88c11a0b4d --- /dev/null +++ b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.spec.ts @@ -0,0 +1,107 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { CommunityDataService } from '../../../core/data/community-data.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Community } from '../../../core/shared/community.model'; +import { SharedModule } from '../../shared.module'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { NormalizedDSpaceObject } from '../../../core/cache/models/normalized-dspace-object.model'; +import { EditComColPageComponent } from './edit-comcol-page.component'; +import { DataService } from '../../../core/data/data.service'; + +describe('EditComColPageComponent', () => { + let comp: EditComColPageComponent; + let fixture: ComponentFixture>; + let dsoDataService: CommunityDataService; + let router: Router; + + let community; + let newCommunity; + let communityDataServiceStub; + let routerStub; + let routeStub; + + function initializeVars() { + community = Object.assign(new Community(), { + uuid: 'a20da287-e174-466a-9926-f66b9300d347', + metadata: [{ + key: 'dc.title', + value: 'test community' + }] + }); + + newCommunity = Object.assign(new Community(), { + uuid: '1ff59938-a69a-4e62-b9a4-718569c55d48', + metadata: [{ + key: 'dc.title', + value: 'new community' + }] + }); + + communityDataServiceStub = { + update: (com, uuid?) => observableOf(new RemoteData(false, false, true, undefined, newCommunity)) + + }; + + routerStub = { + navigate: (commands) => commands + }; + + routeStub = { + data: observableOf(community) + }; + + } + + beforeEach(async(() => { + initializeVars(); + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + providers: [ + { provide: DataService, useValue: communityDataServiceStub }, + { provide: Router, useValue: routerStub }, + { provide: ActivatedRoute, useValue: routeStub }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EditComColPageComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + dsoDataService = (comp as any).dsoDataService; + router = (comp as any).router; + }); + + describe('onSubmit', () => { + let data; + beforeEach(() => { + data = Object.assign(new Community(), { + metadata: [{ + key: 'dc.title', + value: 'test' + }] + }); + }); + it('should navigate when successful', () => { + spyOn(router, 'navigate'); + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should not navigate on failure', () => { + spyOn(router, 'navigate'); + spyOn(dsoDataService, 'update').and.returnValue(observableOf(new RemoteData(true, true, false, undefined, newCommunity))); + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigate).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts new file mode 100644 index 0000000000..b669fcea54 --- /dev/null +++ b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts @@ -0,0 +1,54 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { ActivatedRoute, Router } from '@angular/router'; +import { RemoteData } from '../../../core/data/remote-data'; +import { isNotUndefined } from '../../empty.util'; +import { first, map } from 'rxjs/operators'; +import { getSucceededRemoteData } from '../../../core/shared/operators'; +import { DataService } from '../../../core/data/data.service'; +import { NormalizedDSpaceObject } from '../../../core/cache/models/normalized-dspace-object.model'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; + +/** + * Component representing the edit page for communities and collections + */ +@Component({ + selector: 'ds-edit-comcol', + template: '' +}) +export class EditComColPageComponent implements OnInit { + /** + * Frontend endpoint for this type of DSO + */ + protected frontendURL: string; + /** + * The initial DSO object + */ + public dsoRD$: Observable>; + + public constructor( + protected dsoDataService: DataService, + protected router: Router, + protected route: ActivatedRoute + ) { + } + + ngOnInit(): void { + this.dsoRD$ = this.route.data.pipe(first(), map((data) => data.dso)); + } + + /** + * @param {TDomain} dso The updated version of the DSO + * Updates an existing DSO based on the submitted user data and navigates to the edited object's home page + */ + onSubmit(dso: TDomain) { + this.dsoDataService.update(dso) + .pipe(getSucceededRemoteData()) + .subscribe((dsoRD: RemoteData) => { + if (isNotUndefined(dsoRD)) { + const newUUID = dsoRD.payload.uuid; + this.router.navigate([this.frontendURL + newUUID]); + } + }); + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts index 3544bce280..8a3cf52abb 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts @@ -96,7 +96,6 @@ export class DsDynamicFormControlComponent extends DynamicFormControlContainerCo } static getFormControlType(model: DynamicFormControlModel): Type | null { - switch (model.type) { case DYNAMIC_FORM_CONTROL_TYPE_ARRAY: diff --git a/src/app/shared/form/form.component.html b/src/app/shared/form/form.component.html index 291926dcec..21d4a81659 100644 --- a/src/app/shared/form/form.component.html +++ b/src/app/shared/form/form.component.html @@ -51,7 +51,7 @@
-
diff --git a/src/app/shared/form/form.service.ts b/src/app/shared/form/form.service.ts index ae5ba9f278..9356f86e8c 100644 --- a/src/app/shared/form/form.service.ts +++ b/src/app/shared/form/form.service.ts @@ -98,7 +98,7 @@ export class FormService { const errorKey = this.getValidatorNameFromMap(message); let errorMsg = message; - // if form control model has not errorMessages object, create it + // if form control model has no errorMessages object, create it if (!model.errorMessages) { model.errorMessages = {}; } diff --git a/src/app/shared/host-window.service.ts b/src/app/shared/host-window.service.ts index 840b134996..146c616e38 100644 --- a/src/app/shared/host-window.service.ts +++ b/src/app/shared/host-window.service.ts @@ -1,6 +1,6 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { filter, distinctUntilChanged, map, first } from 'rxjs/operators'; +import { filter, distinctUntilChanged, map } from 'rxjs/operators'; import { HostWindowState } from './host-window.reducer'; import { Injectable } from '@angular/core'; import { createSelector, select, Store } from '@ngrx/store'; diff --git a/src/app/shared/mocks/mock-item.ts b/src/app/shared/mocks/mock-item.ts index f3db69a0f2..2e5c764ee2 100644 --- a/src/app/shared/mocks/mock-item.ts +++ b/src/app/shared/mocks/mock-item.ts @@ -51,7 +51,6 @@ export const MockItem: Item = Object.assign(new Item(), { id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713', uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713', type: 'bitstream', - name: 'test_word.docx', metadata: [ { key: 'dc.title', @@ -86,7 +85,6 @@ export const MockItem: Item = Object.assign(new Item(), { id: '99b00f3c-1cc6-4689-8158-91965bee6b28', uuid: '99b00f3c-1cc6-4689-8158-91965bee6b28', type: 'bitstream', - name: 'test_pdf.pdf', metadata: [ { key: 'dc.title', @@ -102,7 +100,6 @@ export const MockItem: Item = Object.assign(new Item(), { id: '0ec7ff22-f211-40ab-a69e-c819b0b1f357', uuid: '0ec7ff22-f211-40ab-a69e-c819b0b1f357', type: 'item', - name: 'Test PowerPoint Document', metadata: [ { key: 'dc.creator', diff --git a/src/app/shared/search-form/search-form.component.spec.ts b/src/app/shared/search-form/search-form.component.spec.ts index 30f5801cc2..004d0c5b21 100644 --- a/src/app/shared/search-form/search-form.component.spec.ts +++ b/src/app/shared/search-form/search-form.component.spec.ts @@ -121,7 +121,6 @@ export const objects: DSpaceObject[] = [ id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', type: ResourceType.Community, - name: 'OR2017 - Demonstration', metadata: [ { key: 'dc.description', @@ -171,7 +170,6 @@ export const objects: DSpaceObject[] = [ id: '9076bd16-e69a-48d6-9e41-0238cb40d863', uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863', type: ResourceType.Community, - name: 'Sample Community', metadata: [ { key: 'dc.description', diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 3561576046..53cf15ab6e 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -86,7 +86,11 @@ import { CapitalizePipe } from './utils/capitalize.pipe'; import { ObjectKeysPipe } from './utils/object-keys-pipe'; import { MomentModule } from 'ngx-moment'; import { MenuModule } from './menu/menu.module'; -import {LangSwitchComponent} from './lang-switch/lang-switch.component'; +import { ComColFormComponent } from './comcol-forms/comcol-form/comcol-form.component'; +import { CreateComColPageComponent } from './comcol-forms/create-comcol-page/create-comcol-page.component'; +import { EditComColPageComponent } from './comcol-forms/edit-comcol-page/edit-comcol-page.component'; +import { DeleteComColPageComponent } from './comcol-forms/delete-comcol-page/delete-comcol-page.component'; +import { LangSwitchComponent } from './lang-switch/lang-switch.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -130,6 +134,10 @@ const COMPONENTS = [ ComcolPageContentComponent, ComcolPageHeaderComponent, ComcolPageLogoComponent, + ComColFormComponent, + CreateComColPageComponent, + EditComColPageComponent, + DeleteComColPageComponent, DsDynamicFormComponent, DsDynamicFormControlComponent, DsDynamicListComponent, diff --git a/src/app/shared/testing/eperson-mock.ts b/src/app/shared/testing/eperson-mock.ts index f163a490b9..ef27f4983d 100644 --- a/src/app/shared/testing/eperson-mock.ts +++ b/src/app/shared/testing/eperson-mock.ts @@ -13,8 +13,12 @@ export const EPersonMock: EPerson = Object.assign(new EPerson(),{ id: 'testid', uuid: 'testid', type: 'eperson', - name: 'User Test', metadata: [ + { + key: 'dc.title', + language: null, + value: 'User Test' + }, { key: 'eperson.firstname', language: null, diff --git a/src/app/shared/testing/utils.ts b/src/app/shared/testing/utils.ts index cd17a1b1f5..770a554439 100644 --- a/src/app/shared/testing/utils.ts +++ b/src/app/shared/testing/utils.ts @@ -31,6 +31,14 @@ export const createTestComponent = (html: string, type: { new(...args: any[]) return fixture as ComponentFixture; }; +/** + * Allows you to spy on a read only property + * + * @param obj + * The object to spy on + * @param prop + * The property to spy on + */ export function spyOnOperator(obj: any, prop: string): any { const oldProp = obj[prop]; Object.defineProperty(obj, prop, { diff --git a/src/config/auto-sync-config.interface.ts b/src/config/auto-sync-config.interface.ts index 5285916b12..90e7ebee80 100644 --- a/src/config/auto-sync-config.interface.ts +++ b/src/config/auto-sync-config.interface.ts @@ -1,11 +1,30 @@ import { RestRequestMethod } from '../app/core/data/rest-request-method'; +/** + * The number of seconds between automatic syncs to the + * server for requests using a certain HTTP Method + */ type TimePerMethod = { [method in RestRequestMethod]: number; }; +/** + * The config that determines how the automatic syncing + * of changed data to the server works + */ export interface AutoSyncConfig { + /** + * The number of seconds between automatic syncs to the server + */ defaultTime: number; + + /** + * HTTP Method specific overrides of defaultTime + */ timePerMethod: TimePerMethod; + + /** + * The max number of requests in the buffer before a sync to the server + */ maxBufferSize: number; }; diff --git a/src/config/cache-config.interface.ts b/src/config/cache-config.interface.ts index a52eca60e2..ef2d19e76e 100644 --- a/src/config/cache-config.interface.ts +++ b/src/config/cache-config.interface.ts @@ -4,7 +4,6 @@ import { AutoSyncConfig } from './auto-sync-config.interface'; export interface CacheConfig extends Config { msToLive: { default: number; - exportToZip: number; }, control: string, autoSync: AutoSyncConfig