diff --git a/src/app/+my-dspace-page/my-dspace-configuration.service.ts b/src/app/+my-dspace-page/my-dspace-configuration.service.ts index 3ee91c5f31..420e045887 100644 --- a/src/app/+my-dspace-page/my-dspace-configuration.service.ts +++ b/src/app/+my-dspace-page/my-dspace-configuration.service.ts @@ -72,6 +72,12 @@ export class MyDSpaceConfigurationService extends SearchConfigurationService { this.isAdmin$ = this.roleService.isAdmin(); } + /** + * Returns the list of available configuration depend on the user role + * + * @return {Observable} + * Emits the available configuration list + */ public getAvailableConfigurationTypes(): Observable { return combineLatest(this.isSubmitter$, this.isController$, this.isAdmin$).pipe( first(), @@ -87,6 +93,12 @@ export class MyDSpaceConfigurationService extends SearchConfigurationService { })); } + /** + * Returns the select options for the available configuration list + * + * @return {Observable} + * Emits the select options list + */ public getAvailableConfigurationOptions(): Observable { return this.getAvailableConfigurationTypes().pipe( first(), diff --git a/src/app/+my-dspace-page/my-dspace-page-routing.module.ts b/src/app/+my-dspace-page/my-dspace-page-routing.module.ts index 70b758af0d..d70a007e3a 100644 --- a/src/app/+my-dspace-page/my-dspace-page-routing.module.ts +++ b/src/app/+my-dspace-page/my-dspace-page-routing.module.ts @@ -18,5 +18,8 @@ import { MyDSpaceGuard } from './my-dspace.guard'; ]) ] }) +/** + * This module defines the default component to load when navigating to the mydspace page path. + */ export class MyDspacePageRoutingModule { } diff --git a/src/app/+my-dspace-page/my-dspace-page.module.ts b/src/app/+my-dspace-page/my-dspace-page.module.ts index 697c22dd0e..4b8cf37b7a 100644 --- a/src/app/+my-dspace-page/my-dspace-page.module.ts +++ b/src/app/+my-dspace-page/my-dspace-page.module.ts @@ -60,6 +60,10 @@ import { MyDSpaceConfigurationService } from './my-dspace-configuration.service' PoolMyDSpaceResultDetailElementComponent ] }) + +/** + * This module handles all components that are necessary for the mydspace page + */ export class MyDSpacePageModule { } diff --git a/src/app/+my-dspace-page/my-dspace-results/my-dspace-results.component.ts b/src/app/+my-dspace-page/my-dspace-results/my-dspace-results.component.ts index e6a086fd46..3a16def9c1 100644 --- a/src/app/+my-dspace-page/my-dspace-results/my-dspace-results.component.ts +++ b/src/app/+my-dspace-page/my-dspace-results/my-dspace-results.component.ts @@ -3,7 +3,6 @@ import { Component, Input } from '@angular/core'; import { RemoteData } from '../../core/data/remote-data'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { fadeIn, fadeInOut } from '../../shared/animations/fade'; -import { SortOptions } from '../../core/cache/models/sort-options.model'; import { MyDSpaceResult } from '../my-dspace-result.model'; import { SearchOptions } from '../../+search-page/search-options.model'; import { PaginatedList } from '../../core/data/paginated-list'; @@ -11,9 +10,7 @@ import { ViewMode } from '../../core/shared/view-mode.model'; import { isEmpty } from '../../shared/empty.util'; /** - * This component renders a simple item page. - * The route parameter 'id' is used to request the item it represents. - * All fields of the item that should be displayed, are defined in its template. + * Component that represents all results for mydspace page */ @Component({ selector: 'ds-my-dspace-results', @@ -24,13 +21,30 @@ import { isEmpty } from '../../shared/empty.util'; ] }) export class MyDSpaceResultsComponent { + + /** + * The actual search result objects + */ @Input() searchResults: RemoteData>>; + + /** + * The current configuration of the search + */ @Input() searchConfig: SearchOptions; - @Input() sortConfig: SortOptions; + + /** + * The current view mode for the search results + */ @Input() viewMode: ViewMode; + /** + * A boolean representing if search results entry are separated by a line + */ hasBorder = true; + /** + * Check if mydspace search results are loading + */ isLoading() { return !this.searchResults || isEmpty(this.searchResults) || this.searchResults.isLoading; } diff --git a/src/app/+search-page/search-switch-configuration/search-configuration-option.model.ts b/src/app/+search-page/search-switch-configuration/search-configuration-option.model.ts index 7f9b4acd96..6f9a72da48 100644 --- a/src/app/+search-page/search-switch-configuration/search-configuration-option.model.ts +++ b/src/app/+search-page/search-switch-configuration/search-configuration-option.model.ts @@ -1,4 +1,15 @@ +/** + * Represents a search configuration select option + */ export interface SearchConfigurationOption { + + /** + * The select option value + */ value: string; + + /** + * The select option label + */ label: string; } diff --git a/src/app/+search-page/search-switch-configuration/search-switch-configuration.component.html b/src/app/+search-page/search-switch-configuration/search-switch-configuration.component.html index 5b1bdc1ddd..8df37214d1 100644 --- a/src/app/+search-page/search-switch-configuration/search-switch-configuration.component.html +++ b/src/app/+search-page/search-switch-configuration/search-switch-configuration.component.html @@ -4,7 +4,7 @@ - diff --git a/src/app/shared/mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component.spec.ts b/src/app/shared/mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component.spec.ts new file mode 100644 index 0000000000..d7e0b53748 --- /dev/null +++ b/src/app/shared/mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component.spec.ts @@ -0,0 +1,108 @@ +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; + +import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; + +import { ClaimedTaskActionsRejectComponent } from './claimed-task-actions-reject.component'; +import { MockTranslateLoader } from '../../../mocks/mock-translate-loader'; + +let component: ClaimedTaskActionsRejectComponent; +let fixture: ComponentFixture; +let formBuilder: FormBuilder; +let modalService: NgbModal; + +describe('ClaimedTaskActionsRejectComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + ReactiveFormsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + }) + ], + declarations: [ClaimedTaskActionsRejectComponent], + providers: [ + FormBuilder, + NgbModal + ], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(ClaimedTaskActionsRejectComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ClaimedTaskActionsRejectComponent); + component = fixture.componentInstance; + formBuilder = TestBed.get(FormBuilder); + modalService = TestBed.get(NgbModal); + component.modalRef = modalService.open('ok'); + fixture.detectChanges(); + }); + + afterEach(() => { + fixture = null; + component = null; + modalService = null; + formBuilder = null; + }); + + it('should init reject form properly', () => { + expect(component.rejectForm).toBeDefined(); + expect(component.rejectForm instanceof FormGroup).toBeTruthy(); + expect(component.rejectForm.controls.reason).toBeDefined(); + }); + + it('should display reject button', () => { + const btn = fixture.debugElement.query(By.css('.btn-danger')); + + expect(btn).toBeDefined(); + }); + + it('should display spin icon when reject is pending', () => { + component.processingReject = true; + fixture.detectChanges(); + + const span = fixture.debugElement.query(By.css('.btn-danger .fa-spin')); + + expect(span).toBeDefined(); + }); + + it('should call openRejectModal on reject button click', fakeAsync(() => { + spyOn(component.rejectForm, 'reset'); + const btn = fixture.debugElement.query(By.css('.btn-danger')); + btn.nativeElement.click(); + fixture.detectChanges(); + + expect(component.rejectForm.reset).toHaveBeenCalled(); + expect(component.modalRef).toBeDefined(); + + component.modalRef.close() + })); + + it('should call confirmReject on form submit', fakeAsync(() => { + spyOn(component.reject, 'emit'); + + const btn = fixture.debugElement.query(By.css('.btn-danger')); + btn.nativeElement.click(); + fixture.detectChanges(); + + expect(component.modalRef).toBeDefined(); + + const form = ((document as any).querySelector('form')); + form.dispatchEvent(new Event('ngSubmit')); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(component.reject.emit).toHaveBeenCalled(); + }); + + })); +}); diff --git a/src/app/shared/mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component.ts b/src/app/shared/mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component.ts index 7f09896b4c..b66c104695 100644 --- a/src/app/shared/mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component.ts +++ b/src/app/shared/mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component.ts @@ -10,18 +10,45 @@ import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; }) export class ClaimedTaskActionsRejectComponent implements OnInit { + + /** + * A boolean representing if a reject operation is pending + */ @Input() processingReject: boolean; - @Input() taskId: string; + + /** + * CSS classes to append to reject button + */ @Input() wrapperClass: string; + /** + * An event fired when a reject action is confirmed. + * Event's payload equals to reject reason. + */ @Output() reject: EventEmitter = new EventEmitter(); + /** + * The reject form group + */ public rejectForm: FormGroup; + + /** + * Reference to NgbModal + */ public modalRef: NgbModalRef; + /** + * Initialize instance variables + * + * @param {FormBuilder} formBuilder + * @param {NgbModal} modalService + */ constructor(private formBuilder: FormBuilder, private modalService: NgbModal) { } + /** + * Initialize form + */ ngOnInit() { this.rejectForm = this.formBuilder.group({ reason: ['', Validators.required] @@ -29,15 +56,23 @@ export class ClaimedTaskActionsRejectComponent implements OnInit { } - click() { + /** + * Close modal and emit reject event + */ + confirmReject() { this.processingReject = true; this.modalRef.close('Send Button'); const reason = this.rejectForm.get('reason').value; this.reject.emit(reason); } - openRejectModal(rejectModal) { + /** + * Open modal + * + * @param content + */ + openRejectModal(content: any) { this.rejectForm.reset(); - this.modalRef = this.modalService.open(rejectModal); + this.modalRef = this.modalService.open(content); } } diff --git a/src/app/shared/mydspace-actions/item/item-actions.component.spec.ts b/src/app/shared/mydspace-actions/item/item-actions.component.spec.ts new file mode 100644 index 0000000000..72be122c8f --- /dev/null +++ b/src/app/shared/mydspace-actions/item/item-actions.component.spec.ts @@ -0,0 +1,96 @@ +import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; + +import { of as observableOf } from 'rxjs'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; + +import { MockTranslateLoader } from '../../mocks/mock-translate-loader'; +import { RouterStub } from '../../testing/router-stub'; +import { Item } from '../../../core/shared/item.model'; +import { ItemActionsComponent } from './item-actions.component'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { NotificationsServiceStub } from '../../testing/notifications-service-stub'; + +let component: ItemActionsComponent; +let fixture: ComponentFixture; + +let mockObject: Item; + +const mockDataService = {}; + +mockObject = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'dc.type': [ + { + language: null, + value: 'Article' + } + ], + 'dc.contributor.author': [ + { + language: 'en_US', + value: 'Smith, Donald' + } + ], + 'dc.date.issued': [ + { + language: null, + value: '2015-06-26' + } + ] + } +}); + +describe('ItemActionsComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + }) + ], + declarations: [ItemActionsComponent], + providers: [ + { provide: Injector, useValue: {} }, + { provide: Router, useValue: new RouterStub() }, + { provide: ItemDataService, useValue: mockDataService }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(ItemActionsComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ItemActionsComponent); + component = fixture.componentInstance; + component.object = mockObject; + fixture.detectChanges(); + }); + + afterEach(() => { + fixture = null; + component = null; + }); + + it('should init object properly', () => { + component.object = null; + component.initObjects(mockObject); + + expect(component.object).toEqual(mockObject); + }); + +}); diff --git a/src/app/shared/mydspace-actions/item/item-actions.component.ts b/src/app/shared/mydspace-actions/item/item-actions.component.ts index b2022bbdd4..0760fe54e0 100644 --- a/src/app/shared/mydspace-actions/item/item-actions.component.ts +++ b/src/app/shared/mydspace-actions/item/item-actions.component.ts @@ -1,11 +1,17 @@ import { Component, Injector, Input } from '@angular/core'; import { Router } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; + import { MyDSpaceActionsComponent } from '../mydspace-actions'; -import { ResourceType } from '../../../core/shared/resource-type'; import { ItemDataService } from '../../../core/data/item-data.service'; import { Item } from '../../../core/shared/item.model'; +import { ResourceType } from '../../../core/shared/resource-type'; +import { NotificationsService } from '../../notifications/notifications.service'; +/** + * This component represents mydspace actions related to Item object. + */ @Component({ selector: 'ds-item-actions', styleUrls: ['./item-actions.component.scss'], @@ -13,13 +19,32 @@ import { Item } from '../../../core/shared/item.model'; }) export class ItemActionsComponent extends MyDSpaceActionsComponent { + + /** + * The Item object + */ @Input() object: Item; + /** + * Initialize instance variables + * + * @param {Injector} injector + * @param {Router} router + * @param {NotificationsService} notificationsService + * @param {TranslateService} translate + */ constructor(protected injector: Injector, - protected router: Router) { - super(ResourceType.Workspaceitem, injector, router); + protected router: Router, + protected notificationsService: NotificationsService, + protected translate: TranslateService) { + super(ResourceType.Item, injector, router, notificationsService, translate); } + /** + * Init the target object + * + * @param {Item} object + */ initObjects(object: Item) { this.object = object; } diff --git a/src/app/shared/mydspace-actions/mydspace-actions-service.factory.ts b/src/app/shared/mydspace-actions/mydspace-actions-service.factory.ts index b3efa4c9db..7aa948f689 100644 --- a/src/app/shared/mydspace-actions/mydspace-actions-service.factory.ts +++ b/src/app/shared/mydspace-actions/mydspace-actions-service.factory.ts @@ -5,10 +5,17 @@ import { ClaimedTaskDataService } from '../../core/tasks/claimed-task-data.servi import { PoolTaskDataService } from '../../core/tasks/pool-task-data.service'; import { WorkflowitemDataService } from '../../core/submission/workflowitem-data.service'; import { CacheableObject } from '../../core/cache/object-cache.reducer'; +import { ItemDataService } from '../../core/data/item-data.service'; +/** + * Class to return DataService for given ResourceType + */ export class MydspaceActionsServiceFactory> { public getConstructor(type: ResourceType): TService { switch (type) { + case ResourceType.Item: { + return ItemDataService as any; + } case ResourceType.Workspaceitem: { return WorkspaceitemDataService as any; } diff --git a/src/app/shared/mydspace-actions/mydspace-actions.ts b/src/app/shared/mydspace-actions/mydspace-actions.ts index ab76eaf280..8e465644c3 100644 --- a/src/app/shared/mydspace-actions/mydspace-actions.ts +++ b/src/app/shared/mydspace-actions/mydspace-actions.ts @@ -8,19 +8,55 @@ import { RemoteData } from '../../core/data/remote-data'; import { DataService } from '../../core/data/data.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { ResourceType } from '../../core/shared/resource-type'; +import { NotificationOptions } from '../notifications/models/notification-options.model'; +import { NotificationsService } from '../notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +/** + * Abstract class for all different representations of mydspace actions + */ export abstract class MyDSpaceActionsComponent> { + + /** + * The target mydspace object + */ @Input() abstract object: T; + + /** + * Instance of DataService realted to mydspace object + */ protected objectDataService: TService; - constructor(protected objectType: ResourceType, protected injector: Injector, protected router: Router) { + /** + * Initialize instance variables + * + * @param {ResourceType} objectType + * @param {Injector} injector + * @param {Router} router + * @param {NotificationsService} notificationsService + * @param {TranslateService} translate + */ + constructor( + protected objectType: ResourceType, + protected injector: Injector, + protected router: Router, + protected notificationsService: NotificationsService, + protected translate: TranslateService) { const factory = new MydspaceActionsServiceFactory(); this.objectDataService = injector.get(factory.getConstructor(objectType)); } + /** + * Abstract method called to init the target object + * + * @param {T} object + */ abstract initObjects(object: T): void; - reload() { + /** + * Refresh current page + */ + reload(): void { // override the route reuse strategy this.router.routeReuseStrategy.shouldReuseRoute = () => { return false; @@ -30,12 +66,34 @@ export abstract class MyDSpaceActionsComponent) => rd.hasSucceeded) ).subscribe((rd: RemoteData) => { this.initObjects(rd.payload as T); }); } + + /** + * Handle action response and show properly notification + * + * @param result + * true on success, false otherwise + */ + handleActionResponse(result: boolean): void { + if (result) { + this.reload(); + this.notificationsService.success(null, + this.translate.get('submission.workflow.tasks.generic.success'), + new NotificationOptions(5000, false)); + } else { + this.notificationsService.error(null, + this.translate.get('submission.workflow.tasks.generic.error'), + new NotificationOptions(20000, true)); + } + } } diff --git a/src/app/shared/mydspace-actions/pool-task/pool-task-actions.component.spec.ts b/src/app/shared/mydspace-actions/pool-task/pool-task-actions.component.spec.ts new file mode 100644 index 0000000000..1c0e8e71fa --- /dev/null +++ b/src/app/shared/mydspace-actions/pool-task/pool-task-actions.component.spec.ts @@ -0,0 +1,170 @@ +import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { By } from '@angular/platform-browser'; + +import { of as observableOf } from 'rxjs'; +import { cold } from 'jasmine-marbles'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; + +import { MockTranslateLoader } from '../../mocks/mock-translate-loader'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { NotificationsServiceStub } from '../../testing/notifications-service-stub'; +import { RouterStub } from '../../testing/router-stub'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Item } from '../../../core/shared/item.model'; +import { PoolTaskDataService } from '../../../core/tasks/pool-task-data.service'; +import { PoolTaskActionsComponent } from './pool-task-actions.component'; +import { PoolTask } from '../../../core/tasks/models/pool-task-object.model'; +import { Workflowitem } from '../../../core/submission/models/workflowitem.model'; + +let component: PoolTaskActionsComponent; +let fixture: ComponentFixture; + +let mockObject: PoolTask; +let notificationsServiceStub: NotificationsServiceStub; +let router: RouterStub; + +const mockDataService = jasmine.createSpyObj('PoolTaskDataService', { + claimTask: jasmine.createSpy('claimTask') +}); + +const item = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'dc.type': [ + { + language: null, + value: 'Article' + } + ], + 'dc.contributor.author': [ + { + language: 'en_US', + value: 'Smith, Donald' + } + ], + 'dc.date.issued': [ + { + language: null, + value: '2015-06-26' + } + ] + } +}); +const rdItem = new RemoteData(false, false, true, null, item); +const workflowitem = Object.assign(new Workflowitem(), { item: observableOf(rdItem) }); +const rdWorkflowitem = new RemoteData(false, false, true, null, workflowitem); +mockObject = Object.assign(new PoolTask(), { workflowitem: observableOf(rdWorkflowitem), id: '1234' }); + +describe('PoolTaskActionsComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + }) + ], + declarations: [PoolTaskActionsComponent], + providers: [ + { provide: Injector, useValue: {} }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: Router, useValue: new RouterStub() }, + { provide: PoolTaskDataService, useValue: mockDataService }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(PoolTaskActionsComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PoolTaskActionsComponent); + component = fixture.componentInstance; + component.object = mockObject; + notificationsServiceStub = TestBed.get(NotificationsService); + router = TestBed.get(Router); + fixture.detectChanges(); + }); + + afterEach(() => { + fixture = null; + component = null; + }); + + it('should init objects properly', () => { + component.object = null; + component.initObjects(mockObject); + + expect(component.object).toEqual(mockObject); + + expect(component.workflowitem$).toBeObservable(cold('(b|)', { + b: rdWorkflowitem.payload + })) + }); + + it('should display claim task button', () => { + const btn = fixture.debugElement.query(By.css('.btn-info')); + + expect(btn).toBeDefined(); + }); + + it('should call claimTask method on claim', fakeAsync(() => { + spyOn(component, 'reload'); + mockDataService.claimTask.and.returnValue(observableOf({hasSucceeded: true})); + + component.claim(); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(mockDataService.claimTask).toHaveBeenCalledWith(mockObject.id); + }); + + })); + + it('should display a success notification on claim success', async(() => { + spyOn(component, 'reload'); + mockDataService.claimTask.and.returnValue(observableOf({hasSucceeded: true})); + + component.claim(); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(notificationsServiceStub.success).toHaveBeenCalled(); + }); + })); + + it('should reload page on claim success', async(() => { + spyOn(router, 'navigateByUrl'); + router.url = 'test.url/test'; + mockDataService.claimTask.and.returnValue(observableOf({hasSucceeded: true})); + + component.claim(); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(router.navigateByUrl).toHaveBeenCalledWith('test.url/test'); + }); + })); + + it('should display an error notification on claim failure', async(() => { + mockDataService.claimTask.and.returnValue(observableOf({hasSucceeded: false})); + + component.claim(); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(notificationsServiceStub.error).toHaveBeenCalled(); + }); + })); + +}); diff --git a/src/app/shared/mydspace-actions/pool-task/pool-task-actions.component.ts b/src/app/shared/mydspace-actions/pool-task/pool-task-actions.component.ts index eccd427668..bd8f3f1a37 100644 --- a/src/app/shared/mydspace-actions/pool-task/pool-task-actions.component.ts +++ b/src/app/shared/mydspace-actions/pool-task/pool-task-actions.component.ts @@ -10,35 +10,64 @@ import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-res import { RemoteData } from '../../../core/data/remote-data'; import { PoolTask } from '../../../core/tasks/models/pool-task-object.model'; import { PoolTaskDataService } from '../../../core/tasks/pool-task-data.service'; -import { NotificationsService } from '../../notifications/notifications.service'; -import { NotificationOptions } from '../../notifications/models/notification-options.model'; import { isNotUndefined } from '../../empty.util'; import { MyDSpaceActionsComponent } from '../mydspace-actions'; import { ResourceType } from '../../../core/shared/resource-type'; +import { NotificationsService } from '../../notifications/notifications.service'; +/** + * This component represents mydspace actions related to PoolTask object. + */ @Component({ selector: 'ds-pool-task-actions', styleUrls: ['./pool-task-actions.component.scss'], templateUrl: './pool-task-actions.component.html', }) - export class PoolTaskActionsComponent extends MyDSpaceActionsComponent { + + /** + * The PoolTask object + */ @Input() object: PoolTask; + /** + * A boolean representing if a claim operation is pending + * @type {BehaviorSubject} + */ public processingClaim$ = new BehaviorSubject(false); + + /** + * The workflowitem object that belonging to the PoolTask object + */ public workflowitem$: Observable; + /** + * Initialize instance variables + * + * @param {Injector} injector + * @param {Router} router + * @param {NotificationsService} notificationsService + * @param {TranslateService} translate + */ constructor(protected injector: Injector, protected router: Router, - private notificationsService: NotificationsService, - private translate: TranslateService) { - super(ResourceType.PoolTask, injector, router); + protected notificationsService: NotificationsService, + protected translate: TranslateService) { + super(ResourceType.PoolTask, injector, router, notificationsService, translate); } + /** + * Initialize objects + */ ngOnInit() { this.initObjects(this.object); } + /** + * Init the PoolTask and Workflowitem objects + * + * @param {PoolTask} object + */ initObjects(object: PoolTask) { this.object = object; this.workflowitem$ = (this.object.workflowitem as Observable>).pipe( @@ -46,27 +75,15 @@ export class PoolTaskActionsComponent extends MyDSpaceActionsComponent) => rd.payload)); } + /** + * Claim the task. + */ claim() { this.processingClaim$.next(true); this.objectDataService.claimTask(this.object.id) .subscribe((res: ProcessTaskResponse) => { - this.responseHandle(res); + this.handleActionResponse(res.hasSucceeded); + this.processingClaim$.next(false); }); } - - private responseHandle(res: ProcessTaskResponse) { - if (res.hasSucceeded) { - this.processingClaim$.next(false); - this.reload(); - this.notificationsService.success(null, - this.translate.get('submission.workflow.tasks.generic.success'), - new NotificationOptions(5000, false)); - } else { - this.processingClaim$.next(false); - this.notificationsService.error(null, - this.translate.get('submission.workflow.tasks.generic.error'), - new NotificationOptions(20000, true)); - } - } - } diff --git a/src/app/shared/mydspace-actions/workflowitem/workflowitem-actions.component.spec.ts b/src/app/shared/mydspace-actions/workflowitem/workflowitem-actions.component.spec.ts new file mode 100644 index 0000000000..7533565afe --- /dev/null +++ b/src/app/shared/mydspace-actions/workflowitem/workflowitem-actions.component.spec.ts @@ -0,0 +1,98 @@ +import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; + +import { of as observableOf } from 'rxjs'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; + +import { MockTranslateLoader } from '../../mocks/mock-translate-loader'; +import { RouterStub } from '../../testing/router-stub'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Item } from '../../../core/shared/item.model'; +import { Workflowitem } from '../../../core/submission/models/workflowitem.model'; +import { WorkflowitemActionsComponent } from './workflowitem-actions.component'; +import { WorkflowitemDataService } from '../../../core/submission/workflowitem-data.service'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { NotificationsServiceStub } from '../../testing/notifications-service-stub'; + +let component: WorkflowitemActionsComponent; +let fixture: ComponentFixture; + +let mockObject: Workflowitem; + +const mockDataService = {}; + +const item = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'dc.type': [ + { + language: null, + value: 'Article' + } + ], + 'dc.contributor.author': [ + { + language: 'en_US', + value: 'Smith, Donald' + } + ], + 'dc.date.issued': [ + { + language: null, + value: '2015-06-26' + } + ] + } +}); +const rd = new RemoteData(false, false, true, null, item); +mockObject = Object.assign(new Workflowitem(), { item: observableOf(rd), id: '1234', uuid: '1234' }); + +describe('WorkflowitemActionsComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + }) + ], + declarations: [WorkflowitemActionsComponent], + providers: [ + { provide: Injector, useValue: {} }, + { provide: Router, useValue: new RouterStub() }, + { provide: WorkflowitemDataService, useValue: mockDataService }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(WorkflowitemActionsComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(WorkflowitemActionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + afterEach(() => { + fixture = null; + component = null; + }); + + it('should init object properly', () => { + component.initObjects(mockObject); + + expect(component.object).toEqual(mockObject); + }); + +}); diff --git a/src/app/shared/mydspace-actions/workflowitem/workflowitem-actions.component.ts b/src/app/shared/mydspace-actions/workflowitem/workflowitem-actions.component.ts index 04de5da885..a6304bf5d4 100644 --- a/src/app/shared/mydspace-actions/workflowitem/workflowitem-actions.component.ts +++ b/src/app/shared/mydspace-actions/workflowitem/workflowitem-actions.component.ts @@ -1,25 +1,49 @@ import { Component, Injector, Input } from '@angular/core'; import { Router } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; + import { MyDSpaceActionsComponent } from '../mydspace-actions'; -import { ResourceType } from '../../../core/shared/resource-type'; import { Workflowitem } from '../../../core/submission/models/workflowitem.model'; import { WorkflowitemDataService } from '../../../core/submission/workflowitem-data.service'; +import { ResourceType } from '../../../core/shared/resource-type'; +import { NotificationsService } from '../../notifications/notifications.service'; +/** + * This component represents mydspace actions related to Workflowitem object. + */ @Component({ selector: 'ds-workflowitem-actions', styleUrls: ['./workflowitem-actions.component.scss'], templateUrl: './workflowitem-actions.component.html', }) - export class WorkflowitemActionsComponent extends MyDSpaceActionsComponent { + + /** + * The Workflowitem object + */ @Input() object: Workflowitem; + /** + * Initialize instance variables + * + * @param {Injector} injector + * @param {Router} router + * @param {NotificationsService} notificationsService + * @param {TranslateService} translate + */ constructor(protected injector: Injector, - protected router: Router) { - super(ResourceType.Workflowitem, injector, router); + protected router: Router, + protected notificationsService: NotificationsService, + protected translate: TranslateService) { + super(ResourceType.Workflowitem, injector, router, notificationsService, translate); } + /** + * Init the target object + * + * @param {Workflowitem} object + */ initObjects(object: Workflowitem) { this.object = object; } diff --git a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.spec.ts b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.spec.ts new file mode 100644 index 0000000000..ec8bc4a11c --- /dev/null +++ b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.spec.ts @@ -0,0 +1,163 @@ +import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { By } from '@angular/platform-browser'; + +import { of as observableOf } from 'rxjs'; +import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; + +import { MockTranslateLoader } from '../../mocks/mock-translate-loader'; +import { NotificationsService } from '../../notifications/notifications.service'; +import { NotificationsServiceStub } from '../../testing/notifications-service-stub'; +import { RouterStub } from '../../testing/router-stub'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Item } from '../../../core/shared/item.model'; +import { Workspaceitem } from '../../../core/submission/models/workspaceitem.model'; +import { WorkspaceitemActionsComponent } from './workspaceitem-actions.component'; +import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service'; + +let component: WorkspaceitemActionsComponent; +let fixture: ComponentFixture; + +let mockObject: Workspaceitem; +let notificationsServiceStub: NotificationsServiceStub; + +const mockDataService = jasmine.createSpyObj('WorkspaceitemDataService', { + delete: jasmine.createSpy('delete') +}); + +const item = Object.assign(new Item(), { + bitstreams: observableOf({}), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'dc.type': [ + { + language: null, + value: 'Article' + } + ], + 'dc.contributor.author': [ + { + language: 'en_US', + value: 'Smith, Donald' + } + ], + 'dc.date.issued': [ + { + language: null, + value: '2015-06-26' + } + ] + } +}); +const rd = new RemoteData(false, false, true, null, item); +mockObject = Object.assign(new Workspaceitem(), { item: observableOf(rd), id: '1234', uuid: '1234' }); + +describe('WorkspaceitemActionsComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbModule.forRoot(), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + }) + ], + declarations: [WorkspaceitemActionsComponent], + providers: [ + { provide: Injector, useValue: {} }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: Router, useValue: new RouterStub() }, + { provide: WorkspaceitemDataService, useValue: mockDataService }, + NgbModal + ], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(WorkspaceitemActionsComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(WorkspaceitemActionsComponent); + component = fixture.componentInstance; + component.object = mockObject; + notificationsServiceStub = TestBed.get(NotificationsService); + fixture.detectChanges(); + }); + + afterEach(() => { + fixture = null; + component = null; + }); + + it('should init object properly', () => { + component.object = null; + component.initObjects(mockObject); + + expect(component.object).toEqual(mockObject); + }); + + it('should display edit button', () => { + const btn = fixture.debugElement.query(By.css('.btn-primary')); + + expect(btn).toBeDefined(); + }); + + it('should display delete button', () => { + const btn = fixture.debugElement.query(By.css('.btn-danger')); + + expect(btn).toBeDefined(); + }); + + it('should call confirmDiscard on discard confirmation', fakeAsync(() => { + mockDataService.delete.and.returnValue(observableOf(true)); + spyOn(component, 'reload'); + const btn = fixture.debugElement.query(By.css('.btn-danger')); + btn.nativeElement.click(); + fixture.detectChanges(); + + const confirmBtn: any = ((document as any).querySelector('.modal-footer .btn-danger')); + confirmBtn.click(); + + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(mockDataService.delete).toHaveBeenCalledWith(mockObject); + }); + + })); + + it('should display a success notification on delete success', async(() => { + spyOn((component as any).modalService, 'open').and.returnValue({result: Promise.resolve('ok')}); + mockDataService.delete.and.returnValue(observableOf(true)); + spyOn(component, 'reload'); + + component.confirmDiscard('ok'); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(notificationsServiceStub.success).toHaveBeenCalled(); + }); + })); + + it('should display an error notification on delete failure', async(() => { + spyOn((component as any).modalService, 'open').and.returnValue({result: Promise.resolve('ok')}); + mockDataService.delete.and.returnValue(observableOf(false)); + spyOn(component, 'reload'); + + component.confirmDiscard('ok'); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(notificationsServiceStub.error).toHaveBeenCalled(); + }); + })); +}); diff --git a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.ts b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.ts index c05eb557d2..cea4c3746e 100644 --- a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.ts +++ b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.ts @@ -7,50 +7,71 @@ import { TranslateService } from '@ngx-translate/core'; import { Workspaceitem } from '../../../core/submission/models/workspaceitem.model'; import { MyDSpaceActionsComponent } from '../mydspace-actions'; -import { SubmissionRestService } from '../../../core/submission/submission-rest.service'; import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service'; import { ResourceType } from '../../../core/shared/resource-type'; import { NotificationsService } from '../../notifications/notifications.service'; -import { NotificationOptions } from '../../notifications/models/notification-options.model'; +/** + * This component represents mydspace actions related to Workspaceitem object. + */ @Component({ selector: 'ds-workspaceitem-actions', styleUrls: ['./workspaceitem-actions.component.scss'], templateUrl: './workspaceitem-actions.component.html', }) - export class WorkspaceitemActionsComponent extends MyDSpaceActionsComponent { + + /** + * The workspaceitem object + */ @Input() object: Workspaceitem; + /** + * A boolean representing if a delete operation is pending + * @type {BehaviorSubject} + */ public processingDelete$ = new BehaviorSubject(false); + /** + * Initialize instance variables + * + * @param {Injector} injector + * @param {Router} router + * @param {NgbModal} modalService + * @param {NotificationsService} notificationsService + * @param {TranslateService} translate + */ constructor(protected injector: Injector, protected router: Router, - private modalService: NgbModal, - private notificationsService: NotificationsService, - private restService: SubmissionRestService, - private translate: TranslateService) { - super(ResourceType.Workspaceitem, injector, router); + protected modalService: NgbModal, + protected notificationsService: NotificationsService, + protected translate: TranslateService) { + super(ResourceType.Workspaceitem, injector, router, notificationsService, translate); } + /** + * Delete the target workspaceitem object + */ public confirmDiscard(content) { this.modalService.open(content).result.then( (result) => { if (result === 'ok') { this.processingDelete$.next(true); - this.restService.deleteById(this.object.id) - .subscribe(() => { - this.notificationsService.success(null, - this.translate.get('submission.workflow.tasks.generic.success'), - new NotificationOptions(5000, false)); + this.objectDataService.delete(this.object) + .subscribe((response: boolean) => { this.processingDelete$.next(false); - this.reload(); + this.handleActionResponse(response); }) } } ); } + /** + * Init the target object + * + * @param {Workspaceitem} object + */ initObjects(object: Workspaceitem) { this.object = object; } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index a5a426f1a7..00f357cda6 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -140,6 +140,7 @@ import { MetadataValuesComponent } from '../+item-page/field-components/metadata import { MetadataUriValuesComponent } from '../+item-page/field-components/metadata-uri-values/metadata-uri-values.component'; import { RoleDirective } from './roles/role.directive'; import { UserMenuComponent } from './auth-nav-menu/user-menu/user-menu.component'; +import { ClaimedTaskActionsReturnToPoolComponent } from './mydspace-actions/claimed-task/return-to-pool/claimed-task-actions-return-to-pool.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -234,6 +235,7 @@ const COMPONENTS = [ ClaimedTaskActionsComponent, ClaimedTaskActionsApproveComponent, ClaimedTaskActionsRejectComponent, + ClaimedTaskActionsReturnToPoolComponent, ItemActionsComponent, PoolTaskActionsComponent, WorkflowitemActionsComponent, diff --git a/src/app/shared/testing/eperson-mock.ts b/src/app/shared/testing/eperson-mock.ts index ef27f4983d..c822fc15d6 100644 --- a/src/app/shared/testing/eperson-mock.ts +++ b/src/app/shared/testing/eperson-mock.ts @@ -13,26 +13,30 @@ export const EPersonMock: EPerson = Object.assign(new EPerson(),{ id: 'testid', uuid: 'testid', type: 'eperson', - metadata: [ - { - key: 'dc.title', - language: null, - value: 'User Test' - }, - { - key: 'eperson.firstname', - language: null, - value: 'User' - }, - { - key: 'eperson.lastname', - language: null, - value: 'Test' - }, - { - key: 'eperson.language', - language: null, - value: 'en' - } - ] + metadata: { + 'dc.title': [ + { + language: null, + value: 'User Test' + } + ], + 'eperson.firstname': [ + { + language: null, + value: 'User' + } + ], + 'eperson.lastname': [ + { + language: null, + value: 'Test' + }, + ], + 'eperson.language': [ + { + language: null, + value: 'en' + }, + ] + } }); diff --git a/src/app/shared/testing/router-stub.ts b/src/app/shared/testing/router-stub.ts index 31c09c41e3..210ee91fdf 100644 --- a/src/app/shared/testing/router-stub.ts +++ b/src/app/shared/testing/router-stub.ts @@ -1,6 +1,7 @@ export class RouterStub { url: string; + routeReuseStrategy = {shouldReuseRoute: {}}; //noinspection TypeScriptUnresolvedFunction navigate = jasmine.createSpy('navigate'); parseUrl = jasmine.createSpy('parseUrl'); diff --git a/src/app/shared/testing/search-configuration-service-stub.ts b/src/app/shared/testing/search-configuration-service-stub.ts index 4c9d94c877..4c9402afb1 100644 --- a/src/app/shared/testing/search-configuration-service-stub.ts +++ b/src/app/shared/testing/search-configuration-service-stub.ts @@ -10,7 +10,10 @@ export class SearchConfigurationServiceStub { } getCurrentScope(a) { - return observableOf('test-id') + return observableOf('test-id'); } + getCurrentConfiguration(a) { + return observableOf(a); + } }