From 1ebd6f0e86a2f0e06edf43c7f90fc6e391411b5e Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 2 Jan 2019 16:51:41 +0100 Subject: [PATCH] Finished refactoring, guards, docs, tests --- .../collection-form.component.spec.ts | 73 ----------- .../collection-form.component.ts | 23 +++- .../collection-page-routing.module.ts | 18 ++- .../collection-page.component.ts | 3 +- .../collection-page.module.ts | 2 + .../create-collection-page.component.html | 13 +- .../create-collection-page.component.spec.ts | 108 ---------------- .../create-collection-page.component.ts | 15 ++- .../create-collection-page.guard.spec.ts | 67 ++++++++++ .../create-collection-page.guard.ts | 46 +++++++ .../edit-collection-page.component.html | 4 +- ...ss => edit-collection-page.component.scss} | 0 .../edit-collection-page.component.ts | 28 ++++ .../edit-community-page.component.spec.ts | 121 ------------------ .../edit-community-page.component.ts | 45 ------- .../community-form.component.ts | 30 +++-- .../community-page-routing.module.ts | 12 +- .../community-page.component.html | 1 - .../create-community-page.component.html | 2 +- .../create-community-page.component.ts | 8 +- .../create-community-page.guard.spec.ts | 67 ++++++++++ .../create-community-page.guard.ts | 46 +++++++ .../edit-community-page.component.html | 2 +- .../edit-community-page.component.ts | 41 ++---- .../edit-comcol-page.component.ts | 45 ------- .../config-response-parsing.service.spec.ts | 8 +- src/app/core/data/data.service.spec.ts | 30 +++-- src/app/core/data/update-comparator.ts | 2 +- .../core/metadata/metadata.service.spec.ts | 4 + src/app/core/metadata/metadata.service.ts | 6 +- src/app/core/shared/operators.ts | 4 + .../comcol-form/comcol-form.component.html | 0 .../comcol-form/comcol-form.component.scss} | 0 .../comcol-form.component.spec.ts} | 59 +++++---- .../comcol-form/comcol-form.component.ts | 53 ++++++-- .../create-comcol-page.component.spec.ts} | 29 +++-- .../create-comcol-page.component.ts | 43 +++++-- .../edit-comcol-page.component.spec.ts} | 31 +++-- .../edit-comcol-page.component.ts | 56 ++++++++ src/app/shared/shared.module.ts | 6 + src/app/shared/testing/utils.ts | 2 +- 41 files changed, 600 insertions(+), 553 deletions(-) delete mode 100644 src/app/+collection-page/collection-form/collection-form.component.spec.ts delete mode 100644 src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts create mode 100644 src/app/+collection-page/create-collection-page/create-collection-page.guard.spec.ts create mode 100644 src/app/+collection-page/create-collection-page/create-collection-page.guard.ts rename src/app/+collection-page/edit-collection-page/{edit-community-page.component.scss => edit-collection-page.component.scss} (100%) create mode 100644 src/app/+collection-page/edit-collection-page/edit-collection-page.component.ts delete mode 100644 src/app/+collection-page/edit-collection-page/edit-community-page.component.spec.ts delete mode 100644 src/app/+collection-page/edit-collection-page/edit-community-page.component.ts create mode 100644 src/app/+community-page/create-community-page/create-community-page.guard.spec.ts create mode 100644 src/app/+community-page/create-community-page/create-community-page.guard.ts delete mode 100644 src/app/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts rename src/app/{ => shared}/comcol-forms/comcol-form/comcol-form.component.html (100%) rename src/app/shared/{mocks/mock-response-cache.service.ts => comcol-forms/comcol-form/comcol-form.component.scss} (100%) rename src/app/{+community-page/community-form/community-form.component.spec.ts => shared/comcol-forms/comcol-form/comcol-form.component.spec.ts} (58%) rename src/app/{ => shared}/comcol-forms/comcol-form/comcol-form.component.ts (65%) rename src/app/{+community-page/create-community-page/create-community-page.component.spec.ts => shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts} (70%) rename src/app/{ => shared}/comcol-forms/create-comcol-page/create-comcol-page.component.ts (54%) rename src/app/{+community-page/edit-community-page/edit-community-page.component.spec.ts => shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.spec.ts} (71%) create mode 100644 src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts diff --git a/src/app/+collection-page/collection-form/collection-form.component.spec.ts b/src/app/+collection-page/collection-form/collection-form.component.spec.ts deleted file mode 100644 index 71ae92572d..0000000000 --- a/src/app/+collection-page/collection-form/collection-form.component.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { SharedModule } from '../../shared/shared.module'; -import { TranslateModule } from '@ngx-translate/core'; -import { CommonModule } from '@angular/common'; -import { RouterTestingModule } from '@angular/router/testing'; -import { By } from '@angular/platform-browser'; -import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; -import { CollectionFormComponent } from './collection-form.component'; -import { Location } from '@angular/common'; -import { DynamicFormService } from '@ng-dynamic-forms/core'; - -describe('CommunityFormComponent', () => { - let comp: CollectionFormComponent; - let fixture: ComponentFixture - let location: Location; - - /* tslint:disable:no-empty */ - const locationStub = { - back: () => {} - }; - /* tslint:enable:no-empty */ - - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [CollectionFormComponent], - providers: [ - { provide: Location, useValue: locationStub }, - ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(CollectionFormComponent); - comp = fixture.componentInstance; - fixture.detectChanges(); - location = (comp as any).location; - }); - - describe('when submitting', () => { - let input: DebugElement; - let submit: DebugElement; - let cancel: DebugElement; - let error: DebugElement; - - beforeEach(() => { - input = fixture.debugElement.query(By.css('input#collection-name')); - submit = fixture.debugElement.query(By.css('button#collection-submit')); - cancel = fixture.debugElement.query(By.css('button#collection-cancel')); - error = fixture.debugElement.query(By.css('div.invalid-feedback')); - }); - - it('should display an error when leaving name empty', () => { - const el = input.nativeElement; - - el.value = ''; - el.dispatchEvent(new Event('input')); - submit.nativeElement.click(); - fixture.detectChanges(); - - expect(error.nativeElement.style.display).not.toEqual('none'); - }); - - it('should navigate back when pressing cancel', () => { - spyOn(location, 'back'); - cancel.nativeElement.click(); - fixture.detectChanges(); - - expect(location.back).toHaveBeenCalled(); - }); - }) -}); diff --git a/src/app/+collection-page/collection-form/collection-form.component.ts b/src/app/+collection-page/collection-form/collection-form.component.ts index 03bd9bfda9..22f2f1271d 100644 --- a/src/app/+collection-page/collection-form/collection-form.component.ts +++ b/src/app/+collection-page/collection-form/collection-form.component.ts @@ -6,16 +6,31 @@ import { import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model'; import { ResourceType } from '../../core/shared/resource-type'; import { Collection } from '../../core/shared/collection.model'; -import { ComColFormComponent } from '../../comcol-forms/comcol-form/comcol-form.component'; +import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component'; +/** + * Form used for creating and editing collections + */ @Component({ selector: 'ds-collection-form', - styleUrls: ['../../comcol-forms/comcol-form.component.scss'], - templateUrl: '../../comcol-forms/comcol-form/comcol-form.component.html' + styleUrls: ['../../shared/comcol-forms/comcol-form/comcol-form.component.scss'], + templateUrl: '../../shared/comcol-forms/comcol-form/comcol-form.component.html' }) export class CollectionFormComponent extends ComColFormComponent { + /** + * @type {Collection} A new collection when a collection is being created, an existing Input collection when a collection is being edited + */ @Input() dso: Collection = new Collection(); - type = ResourceType.Collection; + + /** + * @type {ResourceType.Collection} This is a collection-type form + */ + protected type = ResourceType.Collection; + + /** + * The dynamic form fields used for creating/editing a collection + * @type {(DynamicInputModel | DynamicTextAreaModel)[]} + */ formModel: DynamicFormControlModel[] = [ new DynamicInputModel({ id: 'title', diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index 0b1245578c..ec53796e61 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -5,13 +5,26 @@ import { CollectionPageComponent } from './collection-page.component'; import { CollectionPageResolver } from './collection-page.resolver'; import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; +import { EditCollectionPageComponent } from './edit-collection-page/edit-collection-page.component'; +import { CreateCollectionPageGuard } from './create-collection-page/create-collection-page.guard'; @NgModule({ imports: [ RouterModule.forChild([ - { path: 'create', + { + path: 'create', component: CreateCollectionPageComponent, - canActivate: [AuthenticatedGuard] }, + canActivate: [AuthenticatedGuard, CreateCollectionPageGuard] + }, + { + path: ':id/edit', + pathMatch: 'full', + component: EditCollectionPageComponent, + canActivate: [AuthenticatedGuard], + resolve: { + dso: CollectionPageResolver + } + }, { path: ':id', component: CollectionPageComponent, @@ -24,6 +37,7 @@ import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; ], providers: [ CollectionPageResolver, + CreateCollectionPageGuard ] }) export class CollectionPageRoutingModule { diff --git a/src/app/+collection-page/collection-page.component.ts b/src/app/+collection-page/collection-page.component.ts index b76c0a7520..7c4f2b92ac 100644 --- a/src/app/+collection-page/collection-page.component.ts +++ b/src/app/+collection-page/collection-page.component.ts @@ -55,7 +55,8 @@ export class CollectionPageComponent implements OnInit, OnDestroy { ngOnInit(): void { this.collectionRD$ = this.route.data.pipe( - map((data) => data.collection) + map((data) => data.collection), + tap((data) => this.collectionId = data.payload.id) ); this.logoRD$ = this.collectionRD$.pipe( map((rd: RemoteData) => rd.payload), diff --git a/src/app/+collection-page/collection-page.module.ts b/src/app/+collection-page/collection-page.module.ts index a6316e9189..63a03f4299 100644 --- a/src/app/+collection-page/collection-page.module.ts +++ b/src/app/+collection-page/collection-page.module.ts @@ -8,6 +8,7 @@ import { CollectionPageRoutingModule } from './collection-page-routing.module'; import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component'; import { CollectionFormComponent } from './collection-form/collection-form.component'; import { SearchPageModule } from '../+search-page/search-page.module'; +import { EditCollectionPageComponent } from './edit-collection-page/edit-collection-page.component'; @NgModule({ imports: [ @@ -19,6 +20,7 @@ import { SearchPageModule } from '../+search-page/search-page.module'; declarations: [ CollectionPageComponent, CreateCollectionPageComponent, + EditCollectionPageComponent, CollectionFormComponent ] }) diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.html b/src/app/+collection-page/create-collection-page/create-collection-page.component.html index aa9569b6b8..b3f4361bc6 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.html +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.html @@ -1,11 +1,8 @@
-
-
- - -

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

-
+
+
+

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

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

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

diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index 5a3cf1f4a8..47fb065038 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -3,16 +3,20 @@ import { Community } from '../../core/shared/community.model'; import { CommunityDataService } from '../../core/data/community-data.service'; import { RouteService } from '../../shared/services/route.service'; import { Router } from '@angular/router'; -import { CreateComColPageComponent } from '../../comcol-forms/create-comcol-page/create-comcol-page.component'; +import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component'; import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model'; +/** + * Component that represents the page where a user can create a new Community + */ @Component({ selector: 'ds-create-community', styleUrls: ['./create-community-page.component.scss'], templateUrl: './create-community-page.component.html' }) export class CreateCommunityPageComponent extends CreateComColPageComponent { - protected frontendURL = 'communities'; + protected frontendURL = '/communities/'; + public constructor( protected communityDataService: CommunityDataService, protected routeService: RouteService, diff --git a/src/app/+community-page/create-community-page/create-community-page.guard.spec.ts b/src/app/+community-page/create-community-page/create-community-page.guard.spec.ts new file mode 100644 index 0000000000..0cc7232871 --- /dev/null +++ b/src/app/+community-page/create-community-page/create-community-page.guard.spec.ts @@ -0,0 +1,67 @@ +import { CreateCommunityPageGuard } from './create-community-page.guard'; +import { MockRouter } from '../../shared/mocks/mock-router'; +import { RemoteData } from '../../core/data/remote-data'; +import { Community } from '../../core/shared/community.model'; +import { of as observableOf } from 'rxjs'; +import { first } from 'rxjs/operators'; + +describe('CreateCommunityPageGuard', () => { + describe('canActivate', () => { + let guard: CreateCommunityPageGuard; + let router; + let communityDataServiceStub: any; + + beforeEach(() => { + communityDataServiceStub = { + findById: (id: string) => { + if (id === 'valid-id') { + return observableOf(new RemoteData(false, false, true, null, new Community())); + } else if (id === 'invalid-id') { + return observableOf(new RemoteData(false, false, true, null, undefined)); + } else if (id === 'error-id') { + return observableOf(new RemoteData(false, false, false, null, new Community())); + } + } + }; + router = new MockRouter(); + + guard = new CreateCommunityPageGuard(router, communityDataServiceStub); + }); + + it('should return true when the parent ID resolves to a community', () => { + guard.canActivate({ queryParams: { parent: 'valid-id' } } as any, undefined) + .pipe(first()) + .subscribe( + (canActivate) => + expect(canActivate).toEqual(true) + ); + }); + + it('should return true when no parent ID has been provided', () => { + guard.canActivate({ queryParams: { } } as any, undefined) + .pipe(first()) + .subscribe( + (canActivate) => + expect(canActivate).toEqual(true) + ); + }); + + it('should return false when the parent ID does not resolve to a community', () => { + guard.canActivate({ queryParams: { parent: 'invalid-id' } } as any, undefined) + .pipe(first()) + .subscribe( + (canActivate) => + expect(canActivate).toEqual(false) + ); + }); + + it('should return false when the parent ID resolves to an error response', () => { + guard.canActivate({ queryParams: { parent: 'error-id' } } as any, undefined) + .pipe(first()) + .subscribe( + (canActivate) => + expect(canActivate).toEqual(false) + ); + }); + }); +}); diff --git a/src/app/+community-page/create-community-page/create-community-page.guard.ts b/src/app/+community-page/create-community-page/create-community-page.guard.ts new file mode 100644 index 0000000000..2ee5cb6064 --- /dev/null +++ b/src/app/+community-page/create-community-page/create-community-page.guard.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; + +import { hasNoValue, hasValue } from '../../shared/empty.util'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { RemoteData } from '../../core/data/remote-data'; +import { Community } from '../../core/shared/community.model'; +import { getFinishedRemoteData } from '../../core/shared/operators'; +import { map, tap } from 'rxjs/operators'; +import { Observable, of as observableOf } from 'rxjs'; + +/** + * Prevent creation of a community with an invalid parent community provided + * @class CreateCommunityPageGuard + */ +@Injectable() +export class CreateCommunityPageGuard implements CanActivate { + public constructor(private router: Router, private communityService: CommunityDataService) { + } + + /** + * True when either NO parent ID query parameter has been provided, or the parent ID resolves to a valid parent community + * Reroutes to a 404 page when the page cannot be activated + * @method canActivate + */ + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + const parentID = route.queryParams.parent; + if (hasNoValue(parentID)) { + return observableOf(true); + } + + const parent: Observable> = this.communityService.findById(parentID) + .pipe( + getFinishedRemoteData(), + ); + + return parent.pipe( + map((communityRD: RemoteData) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)), + tap((isValid: boolean) => { + if (!isValid) { + this.router.navigate(['/404']); + } + }) + ); + } +} diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.html b/src/app/+community-page/edit-community-page/edit-community-page.component.html index 1dc6442307..98649243cc 100644 --- a/src/app/+community-page/edit-community-page/edit-community-page.component.html +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.html @@ -4,5 +4,5 @@
- +
diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.ts b/src/app/+community-page/edit-community-page/edit-community-page.component.ts index 1528c9e8d5..8db5eab204 100644 --- a/src/app/+community-page/edit-community-page/edit-community-page.component.ts +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.ts @@ -1,45 +1,28 @@ import { Component } from '@angular/core'; import { Community } from '../../core/shared/community.model'; import { CommunityDataService } from '../../core/data/community-data.service'; -import { Observable } from 'rxjs'; import { RouteService } from '../../shared/services/route.service'; import { ActivatedRoute, Router } from '@angular/router'; -import { RemoteData } from '../../core/data/remote-data'; -import { isNotUndefined } from '../../shared/empty.util'; -import { first, map } from 'rxjs/operators'; -import { getSucceededRemoteData } from '../../core/shared/operators'; +import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model'; +import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component'; +/** + * Component that represents the page where a user can edit an existing Community + */ @Component({ selector: 'ds-edit-community', styleUrls: ['./edit-community-page.component.scss'], templateUrl: './edit-community-page.component.html' }) -export class EditCommunityPageComponent { - - public parentUUID$: Observable; - public parentRD$: Observable>; - public communityRD$: Observable>; +export class EditCommunityPageComponent extends EditComColPageComponent { + protected frontendURL = '/communities/'; public constructor( - private communityDataService: CommunityDataService, - private routeService: RouteService, - private router: Router, - private route: ActivatedRoute + protected communityDataService: CommunityDataService, + protected routeService: RouteService, + protected router: Router, + protected route: ActivatedRoute ) { - } - - ngOnInit(): void { - this.communityRD$ = this.route.data.pipe(first(), map((data) => data.community)); - } - - onSubmit(community: Community) { - this.communityDataService.update(community) - .pipe(getSucceededRemoteData()) - .subscribe((communityRD: RemoteData) => { - if (isNotUndefined(communityRD)) { - const newUUID = communityRD.payload.uuid; - this.router.navigate(['/communities/' + newUUID]); - } - }); + super(communityDataService, routeService, router, route); } } diff --git a/src/app/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts b/src/app/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts deleted file mode 100644 index 5e11a71c2e..0000000000 --- a/src/app/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts +++ /dev/null @@ -1,45 +0,0 @@ -import {Component, OnInit} from '@angular/core'; -import { Community } from '../../core/shared/community.model'; -import { Observable } from 'rxjs'; -import { RouteService } from '../../shared/services/route.service'; -import { ActivatedRoute, Router } from '@angular/router'; -import { RemoteData } from '../../core/data/remote-data'; -import { isNotUndefined } from '../../shared/empty.util'; -import { first, map } from 'rxjs/operators'; -import { getSucceededRemoteData } from '../../core/shared/operators'; -import { DataService } from '../../core/data/data.service'; -import { NormalizedDSpaceObject } from '../../core/cache/models/normalized-dspace-object.model'; -import { DSpaceObject } from '../../core/shared/dspace-object.model'; - -@Component({ - selector: 'ds-edit-community', - styleUrls: ['./edit-community-page.component.scss'], - templateUrl: './edit-community-page.component.html' -}) -export class EditComColPageComponent implements OnInit { - protected frontendURL: string; - public dsoRD$: Observable>; - - public constructor( - protected dsoDataService: DataService, - private routeService: RouteService, - private router: Router, - private route: ActivatedRoute - ) { - } - - ngOnInit(): void { - this.dsoRD$ = this.route.data.pipe(first(), map((data) => data.dso)); - } - - onSubmit(dso: TDomain) { - this.dsoDataService.update(dso) - .pipe(getSucceededRemoteData()) - .subscribe((dsoRD: RemoteData) => { - if (isNotUndefined(dsoRD)) { - const newUUID = dsoRD.payload.uuid; - this.router.navigate([this.frontendURL + newUUID]); - } - }); - } -} diff --git a/src/app/core/data/config-response-parsing.service.spec.ts b/src/app/core/data/config-response-parsing.service.spec.ts index a33c5cf5b5..a2c5cbbadc 100644 --- a/src/app/core/data/config-response-parsing.service.spec.ts +++ b/src/app/core/data/config-response-parsing.service.spec.ts @@ -161,7 +161,13 @@ describe('ConfigResponseParsingService', () => { page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } }, statusCode: '500' }; - const pageinfo = Object.assign(new PageInfo(), { elementsPerPage: 4, totalElements: 4, totalPages: 1, currentPage: 1 }); + const pageinfo = Object.assign(new PageInfo(), { + elementsPerPage: 4, + totalElements: 4, + totalPages: 1, + currentPage: 1, + self: 'https://rest.api/config/submissiondefinitions/traditional/sections' + }); const definitions = Object.assign(new SubmissionDefinitionsModel(), { isDefault: true, diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index ad6fb5f096..9a690e3c4b 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -5,18 +5,17 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv import { CoreState } from '../core.reducers'; import { Store } from '@ngrx/store'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { Observable } from 'rxjs'; +import { Observable, of as observableOf } from 'rxjs'; import { FindAllOptions } from './request.models'; -import { SortOptions, SortDirection } from '../cache/models/sort-options.model'; -import { of as observableOf } from 'rxjs'; +import { SortDirection, SortOptions } from '../cache/models/sort-options.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { Operation } from '../../../../node_modules/fast-json-patch'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { AuthService } from '../auth/auth.service'; import { UpdateComparator } from './update-comparator'; import { HttpClient } from '@angular/common/http'; import { DataBuildService } from '../cache/builders/data-build.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { compare } from 'fast-json-patch'; const endpoint = 'https://rest.api/core'; @@ -45,6 +44,12 @@ class TestService extends DataService { } } +class DummyComparator implements UpdateComparator { + compare(object1: NormalizedTestObject, object2: NormalizedTestObject): Operation[] { + return compare((object1 as any).metadata, (object2 as any).metadata); + } + +} describe('DataService', () => { let service: TestService; let options: FindAllOptions; @@ -53,8 +58,10 @@ describe('DataService', () => { const rdbService = {} as RemoteDataBuildService; const notificationsService = {} as NotificationsService; const http = {} as HttpClient; - const comparator = {} as any; - const dataBuildService = {} as DataBuildService; + const comparator = new DummyComparator() as any; + const dataBuildService = { + normalize: (object) => object + } as DataBuildService; const objectCache = { addPatch: () => { /* empty */ @@ -136,7 +143,7 @@ describe('DataService', () => { elementsPerPage: 10, sort: sortOptions, startsWith: 'ab' - } + }; const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` + `&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`; @@ -169,7 +176,7 @@ describe('DataService', () => { const name1 = 'random string'; const name2 = 'another random string'; beforeEach(() => { - operations = [{ op: 'replace', path: '/metadata/dc.title', value: name2 } as Operation]; + operations = [{ op: 'replace', path: '/0/value', value: name2 } as Operation]; selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; dso = new DSpaceObject(); @@ -180,17 +187,18 @@ describe('DataService', () => { dso2.self = selfLink; dso2.metadata = [{ key: 'dc.title', value: name2 }]; - spyOn(objectCache, 'getBySelfLink').and.returnValue(dso); + spyOn(service, 'findById').and.returnValues(observableOf(dso)); + spyOn(objectCache, 'getBySelfLink').and.returnValues(observableOf(dso)); spyOn(objectCache, 'addPatch'); }); it('should call addPatch on the object cache with the right parameters when there are differences', () => { - service.update(dso2); + service.update(dso2).subscribe(); expect(objectCache.addPatch).toHaveBeenCalledWith(selfLink, operations); }); it('should not call addPatch on the object cache with the right parameters when there are no differences', () => { - service.update(dso); + service.update(dso).subscribe(); expect(objectCache.addPatch).not.toHaveBeenCalled(); }); }); diff --git a/src/app/core/data/update-comparator.ts b/src/app/core/data/update-comparator.ts index 884ac585f5..f064d7f3f2 100644 --- a/src/app/core/data/update-comparator.ts +++ b/src/app/core/data/update-comparator.ts @@ -3,4 +3,4 @@ import { Operation } from 'fast-json-patch/lib/core'; export interface UpdateComparator { compare(object1: TNormalized, object2: TNormalized): Operation[]; -} \ No newline at end of file +} diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 21ace8a250..ef1d69b8b5 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -35,6 +35,8 @@ import { AuthService } from '../auth/auth.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; import { EmptyError } from 'rxjs/internal-compatibility'; +import { DataBuildService } from '../cache/builders/data-build.service'; +import { DSOUpdateComparator } from '../data/dso-update-comparator'; /* tslint:disable:max-classes-per-file */ @Component({ @@ -117,6 +119,8 @@ describe('MetadataService', () => { { provide: AuthService, useValue: {} }, { provide: NotificationsService, useValue: {} }, { provide: HttpClient, useValue: {} }, + { provide: DataBuildService, useValue: {} }, + { provide: DSOUpdateComparator, useValue: {} }, Meta, Title, ItemDataService, diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index e73613fddb..9dbfae3f90 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -67,7 +67,7 @@ export class MetadataService { public processRemoteData(remoteData: Observable>): void { remoteData.pipe(map((rd: RemoteData) => rd.payload), filter((co: CacheableObject) => hasValue(co)), - take(1),) + take(1)) .subscribe((dspaceObject: DSpaceObject) => { if (!this.initialized) { this.initialize(dspaceObject); @@ -269,7 +269,7 @@ export class MetadataService { const item = this.currentObject.value as Item; item.getFiles() .pipe( - find((files) => isNotEmpty(files)), + first((files) => isNotEmpty(files)), catchError((error) => { console.debug(error.message); return [] @@ -277,7 +277,7 @@ export class MetadataService { .subscribe((bitstreams: Bitstream[]) => { for (const bitstream of bitstreams) { bitstream.format.pipe( - take(1), + first(), catchError((error: Error) => { console.debug(error.message); return [] diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index 388c4289e2..09cb86e010 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -62,6 +62,10 @@ export const getSucceededRemoteData = () => (source: Observable>): Observable> => source.pipe(find((rd: RemoteData) => rd.hasSucceeded)); +export const getFinishedRemoteData = () => + (source: Observable>): Observable> => + source.pipe(find((rd: RemoteData) => !rd.isLoading)); + export const toDSpaceObjectListRD = () => (source: Observable>>>): Observable>> => source.pipe( diff --git a/src/app/comcol-forms/comcol-form/comcol-form.component.html b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.html similarity index 100% rename from src/app/comcol-forms/comcol-form/comcol-form.component.html rename to src/app/shared/comcol-forms/comcol-form/comcol-form.component.html diff --git a/src/app/shared/mocks/mock-response-cache.service.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.scss similarity index 100% rename from src/app/shared/mocks/mock-response-cache.service.ts rename to src/app/shared/comcol-forms/comcol-form/comcol-form.component.scss diff --git a/src/app/+community-page/community-form/community-form.component.spec.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts similarity index 58% rename from src/app/+community-page/community-form/community-form.component.spec.ts rename to src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts index b90a2b0713..a6f5e0d45a 100644 --- a/src/app/+community-page/community-form/community-form.component.spec.ts +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.spec.ts @@ -1,41 +1,46 @@ -import { CommunityFormComponent } from './community-form.component'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; import { Location } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { - DynamicFormService, - DynamicInputControlModel, - DynamicInputModel -} from '@ng-dynamic-forms/core'; +import { DynamicFormService, DynamicInputModel } from '@ng-dynamic-forms/core'; import { FormControl, FormGroup } from '@angular/forms'; import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model'; -import { Community } from '../../core/shared/community.model'; -import { ResourceType } from '../../core/shared/resource-type'; +import { Community } from '../../../core/shared/community.model'; +import { ResourceType } from '../../../core/shared/resource-type'; +import { ComColFormComponent } from './comcol-form.component'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { hasValue } from '../../empty.util'; +import { Metadatum } from '../../../core/shared/metadatum.model'; -fdescribe('CommunityFormComponent', () => { - let comp: CommunityFormComponent; - let fixture: ComponentFixture; +describe('ComColFormComponent', () => { + let comp: ComColFormComponent; + let fixture: ComponentFixture>; let location: Location; const formServiceStub: any = { - createFormGroup: (formModel: DynamicFormControlModel[]) => { + createFormGroup: (fModel: DynamicFormControlModel[]) => { const controls = {}; - formModel.forEach((controlModel) => { - controls[controlModel.id] = new FormControl((controlModel as any).value); - }); - return new FormGroup(controls); + if (hasValue(fModel)) { + fModel.forEach((controlModel) => { + controls[controlModel.id] = new FormControl((controlModel as any).value); + }); + return new FormGroup(controls); + } + return undefined; } }; - const titleMD = { key: 'dc.title', value: 'Community Title' }; - const randomMD = { key: 'dc.random', value: 'Random metadata excluded from form' }; - const abstractMD = { key: 'dc.description.abstract', value: 'Community description' }; - const newTitleMD = { key: 'dc.title', value: 'New Community Title' }; + const titleMD = { key: 'dc.title', value: 'Community Title' } as Metadatum; + const randomMD = { key: 'dc.random', value: 'Random metadata excluded from form' } as Metadatum; + const abstractMD = { + key: 'dc.description.abstract', + value: 'Community description' + } as Metadatum; + const newTitleMD = { key: 'dc.title', value: 'New Community Title' } as Metadatum; const formModel = [ new DynamicInputModel({ id: 'title', name: newTitleMD.key, - value: newTitleMD.value + value: 'New Community Title' }), new DynamicInputModel({ id: 'abstract', @@ -54,7 +59,7 @@ fdescribe('CommunityFormComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), RouterTestingModule], - declarations: [CommunityFormComponent], + declarations: [ComColFormComponent], providers: [ { provide: Location, useValue: locationStub }, { provide: DynamicFormService, useValue: formServiceStub } @@ -64,20 +69,22 @@ fdescribe('CommunityFormComponent', () => { })); beforeEach(() => { - fixture = TestBed.createComponent(CommunityFormComponent); + fixture = TestBed.createComponent(ComColFormComponent); comp = fixture.componentInstance; + comp.formModel = []; + comp.dso = new Community(); fixture.detectChanges(); location = (comp as any).location; - comp.formModel = formModel; }); describe('onSubmit', () => { beforeEach(() => { spyOn(comp.submitForm, 'emit'); + comp.formModel = formModel; }); - it('should update emit the new version of the community', () => { - comp.community = Object.assign( + it('should emit the new version of the community', () => { + comp.dso = Object.assign( new Community(), { metadata: [ diff --git a/src/app/comcol-forms/comcol-form/comcol-form.component.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts similarity index 65% rename from src/app/comcol-forms/comcol-form/comcol-form.component.ts rename to src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts index d49e197f5d..19050e2bc2 100644 --- a/src/app/comcol-forms/comcol-form/comcol-form.component.ts +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts @@ -7,23 +7,50 @@ import { import { FormGroup } from '@angular/forms'; import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model'; import { TranslateService } from '@ngx-translate/core'; -import { DSpaceObject } from '../../core/shared/dspace-object.model'; -import { isNotEmpty } from '../../shared/empty.util'; -import { ResourceType } from '../../core/shared/resource-type'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { isNotEmpty } from '../../empty.util'; +import { ResourceType } from '../../../core/shared/resource-type'; @Component({ selector: 'ds-comcol-form', - // styleUrls: ['./comcol-form.component.scss'], + styleUrls: ['./comcol-form.component.scss'], templateUrl: './comcol-form.component.html' }) export class ComColFormComponent implements OnInit { + /** + * DSpaceObject that the form represents + */ @Input() dso: T; - type; - LABEL_KEY_PREFIX = this.type + '.form.'; - ERROR_KEY_PREFIX = this.type + '.form.errors.'; + + /** + * Type of DSpaceObject that the form represents + */ + protected type; + + /** + * @type {string} Key prefix used to generate form labels + */ + LABEL_KEY_PREFIX = '.form.'; + + /** + * @type {string} Key prefix used to generate form error messages + */ + ERROR_KEY_PREFIX = '.form.errors.'; + + /** + * The form model that represents the fields in the form + */ formModel: DynamicFormControlModel[]; + + /** + * The form group of this form + */ formGroup: FormGroup; + /** + * Emits DSO when the form is submitted + * @type {EventEmitter} + */ @Output() submitForm: EventEmitter = new EventEmitter(); public constructor(private location: Location, @@ -38,6 +65,7 @@ export class ComColFormComponent implements OnInit { } ); this.formGroup = this.formService.createFormGroup(this.formModel); + this.updateFieldTranslations(); this.translate.onLangChange .subscribe(() => { @@ -45,6 +73,9 @@ export class ComColFormComponent implements OnInit { }); } + /** + * Checks which new fields where added and sends the updated version of the DSO to the parent component + */ onSubmit() { const metadata = this.formModel.map( (fieldModel: DynamicInputModel) => { @@ -53,6 +84,7 @@ export class ComColFormComponent implements OnInit { ); const filteredOldMetadata = this.dso.metadata.filter((filter) => !metadata.map((md) => md.key).includes(filter.key)); const filteredNewMetadata = metadata.filter((md) => isNotEmpty(md.value)); + const newMetadata = [...filteredOldMetadata, ...filteredNewMetadata]; const updatedDSO = Object.assign({}, this.dso, { metadata: newMetadata, @@ -61,14 +93,17 @@ export class ComColFormComponent implements OnInit { this.submitForm.emit(updatedDSO); } + /** + * Used the update translations of errors and labels on init and on language change + */ private updateFieldTranslations() { this.formModel.forEach( (fieldModel: DynamicInputModel) => { - fieldModel.label = this.translate.instant(this.LABEL_KEY_PREFIX + fieldModel.id); + fieldModel.label = this.translate.instant(this.type + this.LABEL_KEY_PREFIX + fieldModel.id); if (isNotEmpty(fieldModel.validators)) { fieldModel.errorMessages = {}; Object.keys(fieldModel.validators).forEach((key) => { - fieldModel.errorMessages[key] = this.translate.instant(this.ERROR_KEY_PREFIX + fieldModel.id + '.' + key); + fieldModel.errorMessages[key] = this.translate.instant(this.type + this.ERROR_KEY_PREFIX + fieldModel.id + '.' + key); }); } } diff --git a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts similarity index 70% rename from src/app/+community-page/create-community-page/create-community-page.component.spec.ts rename to src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts index 83db9561cc..fd3464ba5e 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.spec.ts @@ -1,21 +1,25 @@ -import { CreateCommunityPageComponent } from './create-community-page.component'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { CommunityDataService } from '../../core/data/community-data.service'; -import { RouteService } from '../../shared/services/route.service'; +import { CommunityDataService } from '../../../core/data/community-data.service'; +import { RouteService } from '../../services/route.service'; import { Router } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { RemoteData } from '../../core/data/remote-data'; -import { Community } from '../../core/shared/community.model'; -import { SharedModule } from '../../shared/shared.module'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Community } from '../../../core/shared/community.model'; +import { SharedModule } from '../../shared.module'; import { CommonModule } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { NormalizedDSpaceObject } from '../../../core/cache/models/normalized-dspace-object.model'; +import { CreateComColPageComponent } from './create-comcol-page.component'; +import { DataService } from '../../../core/data/data.service'; -fdescribe('CreateCommunityPageComponent', () => { - let comp: CreateCommunityPageComponent; - let fixture: ComponentFixture; +describe('CreateComColPageComponent', () => { + let comp: CreateComColPageComponent; + let fixture: ComponentFixture>; let communityDataService: CommunityDataService; + let dsoDataService: CommunityDataService; let routeService: RouteService; let router: Router; @@ -67,8 +71,8 @@ fdescribe('CreateCommunityPageComponent', () => { initializeVars(); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [CreateCommunityPageComponent], providers: [ + { provide: DataService, useValue: communityDataServiceStub }, { provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: RouteService, useValue: routeServiceStub }, { provide: Router, useValue: routerStub }, @@ -78,9 +82,10 @@ fdescribe('CreateCommunityPageComponent', () => { })); beforeEach(() => { - fixture = TestBed.createComponent(CreateCommunityPageComponent); + fixture = TestBed.createComponent(CreateComColPageComponent); comp = fixture.componentInstance; fixture.detectChanges(); + dsoDataService = (comp as any).dsoDataService; communityDataService = (comp as any).communityDataService; routeService = (comp as any).routeService; router = (comp as any).router; @@ -105,7 +110,7 @@ fdescribe('CreateCommunityPageComponent', () => { it('should not navigate on failure', () => { spyOn(router, 'navigate'); - spyOn(communityDataService, 'create').and.returnValue(observableOf(new RemoteData(true, true, false, undefined, newCommunity))); + spyOn(dsoDataService, 'create').and.returnValue(observableOf(new RemoteData(true, true, false, undefined, newCommunity))); comp.onSubmit(data); fixture.detectChanges(); expect(router.navigate).not.toHaveBeenCalled(); diff --git a/src/app/comcol-forms/create-comcol-page/create-comcol-page.component.ts b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts similarity index 54% rename from src/app/comcol-forms/create-comcol-page/create-comcol-page.component.ts rename to src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts index 18f23cf528..2bcd9f9bd0 100644 --- a/src/app/comcol-forms/create-comcol-page/create-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts @@ -1,25 +1,38 @@ import { Component, OnInit } from '@angular/core'; -import { Community } from '../../core/shared/community.model'; -import { CommunityDataService } from '../../core/data/community-data.service'; +import { Community } from '../../../core/shared/community.model'; +import { CommunityDataService } from '../../../core/data/community-data.service'; import { Observable } from 'rxjs'; -import { RouteService } from '../../shared/services/route.service'; +import { RouteService } from '../../services/route.service'; import { Router } from '@angular/router'; -import { RemoteData } from '../../core/data/remote-data'; -import { isNotEmpty, isNotUndefined } from '../../shared/empty.util'; +import { RemoteData } from '../../../core/data/remote-data'; +import { isNotEmpty, isNotUndefined } from '../../empty.util'; import { take } from 'rxjs/operators'; -import { getSucceededRemoteData } from '../../core/shared/operators'; -import { DSpaceObject } from '../../core/shared/dspace-object.model'; -import { DataService } from '../../core/data/data.service'; -import { NormalizedDSpaceObject } from '../../core/cache/models/normalized-dspace-object.model'; +import { getSucceededRemoteData } from '../../../core/shared/operators'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { DataService } from '../../../core/data/data.service'; +import { NormalizedDSpaceObject } from '../../../core/cache/models/normalized-dspace-object.model'; +/** + * Component representing the create page for communities and collections + */ @Component({ - selector: 'ds-create-community', - styleUrls: ['./create-community-page.component.scss'], - templateUrl: './create-community-page.component.html' + selector: 'ds-create-comcol', + template: '' }) export class CreateComColPageComponent implements OnInit { + /** + * Frontend endpoint where for this type of DSP + */ protected frontendURL: string; + + /** + * The provided UUID for the parent community + */ public parentUUID$: Observable; + + /** + * The parent community of the object that is to be created + */ public parentRD$: Observable>; public constructor( @@ -40,6 +53,10 @@ export class CreateComColPageComponent { this.dsoDataService.create(dso, uuid) @@ -49,7 +66,7 @@ export class CreateComColPageComponent { - let comp: EditCommunityPageComponent; - let fixture: ComponentFixture; +describe('EditComColPageComponent', () => { + let comp: EditComColPageComponent; + let fixture: ComponentFixture>; let communityDataService: CommunityDataService; + let dsoDataService: CommunityDataService; let routeService: RouteService; let router: Router; @@ -73,9 +76,8 @@ fdescribe('EditCommunityPageComponent', () => { initializeVars(); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [EditCommunityPageComponent], providers: [ - { provide: CommunityDataService, useValue: communityDataServiceStub }, + { provide: DataService, useValue: communityDataServiceStub }, { provide: RouteService, useValue: routeServiceStub }, { provide: Router, useValue: routerStub }, { provide: ActivatedRoute, useValue: routeStub }, @@ -85,9 +87,10 @@ fdescribe('EditCommunityPageComponent', () => { })); beforeEach(() => { - fixture = TestBed.createComponent(EditCommunityPageComponent); + fixture = TestBed.createComponent(EditComColPageComponent); comp = fixture.componentInstance; fixture.detectChanges(); + dsoDataService = (comp as any).dsoDataService; communityDataService = (comp as any).communityDataService; routeService = (comp as any).routeService; router = (comp as any).router; @@ -112,7 +115,7 @@ fdescribe('EditCommunityPageComponent', () => { it('should not navigate on failure', () => { spyOn(router, 'navigate'); - spyOn(communityDataService, 'update').and.returnValue(observableOf(new RemoteData(true, true, false, undefined, newCommunity))); + spyOn(dsoDataService, 'update').and.returnValue(observableOf(new RemoteData(true, true, false, undefined, newCommunity))); comp.onSubmit(data); fixture.detectChanges(); expect(router.navigate).not.toHaveBeenCalled(); diff --git a/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts new file mode 100644 index 0000000000..8f03280fc0 --- /dev/null +++ b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts @@ -0,0 +1,56 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { RouteService } from '../../services/route.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { RemoteData } from '../../../core/data/remote-data'; +import { isNotUndefined } from '../../empty.util'; +import { first, map } from 'rxjs/operators'; +import { getSucceededRemoteData } from '../../../core/shared/operators'; +import { DataService } from '../../../core/data/data.service'; +import { NormalizedDSpaceObject } from '../../../core/cache/models/normalized-dspace-object.model'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; + +/** + * Component representing the edit page for communities and collections + */ +@Component({ + selector: 'ds-edit-comcol', + template: '' +}) +export class EditComColPageComponent implements OnInit { + /** + * Frontend endpoint where for this type of DSP + */ + protected frontendURL: string; + /** + * The initial DSO object + */ + public dsoRD$: Observable>; + + public constructor( + protected dsoDataService: DataService, + protected routeService: RouteService, + protected router: Router, + protected route: ActivatedRoute + ) { + } + + ngOnInit(): void { + this.dsoRD$ = this.route.data.pipe(first(), map((data) => data.dso)); + } + + /** + * @param {TDomain} dso The updated version of the DSO + * Updates an existing DSO based on the submitted user data and navigates to the editted object's home page + */ + onSubmit(dso: TDomain) { + this.dsoDataService.update(dso) + .pipe(getSucceededRemoteData()) + .subscribe((dsoRD: RemoteData) => { + if (isNotUndefined(dsoRD)) { + const newUUID = dsoRD.payload.uuid; + this.router.navigate([this.frontendURL + newUUID]); + } + }); + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index b7d42e4856..a261590b47 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -86,6 +86,9 @@ import { CapitalizePipe } from './utils/capitalize.pipe'; import { ObjectKeysPipe } from './utils/object-keys-pipe'; import { MomentModule } from 'ngx-moment'; import { MenuModule } from './menu/menu.module'; +import { ComColFormComponent } from './comcol-forms/comcol-form/comcol-form.component'; +import { CreateComColPageComponent } from './comcol-forms/create-comcol-page/create-comcol-page.component'; +import { EditComColPageComponent } from './comcol-forms/edit-comcol-page/edit-comcol-page.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -129,6 +132,9 @@ const COMPONENTS = [ ComcolPageContentComponent, ComcolPageHeaderComponent, ComcolPageLogoComponent, + ComColFormComponent, + CreateComColPageComponent, + EditComColPageComponent, DsDynamicFormComponent, DsDynamicFormControlComponent, DsDynamicListComponent, diff --git a/src/app/shared/testing/utils.ts b/src/app/shared/testing/utils.ts index 8714358100..cd17a1b1f5 100644 --- a/src/app/shared/testing/utils.ts +++ b/src/app/shared/testing/utils.ts @@ -41,4 +41,4 @@ export function spyOnOperator(obj: any, prop: string): any { }); return spyOn(obj, prop); -} \ No newline at end of file +}