diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 299c2afe634626e79f0f017cec35d230b8e2ac74..fc4c0aee5709a969da5b9458651c4a5d702a6cd0 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -244,6 +244,8 @@ "collection.create.head": "Create a Collection", + "collection.create.notifications.success": "Successfully created the Collection", + "collection.create.sub-head": "Create a Collection for Community {{ parent }}", "collection.delete.cancel": "Cancel", @@ -302,6 +304,46 @@ + "collection.edit.logo.label": "Collection logo", + + "collection.edit.logo.notifications.add.error": "Uploading Collection logo failed. Please verify the content before retrying.", + + "collection.edit.logo.notifications.add.success": "Upload Collection logo successful.", + + "collection.edit.logo.notifications.delete.success.title": "Logo deleted", + + "collection.edit.logo.notifications.delete.success.content": "Successfully deleted the collection's logo", + + "collection.edit.logo.notifications.delete.error.title": "Error deleting logo", + + "collection.edit.logo.upload": "Drop a Collection Logo to upload", + + + + "collection.edit.notifications.success": "Successfully edited the Collection", + + "collection.edit.return": "Return", + + + + "collection.edit.tabs.curate.head": "Curate", + + "collection.edit.tabs.curate.title": "Collection Edit - Curate", + + "collection.edit.tabs.metadata.head": "Edit Metadata", + + "collection.edit.tabs.metadata.title": "Collection Edit - Metadata", + + "collection.edit.tabs.roles.head": "Assign Roles", + + "collection.edit.tabs.roles.title": "Collection Edit - Roles", + + "collection.edit.tabs.source.head": "Content Source", + + "collection.edit.tabs.source.title": "Collection Edit - Content Source", + + + "collection.form.abstract": "Short Description", "collection.form.description": "Introductory text (HTML)", @@ -350,6 +392,8 @@ "community.create.head": "Create a Community", + "community.create.notifications.success": "Successfully created the Community", + "community.create.sub-head": "Create a Sub-Community for Community {{ parent }}", "community.delete.cancel": "Cancel", @@ -368,6 +412,44 @@ "community.edit.head": "Edit Community", + + + "community.edit.logo.label": "Community logo", + + "community.edit.logo.notifications.add.error": "Uploading Community logo failed. Please verify the content before retrying.", + + "community.edit.logo.notifications.add.success": "Upload Community logo successful.", + + "community.edit.logo.notifications.delete.success.title": "Logo deleted", + + "community.edit.logo.notifications.delete.success.content": "Successfully deleted the community's logo", + + "community.edit.logo.notifications.delete.error.title": "Error deleting logo", + + "community.edit.logo.upload": "Drop a Community Logo to upload", + + + + "community.edit.notifications.success": "Successfully edited the Community", + + "community.edit.return": "Return", + + + + "community.edit.tabs.curate.head": "Curate", + + "community.edit.tabs.curate.title": "Community Edit - Curate", + + "community.edit.tabs.metadata.head": "Edit Metadata", + + "community.edit.tabs.metadata.title": "Community Edit - Metadata", + + "community.edit.tabs.roles.head": "Assign Roles", + + "community.edit.tabs.roles.title": "Community Edit - Roles", + + + "community.form.abstract": "Short Description", "community.form.description": "Introductory text (HTML)", @@ -1771,7 +1853,7 @@ "uploader.drag-message": "Drag & Drop your files here", - "uploader.or": ", or", + "uploader.or": ", or ", "uploader.processing": "Processing", 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 bc9eaab9947d81ea9806bca907c4d2e3bbfec993..59433e49a097746574b423b865c885105f5082a5 100644 --- a/src/app/+collection-page/collection-form/collection-form.component.ts +++ b/src/app/+collection-page/collection-form/collection-form.component.ts @@ -1,7 +1,19 @@ import { Component, Input } from '@angular/core'; -import { DynamicFormControlModel, DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core'; +import { + DynamicFormControlModel, + DynamicFormService, + DynamicInputModel, + DynamicTextAreaModel +} from '@ng-dynamic-forms/core'; import { Collection } from '../../core/shared/collection.model'; import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component'; +import { Location } from '@angular/common'; +import { TranslateService } from '@ngx-translate/core'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { AuthService } from '../../core/auth/auth.service'; +import { RequestService } from '../../core/data/request.service'; +import { ObjectCacheService } from '../../core/cache/object-cache.service'; /** * Form used for creating and editing collections @@ -20,7 +32,7 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> { /** * @type {Collection.type} This is a collection-type form */ - protected type = Collection.type; + type = Collection.type; /** * The dynamic form fields used for creating/editing a collection @@ -63,4 +75,15 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> { name: 'dc.description.provenance', }), ]; + + public constructor(protected location: Location, + protected formService: DynamicFormService, + protected translate: TranslateService, + protected notificationsService: NotificationsService, + protected authService: AuthService, + protected dsoService: CommunityDataService, + protected requestService: RequestService, + protected objectCache: ObjectCacheService) { + super(location, formService, translate, notificationsService, authService, requestService, objectCache); + } } diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index 66c623657dd76a2006d1a7f3c8d06d7d5601784c..2df7997e1eb7cb42922f7b5af6529a7299673100 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -5,7 +5,6 @@ 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'; import { URLCombiner } from '../core/url-combiner/url-combiner'; @@ -39,12 +38,8 @@ const COLLECTION_EDIT_PATH = ':id/edit'; }, { path: COLLECTION_EDIT_PATH, - pathMatch: 'full', - component: EditCollectionPageComponent, - canActivate: [AuthenticatedGuard], - resolve: { - dso: CollectionPageResolver - } + loadChildren: './edit-collection-page/edit-collection-page.module#EditCollectionPageModule', + canActivate: [AuthenticatedGuard] }, { path: ':id/delete', diff --git a/src/app/+collection-page/collection-page.component.html b/src/app/+collection-page/collection-page.component.html index 12d5c200fd04012df99b9d70e4a1be593a2f5f6d..98552ed40b707fd8448f86f863db7a6b89239b7d 100644 --- a/src/app/+collection-page/collection-page.component.html +++ b/src/app/+collection-page/collection-page.component.html @@ -5,15 +5,17 @@ <div *ngIf="collectionRD?.payload as collection"> <ds-view-tracker [object]="collection"></ds-view-tracker> <header class="comcol-header border-bottom mb-4 pb-4"> + <!-- Collection Name --> + <ds-comcol-page-header + [name]="collection.name"> + </ds-comcol-page-header> <!-- Collection logo --> <ds-comcol-page-logo *ngIf="logoRD$" - [logo]="(logoRD$ | async)?.payload" [alternateText]="'Collection Logo'"> + [logo]="(logoRD$ | async)?.payload" + [alternateText]="'Collection Logo'" [alternateText]="'Collection Logo'"> </ds-comcol-page-logo> - <!-- Collection Name --> - <ds-comcol-page-header - [name]="collection.name"> - </ds-comcol-page-header> + <!-- Handle --> <ds-comcol-page-handle [content]="collection.handle" diff --git a/src/app/+collection-page/collection-page.module.ts b/src/app/+collection-page/collection-page.module.ts index d9e1d9465ee21ad7d9a83283fa847ca5e80f68ad..03daae68ae124ddae1abd27421e66b23a9589785 100644 --- a/src/app/+collection-page/collection-page.module.ts +++ b/src/app/+collection-page/collection-page.module.ts @@ -7,7 +7,6 @@ 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 { EditCollectionPageComponent } from './edit-collection-page/edit-collection-page.component'; import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component'; import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component'; import { SearchService } from '../core/shared/search/search.service'; @@ -23,11 +22,13 @@ import { StatisticsModule } from '../statistics/statistics.module'; declarations: [ CollectionPageComponent, CreateCollectionPageComponent, - EditCollectionPageComponent, DeleteCollectionPageComponent, CollectionFormComponent, CollectionItemMapperComponent ], + exports: [ + CollectionFormComponent + ], providers: [ SearchService, ] 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 b3f4361bc63ca755463dcde6b52265e165274a5d..800d2858461cf63c80a1b98eaccaa90ec1d21c55 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 @@ -4,5 +4,5 @@ <h2 id="sub-header" class="border-bottom pb-2">{{'collection.create.sub-head' | translate:{ parent: (parentRD$| async)?.payload.name } }}</h2> </div> </div> - <ds-collection-form (submitForm)="onSubmit($event)"></ds-collection-form> + <ds-collection-form (submitForm)="onSubmit($event)" (finish)="navigateToNewPage()"></ds-collection-form> </div> diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts index e223b11c6568ec422d950f9841c187778534e3a7..869a89d5e0b605d90c4a1cb224bbde27a22160ec 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts @@ -10,6 +10,8 @@ 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'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub'; describe('CreateCollectionPageComponent', () => { let comp: CreateCollectionPageComponent; @@ -27,6 +29,7 @@ describe('CreateCollectionPageComponent', () => { }, { provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } }, { provide: Router, useValue: {} }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); 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 2cab36d2851ccc5b5fbc8ae102779405bd9bb1e4..ae31b94c3dde271428cd61f4e7de173684cc5bfa 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -5,6 +5,8 @@ import { Router } from '@angular/router'; import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component'; import { Collection } from '../../core/shared/collection.model'; import { CollectionDataService } from '../../core/data/collection-data.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; /** * Component that represents the page where a user can create a new Collection @@ -16,13 +18,16 @@ import { CollectionDataService } from '../../core/data/collection-data.service'; }) export class CreateCollectionPageComponent extends CreateComColPageComponent<Collection> { protected frontendURL = '/collections/'; + protected type = Collection.type; public constructor( protected communityDataService: CommunityDataService, protected collectionDataService: CollectionDataService, protected routeService: RouteService, - protected router: Router + protected router: Router, + protected notificationsService: NotificationsService, + protected translate: TranslateService ) { - super(collectionDataService, communityDataService, routeService, router); + super(collectionDataService, communityDataService, routeService, router, notificationsService, translate); } } diff --git a/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.html b/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.html new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.ts b/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..d7deaea9826ab1f8d328aaff55dcce06b85551cf --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +/** + * Component for managing a collection's curation tasks + */ +@Component({ + selector: 'ds-collection-curate', + templateUrl: './collection-curate.component.html', +}) +export class CollectionCurateComponent { + /* TODO: Implement Collection Edit - Curate */ +} diff --git a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html new file mode 100644 index 0000000000000000000000000000000000000000..6f3a63790dab5eaa3b8a026729166e3302670217 --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html @@ -0,0 +1,6 @@ +<ds-collection-form (submitForm)="onSubmit($event)" + [dso]="(dsoRD$ | async)?.payload" + (finish)="navigateToHomePage()"></ds-collection-form> +<a class="btn btn-danger" + [routerLink]="'/collections/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'collection.edit.delete' + | translate}}</a> diff --git a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..71cb06394f8499d5fa2959578b0e11c42bdee622 --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts @@ -0,0 +1,42 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { SharedModule } from '../../../shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; +import { ActivatedRoute } from '@angular/router'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CollectionMetadataComponent } from './collection-metadata.component'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; + +describe('CollectionMetadataComponent', () => { + let comp: CollectionMetadataComponent; + let fixture: ComponentFixture<CollectionMetadataComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + declarations: [CollectionMetadataComponent], + providers: [ + { provide: CollectionDataService, useValue: {} }, + { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: { payload: {} } }) } } }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CollectionMetadataComponent); + 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/collection-metadata/collection-metadata.component.ts b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..af2ab7d0a7c2ffcf9b0d76bd1c722c30d2fdda94 --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { ComcolMetadataComponent } from '../../../shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; +import { Collection } from '../../../core/shared/collection.model'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; + +/** + * Component for editing a collection's metadata + */ +@Component({ + selector: 'ds-collection-metadata', + templateUrl: './collection-metadata.component.html', +}) +export class CollectionMetadataComponent extends ComcolMetadataComponent<Collection> { + protected frontendURL = '/collections/'; + protected type = Collection.type; + + public constructor( + protected collectionDataService: CollectionDataService, + protected router: Router, + protected route: ActivatedRoute, + protected notificationsService: NotificationsService, + protected translate: TranslateService + ) { + super(collectionDataService, router, route, notificationsService, translate); + } +} diff --git a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.html b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.html new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..39f72fd2cecac2cd02341cf203259bc79e5b40ea --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +/** + * Component for managing a collection's roles + */ +@Component({ + selector: 'ds-collection-roles', + templateUrl: './collection-roles.component.html', +}) +export class CollectionRolesComponent { + /* TODO: Implement Collection Edit - Roles */ +} diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ec5be884d68d03de274b017baaeded26c6c0a21 --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +/** + * Component for managing the content source of the collection + */ +@Component({ + selector: 'ds-collection-source', + templateUrl: './collection-source.component.html', +}) +export class CollectionSourceComponent { + /* TODO: Implement Collection Edit - Content Source */ +} 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 deleted file mode 100644 index c389c681ce5cd81f2d1dc56547b6a9929751c1de..0000000000000000000000000000000000000000 --- a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.html +++ /dev/null @@ -1,11 +0,0 @@ -<div class="container"> - <div class="row"> - <div class="col-12 pb-4"> - <h2 id="header" class="border-bottom pb-2">{{ 'collection.edit.head' | translate }}</h2> - <ds-collection-form (submitForm)="onSubmit($event)" [dso]="(dsoRD$ | async)?.payload"></ds-collection-form> - <a class="btn btn-danger" - [routerLink]="'/collections/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'collection.edit.delete' - | translate}}</a> - </div> - </div> -</div> 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 deleted file mode 100644 index 8b137891791fe96927ad78e64b0aad7bded08bdc..0000000000000000000000000000000000000000 --- a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.scss +++ /dev/null @@ -1 +0,0 @@ - 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 index 193cb293e4d3252e74669664e00ee9f7b54dfe51..9f915d2d7a44638fa9d334ecc4b701cac4d78bc7 100644 --- 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 @@ -13,13 +13,29 @@ describe('EditCollectionPageComponent', () => { let comp: EditCollectionPageComponent; let fixture: ComponentFixture<EditCollectionPageComponent>; + const routeStub = { + data: observableOf({ + dso: { payload: {} } + }), + routeConfig: { + children: [] + }, + snapshot: { + firstChild: { + routeConfig: { + path: 'mockUrl' + } + } + } + }; + beforeEach(async(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], declarations: [EditCollectionPageComponent], providers: [ { provide: CollectionDataService, useValue: {} }, - { provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } }, + { provide: ActivatedRoute, useValue: routeStub }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -31,9 +47,9 @@ describe('EditCollectionPageComponent', () => { fixture.detectChanges(); }); - describe('frontendURL', () => { - it('should have the right frontendURL set', () => { - expect((comp as any).frontendURL).toEqual('/collections/'); + describe('type', () => { + it('should have the right type set', () => { + expect((comp as any).type).toEqual('collection'); }) }); }); 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 index ba70bd26c6ba5566dafa5114805d0dc7c8752c50..209ce5149a1c8499923ebc1df6cc49d789ea5b1e 100644 --- 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 @@ -2,24 +2,30 @@ 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 { Collection } from '../../core/shared/collection.model'; -import { CollectionDataService } from '../../core/data/collection-data.service'; +import { getCollectionPageRoute } from '../collection-page-routing.module'; /** * 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' + templateUrl: '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html' }) export class EditCollectionPageComponent extends EditComColPageComponent<Collection> { - protected frontendURL = '/collections/'; + type = 'collection'; public constructor( - protected collectionDataService: CollectionDataService, protected router: Router, protected route: ActivatedRoute ) { - super(collectionDataService, router, route); + super(router, route); + } + + /** + * Get the collection page url + * @param collection The collection for which the url is requested + */ + getPageUrl(collection: Collection): string { + return getCollectionPageRoute(collection.id) } } diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.module.ts b/src/app/+collection-page/edit-collection-page/edit-collection-page.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..f442aae4d684210314d6bf93089df31b02de6be9 --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.module.ts @@ -0,0 +1,32 @@ +import { NgModule } from '@angular/core'; +import { EditCollectionPageComponent } from './edit-collection-page.component'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../../shared/shared.module'; +import { EditCollectionPageRoutingModule } from './edit-collection-page.routing.module'; +import { CollectionMetadataComponent } from './collection-metadata/collection-metadata.component'; +import { CollectionPageModule } from '../collection-page.module'; +import { CollectionRolesComponent } from './collection-roles/collection-roles.component'; +import { CollectionCurateComponent } from './collection-curate/collection-curate.component'; +import { CollectionSourceComponent } from './collection-source/collection-source.component'; + +/** + * Module that contains all components related to the Edit Collection page administrator functionality + */ +@NgModule({ + imports: [ + CommonModule, + SharedModule, + EditCollectionPageRoutingModule, + CollectionPageModule + ], + declarations: [ + EditCollectionPageComponent, + CollectionMetadataComponent, + CollectionRolesComponent, + CollectionCurateComponent, + CollectionSourceComponent + ] +}) +export class EditCollectionPageModule { + +} diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts b/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..fcfced9d810a52a2802886fcd3256f9fac51d1b8 --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts @@ -0,0 +1,61 @@ +import { RouterModule } from '@angular/router'; +import { NgModule } from '@angular/core'; +import { EditCollectionPageComponent } from './edit-collection-page.component'; +import { CollectionPageResolver } from '../collection-page.resolver'; +import { CollectionMetadataComponent } from './collection-metadata/collection-metadata.component'; +import { CollectionRolesComponent } from './collection-roles/collection-roles.component'; +import { CollectionSourceComponent } from './collection-source/collection-source.component'; +import { CollectionCurateComponent } from './collection-curate/collection-curate.component'; + +/** + * Routing module that handles the routing for the Edit Collection page administrator functionality + */ +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: '', + component: EditCollectionPageComponent, + resolve: { + dso: CollectionPageResolver + }, + children: [ + { + path: '', + redirectTo: 'metadata', + pathMatch: 'full' + }, + { + path: 'metadata', + component: CollectionMetadataComponent, + data: { + title: 'collection.edit.tabs.metadata.title', + hideReturnButton: true + } + }, + { + path: 'roles', + component: CollectionRolesComponent, + data: { title: 'collection.edit.tabs.roles.title' } + }, + { + path: 'source', + component: CollectionSourceComponent, + data: { title: 'collection.edit.tabs.source.title' } + }, + { + path: 'curate', + component: CollectionCurateComponent, + data: { title: 'collection.edit.tabs.curate.title' } + } + ] + } + ]) + ], + providers: [ + CollectionPageResolver, + ] +}) +export class EditCollectionPageRoutingModule { + +} 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 2932d7f1bb795937f9e0397b0669cf141e393b02..e9bd2f66c8ffd60b352717012ea78671b7d12ac5 100644 --- a/src/app/+community-page/community-form/community-form.component.ts +++ b/src/app/+community-page/community-form/community-form.component.ts @@ -1,7 +1,19 @@ import { Component, Input } from '@angular/core'; -import { DynamicFormControlModel, DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core'; +import { + DynamicFormControlModel, + DynamicFormService, + DynamicInputModel, + DynamicTextAreaModel +} from '@ng-dynamic-forms/core'; import { Community } from '../../core/shared/community.model'; import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component'; +import { Location } from '@angular/common'; +import { TranslateService } from '@ngx-translate/core'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { AuthService } from '../../core/auth/auth.service'; +import { RequestService } from '../../core/data/request.service'; +import { ObjectCacheService } from '../../core/cache/object-cache.service'; /** * Form used for creating and editing communities @@ -20,7 +32,7 @@ export class CommunityFormComponent extends ComColFormComponent<Community> { /** * @type {Community.type} This is a community-type form */ - protected type = Community.type; + type = Community.type; /** * The dynamic form fields used for creating/editing a community @@ -55,4 +67,15 @@ export class CommunityFormComponent extends ComColFormComponent<Community> { name: 'dc.description.tableofcontents', }), ]; + + public constructor(protected location: Location, + protected formService: DynamicFormService, + protected translate: TranslateService, + protected notificationsService: NotificationsService, + protected authService: AuthService, + protected dsoService: CommunityDataService, + protected requestService: RequestService, + protected objectCache: ObjectCacheService) { + super(location, formService, translate, notificationsService, authService, requestService, objectCache); + } } diff --git a/src/app/+community-page/community-page-routing.module.ts b/src/app/+community-page/community-page-routing.module.ts index cecd17ec10844aa4912882f52fb73f119f5d5f48..df548e061729ce9d795855d1fa0bb286aede0b08 100644 --- a/src/app/+community-page/community-page-routing.module.ts +++ b/src/app/+community-page/community-page-routing.module.ts @@ -5,7 +5,6 @@ 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'; import { URLCombiner } from '../core/url-combiner/url-combiner'; @@ -38,12 +37,8 @@ const COMMUNITY_EDIT_PATH = ':id/edit'; }, { path: COMMUNITY_EDIT_PATH, - pathMatch: 'full', - component: EditCommunityPageComponent, - canActivate: [AuthenticatedGuard], - resolve: { - dso: CommunityPageResolver - } + loadChildren: './edit-community-page/edit-community-page.module#EditCommunityPageModule', + canActivate: [AuthenticatedGuard] }, { path: ':id/delete', diff --git a/src/app/+community-page/community-page.component.html b/src/app/+community-page/community-page.component.html index 5bd7089e820e03be9484cb0f9665b28b26a4160b..dfd1ce93d900e5d735afc44506574b4688bebfc9 100644 --- a/src/app/+community-page/community-page.component.html +++ b/src/app/+community-page/community-page.component.html @@ -3,12 +3,11 @@ <div *ngIf="communityRD?.payload; let communityPayload"> <ds-view-tracker [object]="communityPayload"></ds-view-tracker> <header class="comcol-header border-bottom mb-4 pb-4"> + <!-- Community name --> + <ds-comcol-page-header [name]="communityPayload.name"></ds-comcol-page-header> <!-- Community logo --> <ds-comcol-page-logo *ngIf="logoRD$" [logo]="(logoRD$ | async)?.payload" [alternateText]="'Community Logo'"> </ds-comcol-page-logo> - - <!-- Community name --> - <ds-comcol-page-header [name]="communityPayload.name"></ds-comcol-page-header> <!-- Handle --> <ds-comcol-page-handle [content]="communityPayload.handle" [title]="'community.page.handle'"> </ds-comcol-page-handle> diff --git a/src/app/+community-page/community-page.module.ts b/src/app/+community-page/community-page.module.ts index 8b02471fc2f709fd5459f8a68ffc902957277f89..1228783c3bdf00250838f3a7e9776f00be9dcd05 100644 --- a/src/app/+community-page/community-page.module.ts +++ b/src/app/+community-page/community-page.module.ts @@ -9,7 +9,6 @@ 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'; import { StatisticsModule } from '../statistics/statistics.module'; @@ -25,9 +24,11 @@ import { StatisticsModule } from '../statistics/statistics.module'; CommunityPageSubCollectionListComponent, CommunityPageSubCommunityListComponent, CreateCommunityPageComponent, - EditCommunityPageComponent, DeleteCommunityPageComponent, CommunityFormComponent + ], + exports: [ + CommunityFormComponent ] }) diff --git a/src/app/+community-page/create-community-page/create-community-page.component.html b/src/app/+community-page/create-community-page/create-community-page.component.html index 55a080d2a15ab8891c077b41faeef6242805e24a..4f75771f6df97c43b1a4715de214edb9a901491b 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.html +++ b/src/app/+community-page/create-community-page/create-community-page.component.html @@ -7,5 +7,5 @@ </ng-container> </div> </div> - <ds-community-form (submitForm)="onSubmit($event)"></ds-community-form> + <ds-community-form (submitForm)="onSubmit($event)" (finish)="navigateToNewPage()"></ds-community-form> </div> diff --git a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts index dead5a5c3baeb2ff97cc9977c1672d2421e74097..d0de8ec71c611102e2bb1d5af400367cb1b8e313 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts @@ -10,6 +10,8 @@ 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'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub'; describe('CreateCommunityPageComponent', () => { let comp: CreateCommunityPageComponent; @@ -23,6 +25,7 @@ describe('CreateCommunityPageComponent', () => { { provide: CommunityDataService, useValue: { findById: () => observableOf({}) } }, { provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } }, { provide: Router, useValue: {} }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); 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 fd5f18442a957db4cf1b2946c95c8ed1193b13fb..30a2acbb0d8b19e14e7add4d0bb4ea6c9f329f2a 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -4,6 +4,8 @@ import { CommunityDataService } from '../../core/data/community-data.service'; import { RouteService } from '../../core/services/route.service'; import { Router } from '@angular/router'; import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-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 create a new Community @@ -15,12 +17,15 @@ import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comc }) export class CreateCommunityPageComponent extends CreateComColPageComponent<Community> { protected frontendURL = '/communities/'; + protected type = Community.type; public constructor( protected communityDataService: CommunityDataService, protected routeService: RouteService, - protected router: Router + protected router: Router, + protected notificationsService: NotificationsService, + protected translate: TranslateService ) { - super(communityDataService, communityDataService, routeService, router); + super(communityDataService, communityDataService, routeService, router, notificationsService, translate); } } diff --git a/src/app/+community-page/edit-community-page/community-curate/community-curate.component.html b/src/app/+community-page/edit-community-page/community-curate/community-curate.component.html new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/+community-page/edit-community-page/community-curate/community-curate.component.ts b/src/app/+community-page/edit-community-page/community-curate/community-curate.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..6151d3fe9ad7f66809db5008404ba8911451ddf0 --- /dev/null +++ b/src/app/+community-page/edit-community-page/community-curate/community-curate.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +/** + * Component for managing a community's curation tasks + */ +@Component({ + selector: 'ds-community-curate', + templateUrl: './community-curate.component.html', +}) +export class CommunityCurateComponent { + /* TODO: Implement Community Edit - Curate */ +} diff --git a/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.html b/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.html new file mode 100644 index 0000000000000000000000000000000000000000..6b441dbabdb96a7507fee247c7d69c52d8d99587 --- /dev/null +++ b/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.html @@ -0,0 +1,6 @@ +<ds-community-form (submitForm)="onSubmit($event)" + [dso]="(dsoRD$ | async)?.payload" + (finish)="navigateToHomePage()"></ds-community-form> +<a class="btn btn-danger" + [routerLink]="'/communities/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'community.edit.delete' + | translate}}</a> diff --git a/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts b/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..abeafb4e237374b628832f682fafcb7baff85009 --- /dev/null +++ b/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts @@ -0,0 +1,42 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { SharedModule } from '../../../shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CommunityMetadataComponent } from './community-metadata.component'; +import { CommunityDataService } from '../../../core/data/community-data.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; + +describe('CommunityMetadataComponent', () => { + let comp: CommunityMetadataComponent; + let fixture: ComponentFixture<CommunityMetadataComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + declarations: [CommunityMetadataComponent], + providers: [ + { provide: CommunityDataService, useValue: {} }, + { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: { payload: {} } }) } } }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CommunityMetadataComponent); + 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/community-metadata/community-metadata.component.ts b/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..c4bb88289fa990c139010e8646e4f41936f2a69e --- /dev/null +++ b/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { ComcolMetadataComponent } from '../../../shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Community } from '../../../core/shared/community.model'; +import { CommunityDataService } from '../../../core/data/community-data.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; + +/** + * Component for editing a community's metadata + */ +@Component({ + selector: 'ds-community-metadata', + templateUrl: './community-metadata.component.html', +}) +export class CommunityMetadataComponent extends ComcolMetadataComponent<Community> { + protected frontendURL = '/communities/'; + protected type = Community.type; + + public constructor( + protected communityDataService: CommunityDataService, + protected router: Router, + protected route: ActivatedRoute, + protected notificationsService: NotificationsService, + protected translate: TranslateService + ) { + super(communityDataService, router, route, notificationsService, translate); + } +} diff --git a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.html b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.html new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..afa1fe14d1aa322d7de89f4dd1da29d26563de79 --- /dev/null +++ b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +/** + * Component for managing a community's roles + */ +@Component({ + selector: 'ds-community-roles', + templateUrl: './community-roles.component.html', +}) +export class CommunityRolesComponent { + /* TODO: Implement Community Edit - Roles */ +} 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 deleted file mode 100644 index cedb771c14f8aea6a29de214c41b33ab9a607851..0000000000000000000000000000000000000000 --- a/src/app/+community-page/edit-community-page/edit-community-page.component.html +++ /dev/null @@ -1,12 +0,0 @@ -<div class="container"> - <div class="row"> - <div class="col-12 pb-4"> - <h2 id="header" class="border-bottom pb-2">{{ 'community.edit.head' | translate }}</h2> - <ds-community-form (submitForm)="onSubmit($event)" - [dso]="(dsoRD$ | async)?.payload"></ds-community-form> - <a class="btn btn-danger" - [routerLink]="'/communities/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'community.edit.delete' - | translate}}</a> - </div> - </div> -</div> 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 deleted file mode 100644 index 8b137891791fe96927ad78e64b0aad7bded08bdc..0000000000000000000000000000000000000000 --- a/src/app/+community-page/edit-community-page/edit-community-page.component.scss +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts b/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts index 54f2133ce7854c950a6336a71395a3f2515b5021..b61924dd00813354cf53fab22e55754d71ef8f16 100644 --- a/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts @@ -13,13 +13,29 @@ describe('EditCommunityPageComponent', () => { let comp: EditCommunityPageComponent; let fixture: ComponentFixture<EditCommunityPageComponent>; + const routeStub = { + data: observableOf({ + dso: { payload: {} } + }), + routeConfig: { + children: [] + }, + snapshot: { + firstChild: { + routeConfig: { + path: 'mockUrl' + } + } + } + }; + beforeEach(async(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], declarations: [EditCommunityPageComponent], providers: [ { provide: CommunityDataService, useValue: {} }, - { provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } }, + { provide: ActivatedRoute, useValue: routeStub }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -31,9 +47,9 @@ describe('EditCommunityPageComponent', () => { fixture.detectChanges(); }); - describe('frontendURL', () => { - it('should have the right frontendURL set', () => { - expect((comp as any).frontendURL).toEqual('/communities/'); + describe('type', () => { + it('should have the right type set', () => { + expect((comp as any).type).toEqual('community'); }) }); }); 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 9f49ac49dd1f8d4c00c5c5968e25351e4fba8a13..c0adfe0ff1f62d9a02145326a1df4299b812ac6c 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,25 +1,31 @@ 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 { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component'; +import { getCommunityPageRoute } from '../community-page-routing.module'; /** * 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' + templateUrl: '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html' }) export class EditCommunityPageComponent extends EditComColPageComponent<Community> { - protected frontendURL = '/communities/'; + type = 'community'; public constructor( - protected communityDataService: CommunityDataService, protected router: Router, protected route: ActivatedRoute ) { - super(communityDataService, router, route); + super(router, route); + } + + /** + * Get the community page url + * @param community The community for which the url is requested + */ + getPageUrl(community: Community): string { + return getCommunityPageRoute(community.id) } } diff --git a/src/app/+community-page/edit-community-page/edit-community-page.module.ts b/src/app/+community-page/edit-community-page/edit-community-page.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..f9a1e11a1432d5e89b6fa0fd7ac6a829b331a9bc --- /dev/null +++ b/src/app/+community-page/edit-community-page/edit-community-page.module.ts @@ -0,0 +1,30 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../../shared/shared.module'; +import { EditCommunityPageRoutingModule } from './edit-community-page.routing.module'; +import { CommunityPageModule } from '../community-page.module'; +import { EditCommunityPageComponent } from './edit-community-page.component'; +import { CommunityCurateComponent } from './community-curate/community-curate.component'; +import { CommunityMetadataComponent } from './community-metadata/community-metadata.component'; +import { CommunityRolesComponent } from './community-roles/community-roles.component'; + +/** + * Module that contains all components related to the Edit Community page administrator functionality + */ +@NgModule({ + imports: [ + CommonModule, + SharedModule, + EditCommunityPageRoutingModule, + CommunityPageModule + ], + declarations: [ + EditCommunityPageComponent, + CommunityCurateComponent, + CommunityMetadataComponent, + CommunityRolesComponent + ] +}) +export class EditCommunityPageModule { + +} diff --git a/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts b/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..1182db2de1e362989a44bfd34281e6c1af4c9cc9 --- /dev/null +++ b/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts @@ -0,0 +1,55 @@ +import { CommunityPageResolver } from '../community-page.resolver'; +import { EditCommunityPageComponent } from './edit-community-page.component'; +import { RouterModule } from '@angular/router'; +import { NgModule } from '@angular/core'; +import { CommunityMetadataComponent } from './community-metadata/community-metadata.component'; +import { CommunityRolesComponent } from './community-roles/community-roles.component'; +import { CommunityCurateComponent } from './community-curate/community-curate.component'; + +/** + * Routing module that handles the routing for the Edit Community page administrator functionality + */ +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: '', + component: EditCommunityPageComponent, + resolve: { + dso: CommunityPageResolver + }, + children: [ + { + path: '', + redirectTo: 'metadata', + pathMatch: 'full' + }, + { + path: 'metadata', + component: CommunityMetadataComponent, + data: { + title: 'community.edit.tabs.metadata.title', + hideReturnButton: true + } + }, + { + path: 'roles', + component: CommunityRolesComponent, + data: { title: 'community.edit.tabs.roles.title' } + }, + { + path: 'curate', + component: CommunityCurateComponent, + data: { title: 'community.edit.tabs.curate.title' } + } + ] + } + ]) + ], + providers: [ + CommunityPageResolver, + ] +}) +export class EditCommunityPageRoutingModule { + +} diff --git a/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts b/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts index 1d5564a29564ad29cd59e64060a134413ead47b6..81d66bb5f706939908be5207d202e1870535a998 100644 --- a/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts +++ b/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts @@ -33,12 +33,7 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit { /** * The UploaderOptions object */ - public uploadFilesOptions: UploaderOptions = { - url: '', - authToken: null, - disableMultipart: false, - itemAlias: null - }; + public uploadFilesOptions: UploaderOptions = new UploaderOptions(); /** * Subscription to unsubscribe from diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 867ee24fc102f584dc2af65a3f70e7a6ef4a0acd..2ce0362a4e85e69c09b0f38e84e285fdddcc7c61 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -1,32 +1,41 @@ import { distinctUntilChanged, - filter, first, - map, - mergeMap, - share, - switchMap, + filter, first,map, mergeMap, share, switchMap, take, tap } from 'rxjs/operators'; -import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs'; +import { merge as observableMerge, Observable, throwError as observableThrowError, combineLatest as observableCombineLatest } from 'rxjs'; import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { NormalizedCommunity } from '../cache/models/normalized-community.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { CommunityDataService } from './community-data.service'; import { DataService } from './data.service'; +import { DeleteRequest, FindListOptions, FindByIDRequest, RestRequest } from './request.models'; import { PaginatedList } from './paginated-list'; import { RemoteData } from './remote-data'; -import { FindListOptions, FindByIDRequest } from './request.models'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { getResponseFromEntry } from '../shared/operators'; +import { + configureRequest, + getRemoteDataPayload, + getResponseFromEntry, + getSucceededRemoteData +} from '../shared/operators'; import { CacheableObject } from '../cache/object-cache.reducer'; +import { RestResponse } from '../cache/response.models'; +import { Bitstream } from '../shared/bitstream.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; export abstract class ComColDataService<T extends CacheableObject> extends DataService<T> { protected abstract cds: CommunityDataService; protected abstract objectCache: ObjectCacheService; protected abstract halService: HALEndpointService; + /** + * Linkpath of endpoint to delete the logo + */ + protected logoDeleteLinkpath = 'bitstreams'; + /** * Get the scoped endpoint URL by fetching the object with * the given scopeID and returning its HAL link with this @@ -76,4 +85,33 @@ export abstract class ComColDataService<T extends CacheableObject> extends DataS return this.findList(href$, options); } + /** + * Get the endpoint for the community or collection's logo + * @param id The community or collection's ID + */ + public getLogoEndpoint(id: string): Observable<string> { + return this.halService.getEndpoint(this.linkPath).pipe( + switchMap((href: string) => this.halService.getEndpoint('logo', `${href}/${id}`)) + ) + } + + /** + * Delete the logo from the community or collection + * @param dso The object to delete the logo from + */ + public deleteLogo(dso: DSpaceObject): Observable<RestResponse> { + const logo$ = (dso as any).logo; + if (hasValue(logo$)) { + return observableCombineLatest( + logo$.pipe(getSucceededRemoteData(), getRemoteDataPayload(), take(1)), + this.halService.getEndpoint(this.logoDeleteLinkpath) + ).pipe( + map(([logo, href]: [Bitstream, string]) => `${href}/${logo.id}`), + map((href: string) => new DeleteRequest(this.requestService.generateRequestId(), href)), + configureRequest(this.requestService), + switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)), + getResponseFromEntry() + ); + } + } } 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 index 6c6793706317b804ecb54f14daa1c264c7330f1f..09f7e459e47e533846e10c7a660d3efe9bf9d7c2 100644 --- a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.html +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.html @@ -1,3 +1,38 @@ +<div class="container-fluid"> + <div class="row"> + <div class="col-12 d-inline-block"> + <label>{{type.value + '.edit.logo.label' | translate}}</label> + </div> + <ng-container *ngVar="(dso?.logo | async)?.payload as logo"> + <div class="col-12 d-inline-block alert" [ngClass]="{'alert-danger': markLogoForDeletion}" id="logo-section"> + <div class="row"> + <div class="col-8 d-inline-block"> + <ds-comcol-page-logo [logo]="logo"></ds-comcol-page-logo> + </div> + <div class="col-4 d-inline-block"> + <div *ngIf="logo" class="btn-group btn-group-sm float-right" role="group"> + <button *ngIf="!markLogoForDeletion" type="button" class="btn btn-danger" (click)="deleteLogo()"> + <i class="fas fa-trash" aria-hidden="true"></i> + </button> + <button *ngIf="markLogoForDeletion" type="button" class="btn btn-warning" (click)="undoDeleteLogo()"> + <i class="fas fa-undo" aria-hidden="true"></i> + </button> + </div> + </div> + </div> + </div> + <div *ngIf="!logo" class="col-12 d-inline-block"> + <ds-uploader *ngIf="initializedUploaderOptions | async" + [dropMsg]="type.value + '.edit.logo.upload'" + [dropOverDocumentMsg]="type.value + '.edit.logo.upload'" + [enableDragOverDocument]="true" + [uploadFilesOptions]="uploadFilesOptions" + (onCompleteItem)="onCompleteItem()" + (onUploadError)="onUploadError()"></ds-uploader> + </div> + </ng-container> + </div> +</div> <ds-form *ngIf="formModel" [formId]="'comcol-form-id'" [formModel]="formModel" (submitForm)="onSubmit()" (cancel)="onCancel()"></ds-form> 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 index 27d50d6f9bb7cda1c039d13fbfdac1c2a1ff448a..3ed55a1a38ecc664a4808b79f7f988a0228f785b 100644 --- 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 @@ -9,6 +9,19 @@ import { Community } from '../../../core/shared/community.model'; import { ComColFormComponent } from './comcol-form.component'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { hasValue } from '../../empty.util'; +import { VarDirective } from '../../utils/var.directive'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { NotificationsServiceStub } from '../../testing/notifications-service-stub'; +import { AuthService } from '../../../core/auth/auth.service'; +import { AuthServiceMock } from '../../mocks/mock-auth.service'; +import { of as observableOf } from 'rxjs'; +import { RemoteData } from '../../../core/data/remote-data'; +import { RestRequestMethod } from '../../../core/data/rest-request-method'; +import { ErrorResponse, RestResponse } from '../../../core/cache/response.models'; +import { RequestError } from '../../../core/data/request.models'; +import { RequestService } from '../../../core/data/request.service'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; +import { By } from '@angular/platform-browser'; describe('ComColFormComponent', () => { let comp: ComColFormComponent<DSpaceObject>; @@ -47,71 +60,264 @@ describe('ComColFormComponent', () => { }) ]; + const logoEndpoint = 'rest/api/logo/endpoint'; + const dsoService = Object.assign({ + getLogoEndpoint: () => observableOf(logoEndpoint), + deleteLogo: () => observableOf({}) + }); + const notificationsService = new NotificationsServiceStub(); + /* tslint:disable:no-empty */ const locationStub = jasmine.createSpyObj('location', ['back']); /* tslint:enable:no-empty */ + const requestServiceStub = jasmine.createSpyObj({ + removeByHrefSubstring: {} + }); + const objectCacheStub = jasmine.createSpyObj({ + remove: {} + }); + beforeEach(async(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), RouterTestingModule], - declarations: [ComColFormComponent], + declarations: [ComColFormComponent, VarDirective], providers: [ { provide: Location, useValue: locationStub }, - { provide: DynamicFormService, useValue: formServiceStub } + { provide: DynamicFormService, useValue: formServiceStub }, + { provide: NotificationsService, useValue: notificationsService }, + { provide: AuthService, useValue: new AuthServiceMock() }, + { provide: RequestService, useValue: requestServiceStub }, + { provide: ObjectCacheService, useValue: objectCacheStub } ], 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', () => { + describe('when the dso doesn\'t contain an ID (newly created)', () => { beforeEach(() => { - spyOn(comp.submitForm, 'emit'); - comp.formModel = formModel; + initComponent(new Community()); }); - it('should emit the new version of the community', () => { - comp.dso = Object.assign( - new Community(), - { - metadata: { - ...titleMD, - ...randomMD - } - } - ); + it('should initialize the uploadFilesOptions with a placeholder url', () => { + expect(comp.uploadFilesOptions.url.length).toBeGreaterThan(0); + }); - comp.onSubmit(); + describe('onSubmit', () => { + beforeEach(() => { + spyOn(comp.submitForm, 'emit'); + comp.formModel = formModel; + }); - expect(comp.submitForm.emit).toHaveBeenCalledWith( - Object.assign( - {}, + it('should emit the new version of the community', () => { + comp.dso = Object.assign( new Community(), { metadata: { - ...newTitleMD, - ...randomMD, - ...abstractMD - }, - type: Community.type - }, - ) - ); - }) - }); + ...titleMD, + ...randomMD + } + } + ); + + comp.onSubmit(); - describe('onCancel', () => { - it('should call the back method on the Location service', () => { + expect(comp.submitForm.emit).toHaveBeenCalledWith( + { + dso: Object.assign( + {}, + new Community(), + { + metadata: { + ...newTitleMD, + ...randomMD, + ...abstractMD + }, + type: Community.type + }, + ), + uploader: {}, + deleteLogo: false + } + ); + }) + }); + + describe('onCancel', () => { + it('should call the back method on the Location service', () => { comp.onCancel(); expect(locationStub.back).toHaveBeenCalled(); + }); + }); + + describe('onCompleteItem', () => { + beforeEach(() => { + spyOn(comp.finish, 'emit'); + comp.onCompleteItem(); + }); + + it('should show a success notification', () => { + expect(notificationsService.success).toHaveBeenCalled(); + }); + + it('should emit finish', () => { + expect(comp.finish.emit).toHaveBeenCalled(); + }); + + it('should remove the object\'s cache', () => { + expect(requestServiceStub.removeByHrefSubstring).toHaveBeenCalled(); + expect(objectCacheStub.remove).toHaveBeenCalled(); + }); + }); + + describe('onUploadError', () => { + beforeEach(() => { + spyOn(comp.finish, 'emit'); + comp.onUploadError(); + }); + + it('should show an error notification', () => { + expect(notificationsService.error).toHaveBeenCalled(); + }); + + it('should emit finish', () => { + expect(comp.finish.emit).toHaveBeenCalled(); + }); }); }); + + describe('when the dso contains an ID (being edited)', () => { + describe('and the dso doesn\'t contain a logo', () => { + beforeEach(() => { + initComponent(Object.assign(new Community(), { + id: 'community-id', + logo: observableOf(new RemoteData(false, false, true, null, undefined)) + })); + }); + + it('should initialize the uploadFilesOptions with the logo\'s endpoint url', () => { + expect(comp.uploadFilesOptions.url).toEqual(logoEndpoint); + }); + + it('should initialize the uploadFilesOptions with a POST method', () => { + expect(comp.uploadFilesOptions.method).toEqual(RestRequestMethod.POST); + }); + }); + + describe('and the dso contains a logo', () => { + beforeEach(() => { + initComponent(Object.assign(new Community(), { + id: 'community-id', + logo: observableOf(new RemoteData(false, false, true, null, {})) + })); + }); + + it('should initialize the uploadFilesOptions with the logo\'s endpoint url', () => { + expect(comp.uploadFilesOptions.url).toEqual(logoEndpoint); + }); + + it('should initialize the uploadFilesOptions with a PUT method', () => { + expect(comp.uploadFilesOptions.method).toEqual(RestRequestMethod.PUT); + }); + + describe('submit with logo marked for deletion', () => { + beforeEach(() => { + comp.markLogoForDeletion = true; + }); + + describe('when dsoService.deleteLogo returns a successful response', () => { + const response = new RestResponse(true, 200, 'OK'); + + beforeEach(() => { + spyOn(dsoService, 'deleteLogo').and.returnValue(observableOf(response)); + comp.onSubmit(); + }); + + it('should display a success notification', () => { + expect(notificationsService.success).toHaveBeenCalled(); + }); + }); + + describe('when dsoService.deleteLogo returns an error response', () => { + const response = new ErrorResponse(new RequestError('errorMessage')); + + beforeEach(() => { + spyOn(dsoService, 'deleteLogo').and.returnValue(observableOf(response)); + comp.onSubmit(); + }); + + it('should display an error notification', () => { + expect(notificationsService.error).toHaveBeenCalled(); + }); + }); + }); + + describe('deleteLogo', () => { + beforeEach(() => { + comp.deleteLogo(); + fixture.detectChanges(); + }); + + it('should set markLogoForDeletion to true', () => { + expect(comp.markLogoForDeletion).toEqual(true); + }); + + it('should mark the logo section with a danger alert', () => { + const logoSection = fixture.debugElement.query(By.css('#logo-section.alert-danger')); + expect(logoSection).toBeTruthy(); + }); + + it('should hide the delete button', () => { + const button = fixture.debugElement.query(By.css('#logo-section .btn-danger')); + expect(button).not.toBeTruthy(); + }); + + it('should show the undo button', () => { + const button = fixture.debugElement.query(By.css('#logo-section .btn-warning')); + expect(button).toBeTruthy(); + }); + }); + + describe('undoDeleteLogo', () => { + beforeEach(() => { + comp.markLogoForDeletion = true; + comp.undoDeleteLogo(); + fixture.detectChanges(); + }); + + it('should set markLogoForDeletion to false', () => { + expect(comp.markLogoForDeletion).toEqual(false); + }); + + it('should disable the danger alert on the logo section', () => { + const logoSection = fixture.debugElement.query(By.css('#logo-section.alert-danger')); + expect(logoSection).not.toBeTruthy(); + }); + + it('should show the delete button', () => { + const button = fixture.debugElement.query(By.css('#logo-section .btn-danger')); + expect(button).toBeTruthy(); + }); + + it('should hide the undo button', () => { + const button = fixture.debugElement.query(By.css('#logo-section .btn-warning')); + expect(button).not.toBeTruthy(); + }); + }); + }); + }); + + function initComponent(dso: Community) { + fixture = TestBed.createComponent(ComColFormComponent); + comp = fixture.componentInstance; + comp.formModel = []; + comp.dso = dso; + (comp as any).type = Community.type; + comp.uploaderComponent = Object.assign({ + uploader: {} + }); + (comp as any).dsoService = dsoService; + fixture.detectChanges(); + location = (comp as any).location; + } }); diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts index dc8b924142f4ecb76cc79f167869232f6aa474ac..435ef61d723a22dcaa595c5af47159f6fae4d376 100644 --- a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; import { Location } from '@angular/common'; import { DynamicFormControlModel, @@ -11,8 +11,24 @@ import { TranslateService } from '@ngx-translate/core'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { MetadataMap, MetadataValue } from '../../../core/shared/metadata.models'; import { ResourceType } from '../../../core/shared/resource-type'; -import { isNotEmpty } from '../../empty.util'; +import { hasValue, isNotEmpty } from '../../empty.util'; +import { UploaderOptions } from '../../uploader/uploader-options.model'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { ComColDataService } from '../../../core/data/comcol-data.service'; +import { Subscription } from 'rxjs/internal/Subscription'; +import { AuthService } from '../../../core/auth/auth.service'; import { Community } from '../../../core/shared/community.model'; +import { Collection } from '../../../core/shared/collection.model'; +import { UploaderComponent } from '../../uploader/uploader.component'; +import { FileUploader } from 'ng2-file-upload'; +import { ErrorResponse, RestResponse } from '../../../core/cache/response.models'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Bitstream } from '../../../core/shared/bitstream.model'; +import { combineLatest as observableCombineLatest } from 'rxjs'; +import { RestRequestMethod } from '../../../core/data/rest-request-method'; +import { RequestService } from '../../../core/data/request.service'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; /** * A form for creating and editing Communities or Collections @@ -22,7 +38,13 @@ import { Community } from '../../../core/shared/community.model'; styleUrls: ['./comcol-form.component.scss'], templateUrl: './comcol-form.component.html' }) -export class ComColFormComponent<T extends DSpaceObject> implements OnInit { +export class ComColFormComponent<T extends DSpaceObject> implements OnInit, OnDestroy { + + /** + * The logo uploader component + */ + @ViewChild(UploaderComponent) uploaderComponent: UploaderComponent; + /** * DSpaceObject that the form represents */ @@ -31,7 +53,7 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit { /** * Type of DSpaceObject that the form represents */ - protected type: ResourceType; + type: ResourceType; /** * @type {string} Key prefix used to generate form labels @@ -54,14 +76,56 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit { formGroup: FormGroup; /** - * Emits DSO when the form is submitted - * @type {EventEmitter<any>} + * The uploader configuration options + * @type {UploaderOptions} + */ + uploadFilesOptions: UploaderOptions = Object.assign(new UploaderOptions(), { + autoUpload: false + }); + + /** + * Emits DSO and Uploader when the form is submitted */ - @Output() submitForm: EventEmitter<any> = new EventEmitter(); + @Output() submitForm: EventEmitter<{ + dso: T, + uploader: FileUploader, + deleteLogo: boolean + }> = new EventEmitter(); - public constructor(private location: Location, - private formService: DynamicFormService, - private translate: TranslateService) { + /** + * Fires an event when the logo has finished uploading (with or without errors) or was removed + */ + @Output() finish: EventEmitter<any> = new EventEmitter(); + + /** + * Observable keeping track whether or not the uploader has finished initializing + * Used to start rendering the uploader component + */ + initializedUploaderOptions = new BehaviorSubject(false); + + /** + * Is the logo marked to be deleted? + */ + markLogoForDeletion = false; + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + protected subs: Subscription[] = []; + + /** + * The service used to fetch from or send data to + */ + protected dsoService: ComColDataService<Community | Collection>; + + public constructor(protected location: Location, + protected formService: DynamicFormService, + protected translate: TranslateService, + protected notificationsService: NotificationsService, + protected authService: AuthService, + protected requestService: RequestService, + protected objectCache: ObjectCacheService) { } ngOnInit(): void { @@ -77,13 +141,56 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit { .subscribe(() => { this.updateFieldTranslations(); }); + + if (hasValue(this.dso.id)) { + this.subs.push( + observableCombineLatest( + this.dsoService.getLogoEndpoint(this.dso.id), + (this.dso as any).logo + ).subscribe(([href, logoRD]: [string, RemoteData<Bitstream>]) => { + this.uploadFilesOptions.url = href; + this.uploadFilesOptions.authToken = this.authService.buildAuthHeader(); + // If the object already contains a logo, send out a PUT request instead of POST for setting a new logo + if (hasValue(logoRD.payload)) { + this.uploadFilesOptions.method = RestRequestMethod.PUT; + } + this.initializedUploaderOptions.next(true); + }) + ); + } else { + // Set a placeholder URL to not break the uploader component. This will be replaced once the object is created. + this.uploadFilesOptions.url = 'placeholder'; + this.uploadFilesOptions.authToken = this.authService.buildAuthHeader(); + this.initializedUploaderOptions.next(true); + } } /** * Checks which new fields were added and sends the updated version of the DSO to the parent component */ onSubmit() { - const formMetadata = {} as MetadataMap; + if (this.markLogoForDeletion && hasValue(this.dso.id)) { + this.dsoService.deleteLogo(this.dso).subscribe((response: RestResponse) => { + if (response.isSuccessful) { + this.notificationsService.success( + this.translate.get(this.type.value + '.edit.logo.notifications.delete.success.title'), + this.translate.get(this.type.value + '.edit.logo.notifications.delete.success.content') + ); + } else { + const errorResponse = response as ErrorResponse; + this.notificationsService.error( + this.translate.get(this.type.value + '.edit.logo.notifications.delete.error.title'), + errorResponse.errorMessage + ); + } + (this.dso as any).logo = undefined; + this.uploadFilesOptions.method = RestRequestMethod.POST; + this.refreshCache(); + this.finish.emit(); + }); + } + + const formMetadata = {} as MetadataMap; this.formModel.forEach((fieldModel: DynamicInputModel) => { const value: MetadataValue = { value: fieldModel.value as string, @@ -103,7 +210,11 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit { }, type: Community.type }); - this.submitForm.emit(updatedDSO); + this.submitForm.emit({ + dso: updatedDSO, + uploader: hasValue(this.uploaderComponent) ? this.uploaderComponent.uploader : undefined, + deleteLogo: this.markLogoForDeletion + }); } /** @@ -123,7 +234,59 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit { ); } + /** + * Mark the logo to be deleted + * Send out a delete request to remove the logo from the community/collection and display notifications + */ + deleteLogo() { + this.markLogoForDeletion = true; + } + + /** + * Undo marking the logo to be deleted + */ + undoDeleteLogo() { + this.markLogoForDeletion = false; + } + + /** + * Refresh the object's cache to ensure the latest version + */ + private refreshCache() { + this.requestService.removeByHrefSubstring(this.dso.self); + this.objectCache.remove(this.dso.self); + } + + /** + * The request was successful, display a success notification + */ + public onCompleteItem() { + this.refreshCache(); + this.notificationsService.success(null, this.translate.get(this.type.value + '.edit.logo.notifications.add.success')); + this.finish.emit(); + } + + /** + * The request was unsuccessful, display an error notification + */ + public onUploadError() { + this.notificationsService.error(null, this.translate.get(this.type.value + '.edit.logo.notifications.add.error')); + this.finish.emit(); + } + + /** + * Cancel the form and return to the previous page + */ onCancel() { this.location.back(); } + + /** + * Unsubscribe from open subscriptions + */ + ngOnDestroy(): void { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + } } 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 index 6ad2e5b5e1ba7a6268b7a868ddc4509d7acb92c2..717979891fd6b166df93206646f0dc6d6a387ca6 100644 --- 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 @@ -11,11 +11,13 @@ import { RouterTestingModule } from '@angular/router/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { CreateComColPageComponent } from './create-comcol-page.component'; -import { DataService } from '../../../core/data/data.service'; import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../testing/utils'; +import { ComColDataService } from '../../../core/data/comcol-data.service'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { NotificationsServiceStub } from '../../testing/notifications-service-stub'; describe('CreateComColPageComponent', () => { let comp: CreateComColPageComponent<DSpaceObject>; @@ -31,6 +33,8 @@ describe('CreateComColPageComponent', () => { let routeServiceStub; let routerStub; + const logoEndpoint = 'rest/api/logo/endpoint'; + function initializeVars() { community = Object.assign(new Community(), { uuid: 'a20da287-e174-466a-9926-f66b9300d347', @@ -56,8 +60,8 @@ describe('CreateComColPageComponent', () => { value: community.name }] })), - create: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity) - + create: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity), + getLogoEndpoint: () => observableOf(logoEndpoint) }; routeServiceStub = { @@ -74,10 +78,11 @@ describe('CreateComColPageComponent', () => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], providers: [ - { provide: DataService, useValue: communityDataServiceStub }, + { provide: ComColDataService, useValue: communityDataServiceStub }, { provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: RouteService, useValue: routeServiceStub }, { provide: Router, useValue: routerStub }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -86,6 +91,7 @@ describe('CreateComColPageComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(CreateComColPageComponent); comp = fixture.componentInstance; + (comp as any).type = Community.type; fixture.detectChanges(); dsoDataService = (comp as any).dsoDataService; communityDataService = (comp as any).communityDataService; @@ -95,27 +101,86 @@ describe('CreateComColPageComponent', () => { describe('onSubmit', () => { let data; - beforeEach(() => { - data = Object.assign(new Community(), { - metadata: [{ - key: 'dc.title', - value: 'test' - }] + + describe('with an empty queue in the uploader', () => { + beforeEach(() => { + data = { + dso: Object.assign(new Community(), { + metadata: [{ + key: 'dc.title', + value: 'test' + }] + }), + uploader: { + options: { + url: '' + }, + queue: [], + /* tslint:disable:no-empty */ + uploadAll: () => {} + /* tslint:enable:no-empty */ + } + }; + }); + + 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(createFailedRemoteDataObject$(newCommunity)); + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigate).not.toHaveBeenCalled(); }); - }); - 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(createFailedRemoteDataObject$(newCommunity)); - comp.onSubmit(data); - fixture.detectChanges(); - expect(router.navigate).not.toHaveBeenCalled(); + describe('with at least one item in the uploader\'s queue', () => { + beforeEach(() => { + data = { + dso: Object.assign(new Community(), { + metadata: [{ + key: 'dc.title', + value: 'test' + }] + }), + uploader: { + options: { + url: '' + }, + queue: [ + {} + ], + /* tslint:disable:no-empty */ + uploadAll: () => {} + /* tslint:enable:no-empty */ + } + }; + }); + + it('should not navigate', () => { + spyOn(router, 'navigate'); + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('should set the uploader\'s url to the logo\'s endpoint', () => { + comp.onSubmit(data); + fixture.detectChanges(); + expect(data.uploader.options.url).toEqual(logoEndpoint); + }); + + it('should call the uploader\'s uploadAll', () => { + spyOn(data.uploader, 'uploadAll'); + comp.onSubmit(data); + fixture.detectChanges(); + expect(data.uploader.uploadAll).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 index e07f2a5a0ab3cd13a3d6987a9cdd902ff2813e28..7b23c5949872649694d13bf4452d2e0ef70a90d0 100644 --- a/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/create-comcol-page/create-comcol-page.component.ts @@ -3,13 +3,17 @@ import { Community } from '../../../core/shared/community.model'; import { CommunityDataService } from '../../../core/data/community-data.service'; import { Observable } from 'rxjs'; import { RouteService } from '../../../core/services/route.service'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { RemoteData } from '../../../core/data/remote-data'; -import { isNotEmpty, isNotUndefined } from '../../empty.util'; +import { hasValue, 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 { ComColDataService } from '../../../core/data/comcol-data.service'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { ResourceType } from '../../../core/shared/resource-type'; /** * Component representing the create page for communities and collections @@ -34,11 +38,23 @@ export class CreateComColPageComponent<TDomain extends DSpaceObject> implements */ public parentRD$: Observable<RemoteData<Community>>; + /** + * The UUID of the newly created object + */ + private newUUID: string; + + /** + * The type of the dso + */ + protected type: ResourceType; + public constructor( - protected dsoDataService: DataService<TDomain>, + protected dsoDataService: ComColDataService<TDomain>, protected parentDataService: CommunityDataService, protected routeService: RouteService, - protected router: Router + protected router: Router, + protected notificationsService: NotificationsService, + protected translate: TranslateService ) { } @@ -53,20 +69,40 @@ export class CreateComColPageComponent<TDomain extends DSpaceObject> implements } /** - * @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 + * @param event The event returned by the community/collection form. Contains the new dso and logo uploader */ - onSubmit(dso: TDomain) { + onSubmit(event) { + const dso = event.dso; + const uploader = event.uploader; + this.parentUUID$.pipe(take(1)).subscribe((uuid: string) => { this.dsoDataService.create(dso, uuid) .pipe(getSucceededRemoteData()) .subscribe((dsoRD: RemoteData<TDomain>) => { if (isNotUndefined(dsoRD)) { - const newUUID = dsoRD.payload.uuid; - this.router.navigate([this.frontendURL + newUUID]); + this.newUUID = dsoRD.payload.uuid; + if (uploader.queue.length > 0) { + this.dsoDataService.getLogoEndpoint(this.newUUID).pipe(take(1)).subscribe((href: string) => { + uploader.options.url = href; + uploader.uploadAll(); + }); + } else { + this.navigateToNewPage(); + } + this.notificationsService.success(null, this.translate.get(this.type.value + '.create.notifications.success')); } }); }); } + /** + * Navigate to the page of the newly created object + */ + navigateToNewPage() { + if (hasValue(this.newUUID)) { + this.router.navigate([this.frontendURL + this.newUUID]); + } + } + } diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..5711aa4e701433c6656479a3e6306e906cc471f3 --- /dev/null +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts @@ -0,0 +1,189 @@ +import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { CommunityDataService } from '../../../../core/data/community-data.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Community } from '../../../../core/shared/community.model'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { TranslateModule } from '@ngx-translate/core'; +import { SharedModule } from '../../../shared.module'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { DataService } from '../../../../core/data/data.service'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComcolMetadataComponent } from './comcol-metadata.component'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../testing/utils'; +import { ComColDataService } from '../../../../core/data/comcol-data.service'; +import { NotificationsServiceStub } from '../../../testing/notifications-service-stub'; +import { NotificationsService } from '../../../notifications/notifications.service'; + +describe('ComColMetadataComponent', () => { + let comp: ComcolMetadataComponent<DSpaceObject>; + let fixture: ComponentFixture<ComcolMetadataComponent<DSpaceObject>>; + let dsoDataService: CommunityDataService; + let router: Router; + + let community; + let newCommunity; + let communityDataServiceStub; + let routerStub; + let routeStub; + + const logoEndpoint = 'rest/api/logo/endpoint'; + + 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?) => createSuccessfulRemoteDataObject$(newCommunity), + getLogoEndpoint: () => observableOf(logoEndpoint) + }; + + routerStub = { + navigate: (commands) => commands + }; + + routeStub = { + parent: { + data: observableOf({ + dso: new RemoteData(false, false, true, null, community) + }) + } + }; + + } + + beforeEach(async(() => { + initializeVars(); + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + providers: [ + { provide: ComColDataService, useValue: communityDataServiceStub }, + { provide: Router, useValue: routerStub }, + { provide: ActivatedRoute, useValue: routeStub }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ComcolMetadataComponent); + comp = fixture.componentInstance; + (comp as any).type = Community.type; + fixture.detectChanges(); + dsoDataService = (comp as any).dsoDataService; + router = (comp as any).router; + }); + + describe('onSubmit', () => { + let data; + + describe('with an empty queue in the uploader', () => { + beforeEach(() => { + data = { + dso: Object.assign(new Community(), { + metadata: [{ + key: 'dc.title', + value: 'test' + }] + }), + uploader: { + options: { + url: '' + }, + queue: [], + /* tslint:disable:no-empty */ + uploadAll: () => {} + /* tslint:enable:no-empty */ + } + } + }); + + 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(createFailedRemoteDataObject$(newCommunity)); + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigate).not.toHaveBeenCalled(); + }); + }); + + describe('with at least one item in the uploader\'s queue', () => { + beforeEach(() => { + data = { + dso: Object.assign(new Community(), { + metadata: [{ + key: 'dc.title', + value: 'test' + }] + }), + uploader: { + options: { + url: '' + }, + queue: [ + {} + ], + /* tslint:disable:no-empty */ + uploadAll: () => {} + /* tslint:enable:no-empty */ + } + } + }); + + it('should not navigate', () => { + spyOn(router, 'navigate'); + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('should set the uploader\'s url to the logo\'s endpoint', () => { + comp.onSubmit(data); + fixture.detectChanges(); + expect(data.uploader.options.url).toEqual(logoEndpoint); + }); + + it('should call the uploader\'s uploadAll', () => { + spyOn(data.uploader, 'uploadAll'); + comp.onSubmit(data); + fixture.detectChanges(); + expect(data.uploader.uploadAll).toHaveBeenCalled(); + }); + }); + }); + + describe('navigateToHomePage', () => { + beforeEach(() => { + spyOn(router, 'navigate'); + comp.navigateToHomePage(); + }); + + it('should navigate', () => { + expect(router.navigate).toHaveBeenCalled(); + }); + }); + +}); diff --git a/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.ts b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..1031fead10fc4f4c4ff04347cc677786c10d2a31 --- /dev/null +++ b/src/app/shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.ts @@ -0,0 +1,85 @@ +import { Component, OnInit } from '@angular/core'; +import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; +import { Observable } from 'rxjs/internal/Observable'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { ActivatedRoute, Router } from '@angular/router'; +import { first, map, take } from 'rxjs/operators'; +import { getSucceededRemoteData } from '../../../../core/shared/operators'; +import { hasValue, isNotUndefined } from '../../../empty.util'; +import { DataService } from '../../../../core/data/data.service'; +import { ResourceType } from '../../../../core/shared/resource-type'; +import { ComColDataService } from '../../../../core/data/comcol-data.service'; +import { NotificationsService } from '../../../notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'ds-comcol-metadata', + template: '' +}) +export class ComcolMetadataComponent<TDomain extends DSpaceObject> implements OnInit { + /** + * Frontend endpoint for this type of DSO + */ + protected frontendURL: string; + /** + * The initial DSO object + */ + public dsoRD$: Observable<RemoteData<TDomain>>; + + /** + * The type of the dso + */ + protected type: ResourceType; + + public constructor( + protected dsoDataService: ComColDataService<TDomain>, + protected router: Router, + protected route: ActivatedRoute, + protected notificationsService: NotificationsService, + protected translate: TranslateService + ) { + } + + ngOnInit(): void { + this.dsoRD$ = this.route.parent.data.pipe(first(), map((data) => data.dso)); + } + + /** + * Updates an existing DSO based on the submitted user data and navigates to the edited object's home page + * @param event The event returned by the community/collection form. Contains the new dso and logo uploader + */ + onSubmit(event) { + const dso = event.dso; + const uploader = event.uploader; + const deleteLogo = event.deleteLogo; + + this.dsoDataService.update(dso) + .pipe(getSucceededRemoteData()) + .subscribe((dsoRD: RemoteData<TDomain>) => { + if (isNotUndefined(dsoRD)) { + const newUUID = dsoRD.payload.uuid; + if (hasValue(uploader) && uploader.queue.length > 0) { + this.dsoDataService.getLogoEndpoint(newUUID).pipe(take(1)).subscribe((href: string) => { + uploader.options.url = href; + uploader.uploadAll(); + }); + } else if (!deleteLogo) { + this.router.navigate([this.frontendURL + newUUID]); + } + this.notificationsService.success(null, this.translate.get(this.type.value + '.edit.notifications.success')); + } + }); + } + + /** + * Navigate to the home page of the object + */ + navigateToHomePage() { + this.dsoRD$.pipe( + getSucceededRemoteData(), + take(1) + ).subscribe((dsoRD: RemoteData<TDomain>) => { + this.router.navigate([this.frontendURL + dsoRD.payload.id]); + }); + } +} diff --git a/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html new file mode 100644 index 0000000000000000000000000000000000000000..aa6290ea9fff3a524531f6fb9ae6a2d2d8cc0d64 --- /dev/null +++ b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html @@ -0,0 +1,24 @@ +<div class="container"> + <div class="row"> + <div class="col-12"> + <h2 class="border-bottom">{{ type + '.edit.head' | translate }}</h2> + <div class="pt-2"> + <ul class="nav nav-tabs justify-content-start mb-2"> + <li *ngFor="let page of pages" class="nav-item"> + <a class="nav-link" + [ngClass]="{'active' : page === currentPage}" + [routerLink]="['./' + page]"> + {{ type + '.edit.tabs.' + page + '.head' | translate}} + </a> + </li> + </ul> + <div class="tab-pane active"> + <div class="mb-4"> + <router-outlet></router-outlet> + </div> + <a *ngIf="!hideReturnButton" [routerLink]="getPageUrl((dsoRD$ | async)?.payload)" class="btn btn-outline-secondary">{{ type + '.edit.return' | translate }}</a> + </div> + </div> + </div> + </div> +</div> 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 index 03f751599f8e53b81b426435d6b31b0c67424e78..d1b87db7ae77b942a023168495de8abcb80e39b0 100644 --- 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 @@ -1,5 +1,4 @@ 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'; @@ -10,21 +9,13 @@ import { RouterTestingModule } from '@angular/router/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { EditComColPageComponent } from './edit-comcol-page.component'; -import { DataService } from '../../../core/data/data.service'; -import { - createFailedRemoteDataObject$, - createSuccessfulRemoteDataObject$ -} from '../../testing/utils'; describe('EditComColPageComponent', () => { let comp: EditComColPageComponent<DSpaceObject>; let fixture: ComponentFixture<EditComColPageComponent<DSpaceObject>>; - let dsoDataService: CommunityDataService; let router: Router; let community; - let newCommunity; - let communityDataServiceStub; let routerStub; let routeStub; @@ -37,25 +28,33 @@ describe('EditComColPageComponent', () => { }] }); - newCommunity = Object.assign(new Community(), { - uuid: '1ff59938-a69a-4e62-b9a4-718569c55d48', - metadata: [{ - key: 'dc.title', - value: 'new community' - }] - }); - - communityDataServiceStub = { - update: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity) - - }; - routerStub = { - navigate: (commands) => commands + navigate: (commands) => commands, + events: observableOf({}), + url: 'mockUrl' }; routeStub = { - data: observableOf(community) + data: observableOf({ + dso: community + }), + routeConfig: { + children: [ + { + path: 'mockUrl', + data: { + hideReturnButton: false + } + } + ] + }, + snapshot: { + firstChild: { + routeConfig: { + path: 'mockUrl' + } + } + } }; } @@ -65,7 +64,6 @@ describe('EditComColPageComponent', () => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], providers: [ - { provide: DataService, useValue: communityDataServiceStub }, { provide: Router, useValue: routerStub }, { provide: ActivatedRoute, useValue: routeStub }, ], @@ -77,33 +75,16 @@ describe('EditComColPageComponent', () => { fixture = TestBed.createComponent(EditComColPageComponent); comp = fixture.componentInstance; fixture.detectChanges(); - dsoDataService = (comp as any).dsoDataService; router = (comp as any).router; }); - describe('onSubmit', () => { - let data; + describe('getPageUrl', () => { + let url; beforeEach(() => { - data = Object.assign(new Community(), { - metadata: [{ - key: 'dc.title', - value: 'test' - }] - }); + url = comp.getPageUrl(community); }); - 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(createFailedRemoteDataObject$(newCommunity)); - comp.onSubmit(data); - fixture.detectChanges(); - expect(router.navigate).not.toHaveBeenCalled(); + it('should return the current url as a fallback', () => { + expect(url).toEqual(routerStub.url); }); }); }); diff --git a/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts index 24181b5e6175adee254d86b47e8cd2c85b317e81..0f9d4c55b471960198a97b662fdaf53f60d9496e 100644 --- a/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts +++ b/src/app/shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts @@ -2,7 +2,7 @@ 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 { isNotEmpty, isNotUndefined } from '../../empty.util'; import { first, map } from 'rxjs/operators'; import { getSucceededRemoteData } from '../../../core/shared/operators'; import { DataService } from '../../../core/data/data.service'; @@ -17,37 +17,54 @@ import { DSpaceObject } from '../../../core/shared/dspace-object.model'; }) export class EditComColPageComponent<TDomain extends DSpaceObject> implements OnInit { /** - * Frontend endpoint for this type of DSO + * The type of DSpaceObject (used to create i18n messages) */ - protected frontendURL: string; + public type: string; + + /** + * The current page outlet string + */ + public currentPage: string; + + /** + * All possible page outlet strings + */ + public pages: string[]; + /** - * The initial DSO object + * The DSO to render the edit page for */ public dsoRD$: Observable<RemoteData<TDomain>>; + /** + * Hide the default return button? + */ + public hideReturnButton: boolean; + public constructor( - protected dsoDataService: DataService<TDomain>, protected router: Router, protected route: ActivatedRoute ) { + this.router.events.subscribe(() => { + this.currentPage = this.route.snapshot.firstChild.routeConfig.path; + this.hideReturnButton = this.route.routeConfig.children + .find((child: any) => child.path === this.currentPage).data.hideReturnButton; + }); } ngOnInit(): void { + this.pages = this.route.routeConfig.children + .map((child: any) => child.path) + .filter((path: string) => isNotEmpty(path)); // ignore reroutes 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 + * Get the dso's page url + * This method is expected to be overridden in the edit community/collection page components + * @param dso The DSpaceObject for which the url is requested */ - onSubmit(dso: TDomain) { - this.dsoDataService.update(dso) - .pipe(getSucceededRemoteData()) - .subscribe((dsoRD: RemoteData<TDomain>) => { - if (isNotUndefined(dsoRD)) { - const newUUID = dsoRD.payload.uuid; - this.router.navigate([this.frontendURL + newUUID]); - } - }); + getPageUrl(dso: TDomain): string { + return this.router.url; } } diff --git a/src/app/shared/mocks/mock-auth.service.ts b/src/app/shared/mocks/mock-auth.service.ts index 6258e4aa21a570bd60ce2ae3be1d79746be323dc..a168ffd8e5230528719ab945abb95172895176fe 100644 --- a/src/app/shared/mocks/mock-auth.service.ts +++ b/src/app/shared/mocks/mock-auth.service.ts @@ -3,4 +3,7 @@ export class AuthServiceMock { public checksAuthenticationToken() { return } + public buildAuthHeader() { + return 'auth-header'; + } } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 85d001286d2c5bd3db184b74d2017373e75e6d7f..eb73514d7612bd40c10494726000ee61db4c9625 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -154,6 +154,8 @@ import { DsDynamicDisabledComponent } from './form/builder/ds-dynamic-form-ui/mo import { DsDynamicLookupRelationSearchTabComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component'; import { DsDynamicLookupRelationSelectionTabComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component'; import { PageSizeSelectorComponent } from './page-size-selector/page-size-selector.component'; +import { AbstractTrackableComponent } from './trackable/abstract-trackable.component'; +import { ComcolMetadataComponent } from './comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; import { ItemSelectComponent } from './object-select/item-select/item-select.component'; import { CollectionSelectComponent } from './object-select/collection-select/collection-select.component'; import { FilterInputSuggestionsComponent } from './input-suggestions/filter-suggestions/filter-input-suggestions.component'; @@ -326,6 +328,8 @@ const COMPONENTS = [ CollectionGridElementComponent, CommunityGridElementComponent, BrowseByComponent, + AbstractTrackableComponent, + ComcolMetadataComponent, ItemTypeBadgeComponent, ItemSelectComponent, CollectionSelectComponent, @@ -402,6 +406,7 @@ const SHARED_ITEM_PAGE_COMPONENTS = [ const PROVIDERS = [ TruncatableService, MockAdminGuard, + AbstractTrackableComponent, { provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn diff --git a/src/app/shared/trackable/abstract-trackable.component.spec.ts b/src/app/shared/trackable/abstract-trackable.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..37550922630c7c9a5320ffc33e6ada3fbcb29562 --- /dev/null +++ b/src/app/shared/trackable/abstract-trackable.component.spec.ts @@ -0,0 +1,101 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { AbstractTrackableComponent } from './abstract-trackable.component'; +import { INotification, Notification } from '../notifications/models/notification.model'; +import { NotificationType } from '../notifications/models/notification-type'; +import { of as observableOf } from 'rxjs'; +import { TranslateModule } from '@ngx-translate/core'; +import { ObjectUpdatesService } from '../../core/data/object-updates/object-updates.service'; +import { NotificationsService } from '../notifications/notifications.service'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { TestScheduler } from 'rxjs/testing'; +import { getTestScheduler } from 'jasmine-marbles'; + +describe('AbstractTrackableComponent', () => { + let comp: AbstractTrackableComponent; + let fixture: ComponentFixture<AbstractTrackableComponent>; + let objectUpdatesService; + let scheduler: TestScheduler; + + const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); + const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); + const successNotification: INotification = new Notification('id', NotificationType.Success, 'success'); + + const notificationsService = jasmine.createSpyObj('notificationsService', + { + info: infoNotification, + warning: warningNotification, + success: successNotification + } + ); + + const url = 'http://test-url.com/test-url'; + + beforeEach(async(() => { + objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', + { + saveAddFieldUpdate: {}, + discardFieldUpdates: {}, + reinstateFieldUpdates: observableOf(true), + initialize: {}, + hasUpdates: observableOf(true), + isReinstatable: observableOf(false), // should always return something --> its in ngOnInit + isValidPage: observableOf(true) + } + ); + + scheduler = getTestScheduler(); + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [AbstractTrackableComponent], + providers: [ + {provide: ObjectUpdatesService, useValue: objectUpdatesService}, + {provide: NotificationsService, useValue: notificationsService}, + ], schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AbstractTrackableComponent); + comp = fixture.componentInstance; + comp.url = url; + + fixture.detectChanges(); + }); + + it('should discard object updates', () => { + comp.discard(); + + expect(objectUpdatesService.discardFieldUpdates).toHaveBeenCalledWith(url, infoNotification); + }); + it('should undo the discard of object updates', () => { + comp.reinstate(); + + expect(objectUpdatesService.reinstateFieldUpdates).toHaveBeenCalledWith(url); + }); + + describe('isReinstatable', () => { + beforeEach(() => { + objectUpdatesService.isReinstatable.and.returnValue(observableOf(true)); + }); + + it('should return an observable that emits true', () => { + const expected = '(a|)'; + scheduler.expectObservable(comp.isReinstatable()).toBe(expected, {a: true}); + }); + }); + + describe('hasChanges', () => { + beforeEach(() => { + objectUpdatesService.hasUpdates.and.returnValue(observableOf(true)); + }); + + it('should return an observable that emits true', () => { + const expected = '(a|)'; + scheduler.expectObservable(comp.hasChanges()).toBe(expected, {a: true}); + }); + }); + +}); diff --git a/src/app/shared/trackable/abstract-trackable.component.ts b/src/app/shared/trackable/abstract-trackable.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..cd1b425f1058387864523fd35747aac4aa257340 --- /dev/null +++ b/src/app/shared/trackable/abstract-trackable.component.ts @@ -0,0 +1,78 @@ +import { ObjectUpdatesService } from '../../core/data/object-updates/object-updates.service'; +import { NotificationsService } from '../notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; +import { Component } from '@angular/core'; + +/** + * Abstract Component that is able to track changes made in the inheriting component using the ObjectUpdateService + */ +@Component({ + selector: 'ds-abstract-trackable', + template: '' +}) +export class AbstractTrackableComponent { + + /** + * The time span for being able to undo discarding changes + */ + public discardTimeOut: number; + public message: string; + public url: string; + public notificationsPrefix = 'static-pages.form.notification'; + + constructor( + public objectUpdatesService: ObjectUpdatesService, + public notificationsService: NotificationsService, + public translateService: TranslateService, + ) { + + } + + /** + * Request the object updates service to discard all current changes to this item + * Shows a notification to remind the user that they can undo this + */ + discard() { + const undoNotification = this.notificationsService.info(this.getNotificationTitle('discarded'), this.getNotificationContent('discarded'), {timeOut: this.discardTimeOut}); + this.objectUpdatesService.discardFieldUpdates(this.url, undoNotification); + } + + /** + * Request the object updates service to undo discarding all changes to this item + */ + reinstate() { + this.objectUpdatesService.reinstateFieldUpdates(this.url); + } + + /** + * Checks whether or not the object is currently reinstatable + */ + isReinstatable(): Observable<boolean> { + return this.objectUpdatesService.isReinstatable(this.url); + } + + /** + * Checks whether or not there are currently updates for this object + */ + hasChanges(): Observable<boolean> { + return this.objectUpdatesService.hasUpdates(this.url); + } + + /** + * Get translated notification title + * @param key + */ + private getNotificationTitle(key: string) { + return this.translateService.instant(this.notificationsPrefix + key + '.title'); + } + + /** + * Get translated notification content + * @param key + */ + private getNotificationContent(key: string) { + return this.translateService.instant(this.notificationsPrefix + key + '.content'); + + } +} diff --git a/src/app/shared/uploader/uploader-options.model.ts b/src/app/shared/uploader/uploader-options.model.ts index 0bd6412b1777934e56ab3c261484ea95a80b7cb9..f195b0930e5ecbef072d9bd469dad44348be291c 100644 --- a/src/app/shared/uploader/uploader-options.model.ts +++ b/src/app/shared/uploader/uploader-options.model.ts @@ -1,3 +1,4 @@ +import { RestRequestMethod } from '../../core/data/rest-request-method'; export class UploaderOptions { /** @@ -9,5 +10,15 @@ export class UploaderOptions { disableMultipart = false; - itemAlias: string; + itemAlias: string = null; + + /** + * Automatically send out an upload request when adding files + */ + autoUpload = true; + + /** + * The request method to use for the file upload request + */ + method: RestRequestMethod = RestRequestMethod.POST; } diff --git a/src/app/shared/uploader/uploader.component.html b/src/app/shared/uploader/uploader.component.html index 9d994313c6a64b37bb773c75807f959a652e80ff..36078fbeb4308f170dc8b5e0b3c87009224ffec5 100644 --- a/src/app/shared/uploader/uploader.component.html +++ b/src/app/shared/uploader/uploader.component.html @@ -19,23 +19,24 @@ (fileOver)="fileOverBase($event)" class="well ds-base-drop-zone mt-1 mb-3 text-muted"> <p class="text-center m-0 p-0 d-flex justify-content-center align-items-center" *ngIf="uploader?.queue?.length === 0"> - <span><i class="fas fa-cloud-upload" aria-hidden="true"></i> {{dropMsg | translate}} {{'uploader.or' | translate}} - <label class="btn btn-link m-0 p-0"> - <input class="d-none" type="file" ng2FileSelect [uploader]="uploader" multiple /> - {{'uploader.browse' | translate}} - </label> - </span> + <span><i class="fas fa-cloud-upload" aria-hidden="true"></i> {{dropMsg | translate}} {{'uploader.or' | translate}}</span> + <label class="btn btn-link m-0 p-0 ml-1"> + <input class="d-none" type="file" ng2FileSelect [uploader]="uploader" multiple /> + {{'uploader.browse' | translate}} + </label> </p> <div *ngIf="(isOverBaseDropZone | async) || uploader?.queue?.length !== 0"> <div class="m-1"> <div class="upload-item-top"> - <span class="filename">{{'uploader.queue-length' | translate}}: {{ uploader?.queue?.length }} | {{ uploader?.queue[0]?.file.name }}</span> + <span class="filename"> + <span *ngIf="!uploader.options.disableMultipart">{{'uploader.queue-length' | translate}}: {{ uploader?.queue?.length }} | </span>{{ uploader?.queue[0]?.file.name }} + </span> <div class="btn-group btn-group-sm float-right" role="group"> <button type="button" class="btn btn-danger" (click)="uploader.clearQueue()" [disabled]="!uploader.queue.length"> <i class="fas fa-trash" aria-hidden="true"></i> </button> </div> - <span *ngIf="uploader.progress < 100" class="float-right mr-3">{{ uploader.progress }}%</span> + <span *ngIf="uploader.progress < 100 && !(uploader.progress === 0 && !uploader.options.autoUpload)" class="float-right mr-3">{{ uploader.progress }}%</span> <span *ngIf="uploader.progress === 100" class="float-right mr-3">{{'uploader.processing' | translate}}...</span> </div> <div class="ds-base-drop-zone-progress clearfix mt-2"> diff --git a/src/app/shared/uploader/uploader.component.spec.ts b/src/app/shared/uploader/uploader.component.spec.ts index a36bd7241bb02827b30f803f4d3823d88601f7d2..dcdac911bfa722ae5fab1ad144281aa92616f54c 100644 --- a/src/app/shared/uploader/uploader.component.spec.ts +++ b/src/app/shared/uploader/uploader.component.spec.ts @@ -64,12 +64,12 @@ describe('Chips component', () => { template: `` }) class TestComponent { - public uploadFilesOptions: UploaderOptions = { + public uploadFilesOptions: UploaderOptions = Object.assign(new UploaderOptions(), { url: 'http://test', authToken: null, disableMultipart: false, itemAlias: null - }; + }); /* tslint:disable:no-empty */ public onBeforeUpload = () => { diff --git a/src/app/shared/uploader/uploader.component.ts b/src/app/shared/uploader/uploader.component.ts index ad52f4a93f9f6446a884c41c47f32a126a80cb07..935d196d0848c18aef0243d45909f9fce1c1df7f 100644 --- a/src/app/shared/uploader/uploader.component.ts +++ b/src/app/shared/uploader/uploader.component.ts @@ -95,7 +95,8 @@ export class UploaderComponent { disableMultipart: this.uploadFilesOptions.disableMultipart, itemAlias: this.uploadFilesOptions.itemAlias, removeAfterUpload: true, - autoUpload: true + autoUpload: this.uploadFilesOptions.autoUpload, + method: this.uploadFilesOptions.method }); if (isUndefined(this.enableDragOverDocument)) { @@ -117,7 +118,10 @@ export class UploaderComponent { if (isUndefined(this.onBeforeUpload)) { this.onBeforeUpload = () => {return}; } - this.uploader.onBeforeUploadItem = () => { + this.uploader.onBeforeUploadItem = (item) => { + if (item.url !== this.uploader.options.url) { + item.url = this.uploader.options.url; + } this.onBeforeUpload(); this.isOverDocumentDropZone = observableOf(false); diff --git a/src/app/submission/form/submission-form.component.ts b/src/app/submission/form/submission-form.component.ts index 1732075bf83978695cf87510f861997c27abd4f9..3ea07f9ae76220218e9235e79be5675c47eae650 100644 --- a/src/app/submission/form/submission-form.component.ts +++ b/src/app/submission/form/submission-form.component.ts @@ -77,12 +77,7 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { * The uploader configuration options * @type {UploaderOptions} */ - public uploadFilesOptions: UploaderOptions = { - url: '', - authToken: null, - disableMultipart: false, - itemAlias: null - }; + public uploadFilesOptions: UploaderOptions = new UploaderOptions(); /** * A boolean representing if component is active diff --git a/src/app/submission/form/submission-upload-files/submission-upload-files.component.spec.ts b/src/app/submission/form/submission-upload-files/submission-upload-files.component.spec.ts index 60a572df54fdc2ad6bfbb6ce21f578a9b4231c80..34d291f0e469e8d06098d00908cd421e29d0b0b3 100644 --- a/src/app/submission/form/submission-upload-files/submission-upload-files.component.spec.ts +++ b/src/app/submission/form/submission-upload-files/submission-upload-files.component.spec.ts @@ -28,6 +28,7 @@ import { SubmissionJsonPatchOperationsServiceStub } from '../../../shared/testin import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service'; import { SharedModule } from '../../../shared/shared.module'; import { createTestComponent } from '../../../shared/testing/utils'; +import { UploaderOptions } from '../../../shared/uploader/uploader-options.model'; describe('SubmissionUploadFilesComponent Component', () => { @@ -112,12 +113,12 @@ describe('SubmissionUploadFilesComponent Component', () => { comp.submissionId = submissionId; comp.collectionId = collectionId; comp.sectionId = 'upload'; - comp.uploadFilesOptions = { + comp.uploadFilesOptions = Object.assign(new UploaderOptions(),{ url: '', authToken: null, disableMultipart: false, itemAlias: null - }; + }); }); @@ -208,11 +209,11 @@ class TestComponent { submissionId = mockSubmissionId; collectionId = mockSubmissionCollectionId; sectionId = 'upload'; - uploadFilesOptions = { + uploadFilesOptions = Object.assign(new UploaderOptions(), { url: '', authToken: null, disableMultipart: false, itemAlias: null - }; + }); }