Added tests and comments

This commit is contained in:
Giuseppe Digilio
2019-04-02 15:18:06 +02:00
parent 607beb1bef
commit 16b8e91585
36 changed files with 1693 additions and 247 deletions

View File

@@ -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<MyDSpaceConfigurationValueType[]>}
* Emits the available configuration list
*/
public getAvailableConfigurationTypes(): Observable<MyDSpaceConfigurationValueType[]> {
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<SearchConfigurationOption[]>}
* Emits the select options list
*/
public getAvailableConfigurationOptions(): Observable<SearchConfigurationOption[]> {
return this.getAvailableConfigurationTypes().pipe(
first(),

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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<PaginatedList<MyDSpaceResult<DSpaceObject>>>;
/**
* 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;
}

View File

@@ -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;
}

View File

@@ -4,7 +4,7 @@
<select class="form-control"
[compareWith]="compare"
[(ngModel)]="selectedOption"
(change)="onSelect($event)">
(change)="onSelect()">
<option *ngFor="let option of configurationList;" [ngValue]="option.value">
{{option.label | translate}}
</option>

View File

@@ -1,103 +1,109 @@
// import { SearchService } from '../../search-service/search.service';
// import { async, ComponentFixture, TestBed } from '@angular/core/testing';
// import { SearchSettingsComponent } from '../../search-settings/search-settings.component';
// import { Observable } from 'rxjs/Observable';
// import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
// import { SortOptions } from '../../../core/cache/models/sort-options.model';
// import { TranslateModule } from '@ngx-translate/core';
// import { RouterTestingModule } from '@angular/router/testing';
// import { ActivatedRoute } from '@angular/router';
// import { SearchSidebarService } from '../../search-sidebar/search-sidebar.service';
// import { NO_ERRORS_SCHEMA } from '@angular/core';
// import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
// import { By } from '@angular/platform-browser';
//
// describe('SearchSettingsComponent', () => {
//
// let comp: SearchSettingsComponent;
// let fixture: ComponentFixture<SearchSettingsComponent>;
// let searchServiceObject: SearchService;
//
// const pagination: PaginationComponentOptions = new PaginationComponentOptions();
// pagination.id = 'search-results-pagination';
// pagination.currentPage = 1;
// pagination.pageSize = 10;
// const sort: SortOptions = new SortOptions();
// const mockResults = [ 'test', 'data' ];
// const searchServiceStub = {
// searchOptions: { pagination: pagination, sort: sort },
// search: () => mockResults
// };
// const queryParam = 'test query';
// const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
// const activatedRouteStub = {
// queryParams: Observable.of({
// query: queryParam,
// scope: scopeParam
// })
// };
//
// const sidebarService = {
// isCollapsed: Observable.of(true),
// collapse: () => this.isCollapsed = Observable.of(true),
// expand: () => this.isCollapsed = Observable.of(false)
// }
//
// beforeEach(async(() => {
// TestBed.configureTestingModule({
// imports: [ TranslateModule.forRoot(), RouterTestingModule.withRoutes([]) ],
// declarations: [ SearchSettingsComponent, EnumKeysPipe ],
// providers: [
// { provide: SearchService, useValue: searchServiceStub },
//
// { provide: ActivatedRoute, useValue: activatedRouteStub },
// {
// provide: SearchSidebarService,
// useValue: sidebarService
// },
// ],
// schemas: [ NO_ERRORS_SCHEMA ]
// }).compileComponents();
// }));
//
// beforeEach(() => {
// fixture = TestBed.createComponent(SearchSettingsComponent);
// comp = fixture.componentInstance;
//
// // SearchPageComponent test instance
// fixture.detectChanges();
// searchServiceObject = (comp as any).service;
// spyOn(comp, 'reloadRPP');
// spyOn(comp, 'reloadOrder');
// spyOn(searchServiceObject, 'search').and.callThrough();
//
// });
//
// it('it should show the order settings with the respective selectable options', () => {
// const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
// expect(orderSetting).toBeDefined();
// const childElements = orderSetting.query(By.css('.form-control')).children;
// expect(childElements.length).toEqual(2);
//
// });
//
// it('it should show the size settings with the respective selectable options', () => {
// const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings'));
// expect(pageSizeSetting).toBeDefined();
// const childElements = pageSizeSetting.query(By.css('.form-control')).children;
// expect(childElements.length).toEqual(7);
// });
//
// it('should have the proper order value selected by default', () => {
// const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
// const childElementToBeSelected = orderSetting.query(By.css('.form-control option[value="0"][selected="selected"]'))
// expect(childElementToBeSelected).toBeDefined();
// });
//
// it('should have the proper rpp value selected by default', () => {
// const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings'));
// const childElementToBeSelected = pageSizeSetting.query(By.css('.form-control option[value="10"][selected="selected"]'))
// expect(childElementToBeSelected).toBeDefined();
// });
//
// });
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { of as observableOf } from 'rxjs';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { SearchSwitchConfigurationComponent } from './search-switch-configuration.component';
import { MYDSPACE_ROUTE, SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service-stub';
import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader';
import { NavigationExtras, Router } from '@angular/router';
import { RouterStub } from '../../shared/testing/router-stub';
import { MyDSpaceConfigurationValueType } from '../../+my-dspace-page/my-dspace-configuration-value-type';
import { SearchService } from '../search-service/search.service';
describe('SearchSwitchConfigurationComponent', () => {
let comp: SearchSwitchConfigurationComponent;
let fixture: ComponentFixture<SearchSwitchConfigurationComponent>;
let searchConfService: SearchConfigurationServiceStub;
let select: any;
const searchServiceStub = jasmine.createSpyObj('SearchService', {
getSearchLink: jasmine.createSpy('getSearchLink')
});
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
}
})
],
declarations: [ SearchSwitchConfigurationComponent ],
providers: [
{ provide: Router, useValue: new RouterStub() },
{ provide: SearchService, useValue: searchServiceStub },
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
],
schemas: [ NO_ERRORS_SCHEMA ]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SearchSwitchConfigurationComponent);
comp = fixture.componentInstance;
searchConfService = TestBed.get(SEARCH_CONFIG_SERVICE);
spyOn(searchConfService, 'getCurrentConfiguration').and.returnValue(observableOf(MyDSpaceConfigurationValueType.Workspace));
comp.configurationList = [
{
value: MyDSpaceConfigurationValueType.Workspace,
label: 'workspace'
},
{
value: MyDSpaceConfigurationValueType.Workflow,
label: 'workflow'
},
];
// SearchSwitchConfigurationComponent test instance
fixture.detectChanges();
});
it('should init the current configuration name', () => {
expect(comp.selectedOption).toBe(MyDSpaceConfigurationValueType.Workspace);
});
it('should display select field properly', () => {
const selectField = fixture.debugElement.query(By.css('.form-control'));
expect(selectField).toBeDefined();
const childElements = selectField.children;
expect(childElements.length).toEqual(comp.configurationList.length);
});
it('should call onSelect method when selecting an option', () => {
fixture.whenStable().then(() => {
spyOn(comp, 'onSelect');
select = fixture.debugElement.query(By.css('select'));
const selectEl = select.nativeElement;
selectEl.value = selectEl.options[1].value; // <-- select a new value
selectEl.dispatchEvent(new Event('change'));
fixture.detectChanges();
expect(comp.onSelect).toHaveBeenCalled();
});
});
it('should navigate to the route when selecting an option', () => {
(comp as any).searchService.getSearchLink.and.returnValue(MYDSPACE_ROUTE);
comp.selectedOption = MyDSpaceConfigurationValueType.Workflow;
const navigationExtras: NavigationExtras = {
queryParams: {configuration: MyDSpaceConfigurationValueType.Workflow},
};
fixture.detectChanges();
comp.onSelect();
expect((comp as any).router.navigate).toHaveBeenCalledWith([MYDSPACE_ROUTE], navigationExtras);
});
});

View File

@@ -4,16 +4,20 @@ import { NavigationExtras, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { hasValue } from '../../shared/empty.util';
import { MYDSPACE_ROUTE, SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../search-service/search-configuration.service';
import { MyDSpaceConfigurationValueType } from '../../+my-dspace-page/my-dspace-configuration-value-type';
import { SearchConfigurationOption } from './search-configuration-option.model';
import { SearchService } from '../search-service/search.service';
@Component({
selector: 'ds-search-switch-configuration',
styleUrls: ['./search-switch-configuration.component.scss'],
templateUrl: './search-switch-configuration.component.html',
})
/**
* Represents a select that allow to switch over available search configurations
*/
export class SearchSwitchConfigurationComponent implements OnDestroy, OnInit {
/**
@@ -21,31 +25,53 @@ export class SearchSwitchConfigurationComponent implements OnDestroy, OnInit {
*/
@Input() configurationList: SearchConfigurationOption[] = [];
/**
* The selected option
*/
public selectedOption: string;
/**
* Subscription to unsubscribe from
*/
private sub: Subscription;
constructor(private router: Router,
private searchService: SearchService,
@Inject(SEARCH_CONFIG_SERVICE) private searchConfigService: SearchConfigurationService) {
}
/**
* Init current configuration
*/
ngOnInit() {
this.searchConfigService.getCurrentConfiguration('default')
.subscribe((currentConfiguration) => this.selectedOption = currentConfiguration);
}
onSelect(event: Event) {
/**
* Init current configuration
*/
onSelect() {
const navigationExtras: NavigationExtras = {
queryParams: {configuration: this.selectedOption},
};
this.router.navigate([MYDSPACE_ROUTE], navigationExtras);
this.router.navigate([this.searchService.getSearchLink()], navigationExtras);
}
/**
* Define the select 'compareWith' method to tell Angular how to compare the values
*
* @param item1
* @param item2
*/
compare(item1: MyDSpaceConfigurationValueType, item2: MyDSpaceConfigurationValueType) {
return item1 === item2;
}
/**
* Make sure the subscription is unsubscribed from when this component is destroyed
*/
ngOnDestroy() {
if (hasValue(this.sub)) {
this.sub.unsubscribe();

View File

@@ -47,7 +47,9 @@ export class EPerson extends DSpaceObject {
*/
public selfRegistered: boolean;
/** Getter to retrieve the EPerson's full name as a string */
/**
* Getter to retrieve the EPerson's full name as a string
*/
get name(): string {
return this.firstMetadataValue('eperson.firstname') + ' ' + this.firstMetadataValue('eperson.lastname');
}

View File

@@ -1,4 +1,4 @@
import { autoserialize, inheritSerialization } from 'cerialize';
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
import { WorkspaceitemSectionsObject } from './workspaceitem-sections.model';
@@ -17,6 +17,12 @@ export class NormalizedSubmissionObject<T extends DSpaceObject> extends Normaliz
@autoserialize
id: string;
/**
* The workspaceitem/workflowitem identifier
*/
@autoserializeAs(String, 'id')
uuid: string;
/**
* The workspaceitem/workflowitem last modified date
*/

View File

@@ -25,6 +25,11 @@ export abstract class SubmissionObject extends DSpaceObject implements Cacheable
*/
id: string;
/**
* The workspaceitem/workflowitem identifier
*/
uuid: string;
/**
* The workspaceitem/workflowitem last modified date
*/

View File

@@ -0,0 +1,151 @@
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { Store, StoreModule } from '@ngrx/store';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { UserMenuComponent } from './user-menu.component';
import { authReducer, AuthState } from '../../../core/auth/auth.reducer';
import { AuthTokenInfo } from '../../../core/auth/models/auth-token-info.model';
import { EPersonMock } from '../../testing/eperson-mock';
import { AppState } from '../../../app.reducer';
import { MockTranslateLoader } from '../../mocks/mock-translate-loader';
import { cold } from 'jasmine-marbles';
import { By } from '@angular/platform-browser';
describe('UserMenuComponent', () => {
let component: UserMenuComponent;
let fixture: ComponentFixture<UserMenuComponent>;
let deUserMenu: DebugElement;
let authState: AuthState;
let authStateLoading: AuthState;
function init() {
authState = {
authenticated: true,
loaded: true,
loading: false,
authToken: new AuthTokenInfo('test_token'),
user: EPersonMock
};
authStateLoading = {
authenticated: true,
loaded: true,
loading: true,
authToken: null,
user: EPersonMock
};
}
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot(authReducer),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
}
})
],
declarations: [
UserMenuComponent
],
schemas: [
NO_ERRORS_SCHEMA
]
}).compileComponents();
}));
beforeEach(() => {
init();
});
describe('when auth state is loading', () => {
beforeEach(inject([Store], (store: Store<AppState>) => {
store
.subscribe((state) => {
(state as any).core = Object.create({});
(state as any).core.auth = authStateLoading;
});
// create component and test fixture
fixture = TestBed.createComponent(UserMenuComponent);
// get test component from the fixture
component = fixture.componentInstance;
fixture.detectChanges();
deUserMenu = fixture.debugElement.query(By.css('div'));
}));
afterEach(() => {
fixture.destroy();
});
it('should init component properly', () => {
expect(component).toBeDefined();
expect(component.loading$).toBeObservable(cold('b', {
b: true
}));
expect(component.user$).toBeObservable(cold('c', {
c: EPersonMock
}));
expect(deUserMenu).toBeNull();
});
});
describe('when auth state is not loading', () => {
beforeEach(inject([Store], (store: Store<AppState>) => {
store
.subscribe((state) => {
(state as any).core = Object.create({});
(state as any).core.auth = authState;
});
// create component and test fixture
fixture = TestBed.createComponent(UserMenuComponent);
// get test component from the fixture
component = fixture.componentInstance;
fixture.detectChanges();
deUserMenu = fixture.debugElement.query(By.css('div'));
}));
afterEach(() => {
fixture.destroy();
});
it('should init component properly', () => {
expect(component).toBeDefined();
expect(component.loading$).toBeObservable(cold('b', {
b: false
}));
expect(component.user$).toBeObservable(cold('c', {
c: EPersonMock
}));
expect(deUserMenu).toBeDefined();
});
it('should display user name and email', () => {
const user = 'User Test (test@test.com)';
const span = deUserMenu.query(By.css('.dropdown-item-text'));
expect(span).toBeDefined();
expect(span.nativeElement.innerHTML).toBe(user);
})
});
});

View File

@@ -8,6 +8,9 @@ import { AppState } from '../../../app.reducer';
import { getAuthenticatedUser, isAuthenticationLoading } from '../../../core/auth/selectors';
import { MYDSPACE_ROUTE } from '../../../+my-dspace-page/my-dspace-page.component';
/**
* This component represents the user nav menu.
*/
@Component({
selector: 'ds-user-menu',
templateUrl: './user-menu.component.html',
@@ -36,6 +39,9 @@ export class UserMenuComponent implements OnInit {
constructor(private store: Store<AppState>) {
}
/**
* Initialize all instance variables
*/
ngOnInit(): void {
// set loading

View File

@@ -2,7 +2,7 @@
[className]="'btn btn-success ' + wrapperClass"
ngbTooltip="{{'submission.workflow.tasks.claimed.approve_help' | translate}}"
[disabled]="processingApprove"
(click)="click()">
(click)="confirmApprove()">
<span *ngIf="processingApprove"><i class='fas fa-circle-notch fa-spin'></i> {{'submission.workflow.tasks.generic.processing' | translate}}</span>
<span *ngIf="!processingApprove"><i class="fa fa-thumbs-up"></i> {{'submission.workflow.tasks.claimed.approve' | translate}}</span>
</button>

View File

@@ -0,0 +1,65 @@
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { ClaimedTaskActionsApproveComponent } from './claimed-task-actions-approve.component';
import { MockTranslateLoader } from '../../../mocks/mock-translate-loader';
let component: ClaimedTaskActionsApproveComponent;
let fixture: ComponentFixture<ClaimedTaskActionsApproveComponent>;
describe('ClaimedTaskActionsApproveComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
}
})
],
declarations: [ClaimedTaskActionsApproveComponent],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(ClaimedTaskActionsApproveComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ClaimedTaskActionsApproveComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
afterEach(() => {
fixture = null;
component = null;
});
it('should display approve button', () => {
const btn = fixture.debugElement.query(By.css('.btn-success'));
expect(btn).toBeDefined();
});
it('should display spin icon when approve is pending', () => {
component.processingApprove = true;
fixture.detectChanges();
const span = fixture.debugElement.query(By.css('.btn-success .fa-spin'));
expect(span).toBeDefined();
});
it('should emit approve event', () => {
spyOn(component.approve, 'emit');
component.confirmApprove();
fixture.detectChanges();
expect(component.approve.emit).toHaveBeenCalled();
});
});

View File

@@ -7,13 +7,26 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
})
export class ClaimedTaskActionsApproveComponent {
/**
* A boolean representing if a reject operation is pending
*/
@Input() processingApprove: boolean;
@Input() taskId: string;
/**
* CSS classes to append to reject button
*/
@Input() wrapperClass: string;
/**
* An event fired when a approve action is confirmed.
*/
@Output() approve: EventEmitter<any> = new EventEmitter<any>();
click() {
/**
* Emit approve event
*/
confirmApprove() {
this.approve.emit();
}
}

View File

@@ -8,23 +8,16 @@
</a>
<ds-claimed-task-actions-approve [processingApprove]="(processingApprove$ | async)"
[taskId]="object.id"
[wrapperClass]="'mt-1 mb-3'"
(approve)="approve()"></ds-claimed-task-actions-approve>
<ds-claimed-task-actions-reject [processingReject]="(processingReject$ | async)"
[taskId]="object.id"
[wrapperClass]="'mt-1 mb-3'"
(reject)="reject($event)"></ds-claimed-task-actions-reject>
<button type="button"
class="btn btn-secondary mt-1 mb-3"
ngbTooltip="{{'submission.workflow.tasks.claimed.return_help' | translate}}"
[disabled]="(processingReturnToPool$ | async)"
(click)="returnToPool()">
<span *ngIf="(processingReturnToPool$ | async)"><i class='fas fa-circle-notch fa-spin'></i> {{'submission.workflow.tasks.generic.processing' | translate}}</span>
<span *ngIf="!(processingReturnToPool$ | async)"><i class="fa fa-undo"></i> {{'submission.workflow.tasks.claimed.return' | translate}}</span>
</button>
<ds-claimed-task-actions-return-to-pool [processingReturnToPool]="(processingReturnToPool$ | async)"
[wrapperClass]="'mt-1 mb-3'"
(returnToPool)="returnToPool()"></ds-claimed-task-actions-return-to-pool>
<ds-message-board
[dso]="workflowitem$ | async"

View File

@@ -0,0 +1,269 @@
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 { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service';
import { ClaimedTaskActionsComponent } from './claimed-task-actions.component';
import { ClaimedTask } from '../../../core/tasks/models/claimed-task-object.model';
import { Workflowitem } from '../../../core/submission/models/workflowitem.model';
let component: ClaimedTaskActionsComponent;
let fixture: ComponentFixture<ClaimedTaskActionsComponent>;
let mockObject: ClaimedTask;
let notificationsServiceStub: NotificationsServiceStub;
let router: RouterStub;
const mockDataService = jasmine.createSpyObj('PoolTaskDataService', {
approveTask: jasmine.createSpy('approveTask'),
rejectTask: jasmine.createSpy('rejectTask'),
returnToPoolTask: jasmine.createSpy('returnToPoolTask'),
});
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 ClaimedTask(), { workflowitem: observableOf(rdWorkflowitem), id: '1234' });
describe('ClaimedTaskActionsComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
}
})
],
declarations: [ClaimedTaskActionsComponent],
providers: [
{ provide: Injector, useValue: {} },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: Router, useValue: new RouterStub() },
{ provide: ClaimedTaskDataService, useValue: mockDataService },
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(ClaimedTaskActionsComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ClaimedTaskActionsComponent);
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 edit task button', () => {
const btn = fixture.debugElement.query(By.css('.btn-info'));
expect(btn).toBeDefined();
});
it('should call approveTask method when approving a task', fakeAsync(() => {
spyOn(component, 'reload');
mockDataService.approveTask.and.returnValue(observableOf({hasSucceeded: true}));
component.approve();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(mockDataService.approveTask).toHaveBeenCalledWith(mockObject.id);
});
}));
it('should display a success notification on approve success', async(() => {
spyOn(component, 'reload');
mockDataService.approveTask.and.returnValue(observableOf({hasSucceeded: true}));
component.approve();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(notificationsServiceStub.success).toHaveBeenCalled();
});
}));
it('should reload page on approve success', async(() => {
spyOn(router, 'navigateByUrl');
router.url = 'test.url/test';
mockDataService.approveTask.and.returnValue(observableOf({hasSucceeded: true}));
component.approve();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(router.navigateByUrl).toHaveBeenCalledWith('test.url/test');
});
}));
it('should display an error notification on approve failure', async(() => {
mockDataService.approveTask.and.returnValue(observableOf({hasSucceeded: false}));
component.approve();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(notificationsServiceStub.error).toHaveBeenCalled();
});
}));
it('should call rejectTask method when rejecting a task', fakeAsync(() => {
spyOn(component, 'reload');
mockDataService.rejectTask.and.returnValue(observableOf({hasSucceeded: true}));
component.reject('test reject');
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(mockDataService.rejectTask).toHaveBeenCalledWith('test reject', mockObject.id);
});
}));
it('should display a success notification on reject success', async(() => {
spyOn(component, 'reload');
mockDataService.rejectTask.and.returnValue(observableOf({hasSucceeded: true}));
component.reject('test reject');
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(notificationsServiceStub.success).toHaveBeenCalled();
});
}));
it('should reload page on reject success', async(() => {
spyOn(router, 'navigateByUrl');
router.url = 'test.url/test';
mockDataService.rejectTask.and.returnValue(observableOf({hasSucceeded: true}));
component.reject('test reject');
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(router.navigateByUrl).toHaveBeenCalledWith('test.url/test');
});
}));
it('should display an error notification on reject failure', async(() => {
mockDataService.rejectTask.and.returnValue(observableOf({hasSucceeded: false}));
component.reject('test reject');
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(notificationsServiceStub.error).toHaveBeenCalled();
});
}));
it('should call returnToPoolTask method when returning a task to pool', fakeAsync(() => {
spyOn(component, 'reload');
mockDataService.returnToPoolTask.and.returnValue(observableOf({hasSucceeded: true}));
component.returnToPool();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(mockDataService.returnToPoolTask).toHaveBeenCalledWith( mockObject.id);
});
}));
it('should display a success notification on return to pool success', async(() => {
spyOn(component, 'reload');
mockDataService.returnToPoolTask.and.returnValue(observableOf({hasSucceeded: true}));
component.returnToPool();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(notificationsServiceStub.success).toHaveBeenCalled();
});
}));
it('should reload page on return to pool success', async(() => {
spyOn(router, 'navigateByUrl');
router.url = 'test.url/test';
mockDataService.returnToPoolTask.and.returnValue(observableOf({hasSucceeded: true}));
component.returnToPool();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(router.navigateByUrl).toHaveBeenCalledWith('test.url/test');
});
}));
it('should display an error notification on return to pool failure', async(() => {
mockDataService.returnToPoolTask.and.returnValue(observableOf({hasSucceeded: false}));
component.returnToPool();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(notificationsServiceStub.error).toHaveBeenCalled();
});
}));
});

View File

@@ -1,4 +1,4 @@
import { ChangeDetectorRef, Component, Injector, Input, OnInit } from '@angular/core';
import { Component, Injector, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { BehaviorSubject, Observable } from 'rxjs';
@@ -8,41 +8,75 @@ import { TranslateService } from '@ngx-translate/core';
import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service';
import { ClaimedTask } from '../../../core/tasks/models/claimed-task-object.model';
import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-response';
import { NotificationsService } from '../../notifications/notifications.service';
import { NotificationOptions } from '../../notifications/models/notification-options.model';
import { isNotUndefined } from '../../empty.util';
import { Workflowitem } from '../../../core/submission/models/workflowitem.model';
import { RemoteData } from '../../../core/data/remote-data';
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 ClaimedTask object.
*/
@Component({
selector: 'ds-claimed-task-actions',
styleUrls: ['./claimed-task-actions.component.scss'],
templateUrl: './claimed-task-actions.component.html',
})
export class ClaimedTaskActionsComponent extends MyDSpaceActionsComponent<ClaimedTask, ClaimedTaskDataService> implements OnInit {
/**
* The ClaimedTask object
*/
@Input() object: ClaimedTask;
/**
* The workflowitem object that belonging to the ClaimedTask object
*/
public workflowitem$: Observable<Workflowitem>;
/**
* A boolean representing if an approve operation is pending
*/
public processingApprove$ = new BehaviorSubject<boolean>(false);
/**
* A boolean representing if a reject operation is pending
*/
public processingReject$ = new BehaviorSubject<boolean>(false);
/**
* A boolean representing if a return to pool operation is pending
*/
public processingReturnToPool$ = new BehaviorSubject<boolean>(false);
/**
* Initialize instance variables
*
* @param {Injector} injector
* @param {Router} router
* @param {NotificationsService} notificationsService
* @param {TranslateService} translate
*/
constructor(protected injector: Injector,
protected router: Router,
private cd: ChangeDetectorRef,
private notificationsService: NotificationsService,
private translate: TranslateService) {
super(ResourceType.ClaimedTask, injector, router)
protected notificationsService: NotificationsService,
protected translate: TranslateService) {
super(ResourceType.ClaimedTask, injector, router, notificationsService, translate);
}
/**
* Initialize objects
*/
ngOnInit() {
this.initObjects(this.object);
}
/**
* Init the ClaimedTask and Workflowitem objects
*
* @param {PoolTask} object
*/
initObjects(object: ClaimedTask) {
this.object = object;
this.workflowitem$ = (this.object.workflowitem as Observable<RemoteData<Workflowitem>>).pipe(
@@ -50,44 +84,40 @@ export class ClaimedTaskActionsComponent extends MyDSpaceActionsComponent<Claime
map((rd: RemoteData<Workflowitem>) => rd.payload));
}
/**
* Approve the task.
*/
approve() {
this.processingApprove$.next(true);
this.objectDataService.approveTask(this.object.id)
.subscribe((res: ProcessTaskResponse) => {
this.responseHandle(res);
this.processingApprove$.next(false);
this.handleActionResponse(res.hasSucceeded);
});
}
/**
* Reject the task.
*/
reject(reason) {
this.processingReject$.next(true);
this.objectDataService.rejectTask(reason, this.object.id)
.subscribe((res: ProcessTaskResponse) => {
this.responseHandle(res);
this.processingReject$.next(false);
this.handleActionResponse(res.hasSucceeded);
});
}
/**
* Return task to the pool.
*/
returnToPool() {
this.processingReturnToPool$.next(true);
this.objectDataService.returnToPoolTask(this.object.id)
.subscribe((res: ProcessTaskResponse) => {
this.responseHandle(res);
this.processingReturnToPool$.next(false);
this.handleActionResponse(res.hasSucceeded);
});
}
private responseHandle(res: ProcessTaskResponse) {
this.processingApprove$.next(false);
this.processingReject$.next(false);
this.processingReturnToPool$.next(false);
if (res.hasSucceeded) {
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));
}
}
}

View File

@@ -21,14 +21,12 @@
<div class="alert alert-info" role="alert">
{{'submission.workflow.tasks.claimed.reject.reason.info' | translate}}
</div>
<form (ngSubmit)="click(rejectModal);" [formGroup]="rejectForm" >
<textarea
style="width: 100%"
<form (ngSubmit)="confirmReject(rejectModal);" [formGroup]="rejectForm" >
<textarea style="width: 100%"
formControlName="reason"
rows="4"
placeholder="{{'submission.workflow.tasks.claimed.reject.reason.placeholder' | translate}}"></textarea>
<button
id="btn-chat"
<button id="btn-chat"
class="btn btn-danger btn-lg btn-block mt-3"
[disabled]="!rejectForm.valid || processingReject"
type="submit">

View File

@@ -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<ClaimedTaskActionsRejectComponent>;
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();
});
}));
});

View File

@@ -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<string> = new EventEmitter<string>();
/**
* 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);
}
}

View File

@@ -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<ItemActionsComponent>;
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);
});
});

View File

@@ -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<Item, ItemDataService> {
/**
* 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;
}

View File

@@ -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<T extends CacheableObject, TService extends DataService<T>> {
public getConstructor(type: ResourceType): TService {
switch (type) {
case ResourceType.Item: {
return ItemDataService as any;
}
case ResourceType.Workspaceitem: {
return WorkspaceitemDataService as any;
}

View File

@@ -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<T extends DSpaceObject, TService extends DataService<T>> {
/**
* 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<T, TService>();
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<T extends DSpaceObject, TService
this.router.navigateByUrl(url);
}
refresh() {
// override the object with a refreshed one
/**
* Override the target object with a refreshed one
*/
refresh(): void {
// find object by id
this.objectDataService.findById(this.object.id).pipe(
find((rd: RemoteData<T>) => rd.hasSucceeded)
).subscribe((rd: RemoteData<T>) => {
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));
}
}
}

View File

@@ -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<PoolTaskActionsComponent>;
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();
});
}));
});

View File

@@ -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<PoolTask, PoolTaskDataService> {
/**
* The PoolTask object
*/
@Input() object: PoolTask;
/**
* A boolean representing if a claim operation is pending
* @type {BehaviorSubject<boolean>}
*/
public processingClaim$ = new BehaviorSubject<boolean>(false);
/**
* The workflowitem object that belonging to the PoolTask object
*/
public workflowitem$: Observable<Workflowitem>;
/**
* 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<RemoteData<Workflowitem>>).pipe(
@@ -46,27 +75,15 @@ export class PoolTaskActionsComponent extends MyDSpaceActionsComponent<PoolTask,
map((rd: RemoteData<Workflowitem>) => 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));
}
}
}

View File

@@ -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<WorkflowitemActionsComponent>;
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);
});
});

View File

@@ -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<Workflowitem, WorkflowitemDataService> {
/**
* 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;
}

View File

@@ -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<WorkspaceitemActionsComponent>;
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();
});
}));
});

View File

@@ -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<Workspaceitem, WorkspaceitemDataService> {
/**
* The workspaceitem object
*/
@Input() object: Workspaceitem;
/**
* A boolean representing if a delete operation is pending
* @type {BehaviorSubject<boolean>}
*/
public processingDelete$ = new BehaviorSubject<boolean>(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;
}

View File

@@ -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,

View File

@@ -13,26 +13,30 @@ export const EPersonMock: EPerson = Object.assign(new EPerson(),{
id: 'testid',
uuid: 'testid',
type: 'eperson',
metadata: [
metadata: {
'dc.title': [
{
key: 'dc.title',
language: null,
value: 'User Test'
},
}
],
'eperson.firstname': [
{
key: 'eperson.firstname',
language: null,
value: 'User'
},
}
],
'eperson.lastname': [
{
key: 'eperson.lastname',
language: null,
value: 'Test'
},
],
'eperson.language': [
{
key: 'eperson.language',
language: null,
value: 'en'
}
},
]
}
});

View File

@@ -1,6 +1,7 @@
export class RouterStub {
url: string;
routeReuseStrategy = {shouldReuseRoute: {}};
//noinspection TypeScriptUnresolvedFunction
navigate = jasmine.createSpy('navigate');
parseUrl = jasmine.createSpy('parseUrl');

View File

@@ -10,7 +10,10 @@ export class SearchConfigurationServiceStub {
}
getCurrentScope(a) {
return observableOf('test-id')
return observableOf('test-id');
}
getCurrentConfiguration(a) {
return observableOf(a);
}
}