[CSTPER-3620] Workflow Actions refresh entire MyDSpace page instead of just WorkflowItem

Task Service implementation.
ReloadableAction abstraction.
This commit is contained in:
Alessandro Martelli
2020-12-18 12:19:09 +01:00
parent e867badcb5
commit e32c86f127
56 changed files with 1489 additions and 287 deletions

View File

@@ -21,6 +21,8 @@ import { ItemSearchResultListElementSubmissionComponent } from '../shared/object
import { WorkflowItemSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component';
import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component';
import { CollectionSelectorComponent } from './collection-selector/collection-selector.component';
import { ClaimedApprovedSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-approved-search-result/claimed-approved-search-result-list-element.component';
import { ClaimedDeclinedSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component';
@NgModule({
imports: [
@@ -34,6 +36,8 @@ import { CollectionSelectorComponent } from './collection-selector/collection-se
WorkspaceItemSearchResultListElementComponent,
WorkflowItemSearchResultListElementComponent,
ClaimedSearchResultListElementComponent,
ClaimedApprovedSearchResultListElementComponent,
ClaimedDeclinedSearchResultListElementComponent,
PoolSearchResultListElementComponent,
ItemSearchResultDetailElementComponent,
WorkspaceItemSearchResultDetailElementComponent,
@@ -53,6 +57,8 @@ import { CollectionSelectorComponent } from './collection-selector/collection-se
WorkspaceItemSearchResultListElementComponent,
WorkflowItemSearchResultListElementComponent,
ClaimedSearchResultListElementComponent,
ClaimedApprovedSearchResultListElementComponent,
ClaimedDeclinedSearchResultListElementComponent,
PoolSearchResultListElementComponent,
ItemSearchResultDetailElementComponent,
WorkspaceItemSearchResultDetailElementComponent,

View File

@@ -8,9 +8,16 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { CoreState } from '../core.reducers';
import { ClaimedTaskDataService } from './claimed-task-data.service';
import { of as observableOf } from 'rxjs/internal/observable/of';
import { FindListOptions } from '../data/request.models';
import { RequestParam } from '../cache/models/request-param.model';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
describe('ClaimedTaskDataService', () => {
let scheduler: TestScheduler;
let service: ClaimedTaskDataService;
let options: HttpOptions;
const taskEndpoint = 'https://rest.api/task';
@@ -45,6 +52,7 @@ describe('ClaimedTaskDataService', () => {
}
beforeEach(() => {
scheduler = getTestScheduler();
service = initTestService();
options = Object.create({});
let headers = new HttpHeaders();
@@ -68,6 +76,24 @@ describe('ClaimedTaskDataService', () => {
});
});
describe('claimTask', () => {
it('should call postToEndpoint method', () => {
spyOn(service, 'postToEndpoint').and.returnValue(observableOf(null));
scheduler.schedule(() => service.claimTask('scopeId', 'poolTaskHref').subscribe());
scheduler.flush();
const postToEndpointOptions: HttpOptions = Object.create({});
let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'text/uri-list');
postToEndpointOptions.headers = headers;
expect(service.postToEndpoint).toHaveBeenCalledWith(linkPath, 'poolTaskHref', null, postToEndpointOptions);
});
});
describe('returnToPoolTask', () => {
it('should call deleteById method', () => {
const scopeId = '1234';
@@ -79,4 +105,21 @@ describe('ClaimedTaskDataService', () => {
expect(service.deleteById).toHaveBeenCalledWith(linkPath, scopeId, options);
});
});
describe('findByItem', () => {
it('should call searchTask method', () => {
spyOn(service, 'searchTask').and.returnValue(observableOf(null));
scheduler.schedule(() => service.findByItem('a0db0fde-1d12-4d43-bd0d-0f43df8d823c').subscribe());
scheduler.flush();
const findListOptions = new FindListOptions();
findListOptions.searchParams = [
new RequestParam('uuid', 'a0db0fde-1d12-4d43-bd0d-0f43df8d823c')
];
expect(service.searchTask).toHaveBeenCalledWith('findByItem', findListOptions, followLink('workflowitem'));
});
});
});

View File

@@ -1,4 +1,4 @@
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
@@ -15,6 +15,11 @@ import { ClaimedTask } from './models/claimed-task-object.model';
import { CLAIMED_TASK } from './models/claimed-task-object.resource-type';
import { ProcessTaskResponse } from './models/process-task-response';
import { TasksService } from './tasks.service';
import { RemoteData } from '../data/remote-data';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { FindListOptions } from '../data/request.models';
import { RequestParam } from '../cache/models/request-param.model';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
/**
* The service handling all REST requests for ClaimedTask
@@ -54,6 +59,22 @@ export class ClaimedTaskDataService extends TasksService<ClaimedTask> {
super();
}
/**
* Make a request to claim the given task
*
* @param scopeId
* The task id
* @return {Observable<ProcessTaskResponse>}
* Emit the server response
*/
public claimTask(scopeId: string, poolTaskHref: string): Observable<ProcessTaskResponse> {
const options: HttpOptions = Object.create({});
let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'text/uri-list');
options.headers = headers;
return this.postToEndpoint(this.linkPath, poolTaskHref, null, options);
}
/**
* Make a request for the given task
*
@@ -80,4 +101,19 @@ export class ClaimedTaskDataService extends TasksService<ClaimedTask> {
return this.deleteById(this.linkPath, scopeId, this.makeHttpOptions());
}
/**
* Search a claimed task by item uuid.
* @param uuid
* The item uuid
* @return {Observable<RemoteData<ClaimedTask>>}
* The server response
*/
public findByItem(uuid: string): Observable<RemoteData<ClaimedTask>> {
const options = new FindListOptions();
options.searchParams = [
new RequestParam('uuid', uuid)
];
return this.searchTask('findByItem', options, followLink('workflowitem'));
}
}

View File

@@ -8,9 +8,16 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { CoreState } from '../core.reducers';
import { PoolTaskDataService } from './pool-task-data.service';
import { getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing';
import { of as observableOf } from 'rxjs/internal/observable/of';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { FindListOptions } from '../data/request.models';
import { RequestParam } from '../cache/models/request-param.model';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
describe('PoolTaskDataService', () => {
let scheduler: TestScheduler;
let service: PoolTaskDataService;
let options: HttpOptions;
const taskEndpoint = 'https://rest.api/task';
@@ -45,6 +52,7 @@ describe('PoolTaskDataService', () => {
}
beforeEach(() => {
scheduler = getTestScheduler();
service = initTestService();
options = Object.create({});
let headers = new HttpHeaders();
@@ -52,14 +60,33 @@ describe('PoolTaskDataService', () => {
options.headers = headers;
});
describe('claimTask', () => {
describe('findByItem', () => {
it('should call postToEndpoint method', () => {
spyOn(service, 'postToEndpoint');
const scopeId = '1234';
service.claimTask(scopeId);
it('should call searchTask method', () => {
spyOn(service, 'searchTask').and.returnValue(observableOf(null));
scheduler.schedule(() => service.findByItem('a0db0fde-1d12-4d43-bd0d-0f43df8d823c').subscribe());
scheduler.flush();
const findListOptions = new FindListOptions();
findListOptions.searchParams = [
new RequestParam('uuid', 'a0db0fde-1d12-4d43-bd0d-0f43df8d823c')
];
expect(service.searchTask).toHaveBeenCalledWith('findByItem', findListOptions, followLink('workflowitem'));
});
});
describe('getPoolTaskEndpointById', () => {
it('should call getEndpointById method', () => {
spyOn(service, 'getEndpointById').and.returnValue(observableOf(null));
scheduler.schedule(() => service.getPoolTaskEndpointById('a0db0fde-1d12-4d43-bd0d-0f43df8d823c').subscribe());
scheduler.flush();
expect(service.getEndpointById).toHaveBeenCalledWith('a0db0fde-1d12-4d43-bd0d-0f43df8d823c');
expect(service.postToEndpoint).toHaveBeenCalledWith(linkPath, {}, scopeId, options);
});
});
});

View File

@@ -13,8 +13,11 @@ import { RequestService } from '../data/request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { PoolTask } from './models/pool-task-object.model';
import { POOL_TASK } from './models/pool-task-object.resource-type';
import { ProcessTaskResponse } from './models/process-task-response';
import { TasksService } from './tasks.service';
import { RemoteData } from '../data/remote-data';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { FindListOptions } from '../data/request.models';
import { RequestParam } from '../cache/models/request-param.model';
/**
* The service handling all REST requests for PoolTask
@@ -56,14 +59,30 @@ export class PoolTaskDataService extends TasksService<PoolTask> {
}
/**
* Make a request to claim the given task
*
* @param scopeId
* The task id
* @return {Observable<ProcessTaskResponse>}
* Emit the server response
* Search a pool task by item uuid.
* @param uuid
* The item uuid
* @return {Observable<RemoteData<ClaimedTask>>}
* The server response
*/
public claimTask(scopeId: string): Observable<ProcessTaskResponse> {
return this.postToEndpoint(this.linkPath, {}, scopeId, this.makeHttpOptions());
public findByItem(uuid: string): Observable<RemoteData<PoolTask>> {
const options = new FindListOptions();
options.searchParams = [
new RequestParam('uuid', uuid)
];
return this.searchTask('findByItem', options, followLink('workflowitem'));
}
/**
* Get the Href of the pool task
*
* @param poolTaskId
* the poolTask id
* @return {Observable<string>>}
* the Href
*/
public getPoolTaskEndpointById(poolTaskId): Observable<string> {
return this.getEndpointById(poolTaskId);
}
}

View File

@@ -4,7 +4,7 @@ import { TestScheduler } from 'rxjs/testing';
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { TasksService } from './tasks.service';
import { RequestService } from '../data/request.service';
import { TaskDeleteRequest, TaskPostRequest } from '../data/request.models';
import { FindListOptions, TaskDeleteRequest, TaskPostRequest } from '../data/request.models';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
import { TaskObject } from './models/task-object.model';
@@ -17,8 +17,8 @@ import { HttpClient, HttpHeaders } from '@angular/common/http';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
import { ChangeAnalyzer } from '../data/change-analyzer';
import { compare, Operation } from 'fast-json-patch';
import { of as observableOf } from 'rxjs/internal/observable/of';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
const LINK_NAME = 'test';
@@ -57,7 +57,7 @@ describe('TasksService', () => {
const linkPath = 'testTask';
const requestService = getMockRequestService();
const halService: any = new HALEndpointServiceStub(taskEndpoint);
const rdbService = getMockRemoteDataBuildService();
const rdbService = {} as RemoteDataBuildService;
const notificationsService = {} as NotificationsService;
const http = {} as HttpClient;
const comparator = new DummyChangeAnalyzer() as any;
@@ -117,4 +117,38 @@ describe('TasksService', () => {
});
});
describe('searchTask', () => {
it('should call findByHref with the href generated by getSearchByHref', () => {
spyOn(service, 'getSearchByHref').and.returnValue(observableOf('generatedHref'));
spyOn(service, 'findByHref').and.returnValue(null);
const followLinks = {};
const options = new FindListOptions();
options.searchParams = [];
scheduler.schedule(() => service.searchTask('method', options, followLinks as any).subscribe());
scheduler.flush();
expect(service.getSearchByHref).toHaveBeenCalledWith('method', options, followLinks as any);
expect(service.findByHref).toHaveBeenCalledWith('generatedHref');
});
});
describe('getEndpointById', () => {
it('should call halService.getEndpoint and then getEndpointByIDHref', () => {
spyOn(halService, 'getEndpoint').and.returnValue(observableOf('generatedHref'));
spyOn(service, 'getEndpointByIDHref').and.returnValue(null);
scheduler.schedule(() => service.getEndpointById('scopeId').subscribe());
scheduler.flush();
expect(halService.getEndpoint).toHaveBeenCalledWith(service.getLinkPath());
expect(service.getEndpointByIDHref).toHaveBeenCalledWith('generatedHref', 'scopeId');
});
});
});

View File

@@ -1,21 +1,24 @@
import { HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { distinctUntilChanged, filter, flatMap, map, tap } from 'rxjs/operators';
import { merge as observableMerge, Observable, of as observableOf } from 'rxjs';
import { distinctUntilChanged, filter, find, flatMap, map, mergeMap, switchMap, tap } from 'rxjs/operators';
import { DataService } from '../data/data.service';
import {
DeleteRequest,
FindListOptions,
PostRequest,
TaskDeleteRequest,
TaskPostRequest
} from '../data/request.models';
import { isNotEmpty } from '../../shared/empty.util';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { ProcessTaskResponse } from './models/process-task-response';
import { getFirstCompletedRemoteData } from '../shared/operators';
import {getFirstCompletedRemoteData, getResponseFromEntry} from '../shared/operators';
import { ErrorResponse, MessageResponse, RestResponse } from '../cache/response.models';
import { CacheableObject } from '../cache/object-cache.reducer';
import { RemoteData } from '../data/remote-data';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import {HttpOptions} from '../dspace-rest/dspace-rest.service';
/**
* An abstract class that provides methods to handle task requests.
@@ -51,7 +54,7 @@ export abstract class TasksService<T extends CacheableObject> extends DataServic
* @param resourceID
* The identifier for the object
*/
protected getEndpointByIDHref(endpoint, resourceID): string {
getEndpointByIDHref(endpoint, resourceID): string {
return isNotEmpty(resourceID) ? `${endpoint}/${resourceID}` : `${endpoint}`;
}
@@ -95,10 +98,7 @@ export abstract class TasksService<T extends CacheableObject> extends DataServic
*/
public deleteById(linkPath: string, scopeId: string, options?: HttpOptions): Observable<ProcessTaskResponse> {
const requestId = this.requestService.generateRequestId();
return this.halService.getEndpoint(linkPath || this.linkPath).pipe(
filter((href: string) => isNotEmpty(href)),
distinctUntilChanged(),
map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId)),
return this.getEndpointById(scopeId, linkPath).pipe(
map((endpointURL: string) => new TaskDeleteRequest(requestId, endpointURL, null, options)),
tap((request: DeleteRequest) => this.requestService.configure(request)),
flatMap((request: DeleteRequest) => this.fetchRequest(requestId)),
@@ -115,4 +115,33 @@ export abstract class TasksService<T extends CacheableObject> extends DataServic
options.headers = headers;
return options;
}
/**
* Get the endpoint of a task by scopeId.
* @param linkPath
* @param scopeId
*/
public getEndpointById(scopeId: string, linkPath?: string): Observable<string> {
return this.halService.getEndpoint(linkPath || this.linkPath).pipe(
filter((href: string) => isNotEmpty(href)),
distinctUntilChanged(),
map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId)));
}
/**
* Search a task.
* @param searchMethod
* the search method
* @param options
* the find list options
* @param linksToFollow
* links to follow
*/
public searchTask(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<RemoteData<T>> {
const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow);
return hrefObs.pipe(
find((href: string) => hasValue(href)),
switchMap((href) => this.findByHref(href))
);
}
}

View File

@@ -1,8 +1,20 @@
import { EventEmitter, Input, Output } from '@angular/core';
import { Injector } from '@angular/core';
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service';
import { ProcessTaskResponse } from '../../../../core/tasks/models/process-task-response';
import { DSpaceObject} from '../../../../core/shared/dspace-object.model';
import { Router} from '@angular/router';
import { NotificationsService} from '../../../notifications/notifications.service';
import { TranslateService} from '@ngx-translate/core';
import { SearchService} from '../../../../core/shared/search/search.service';
import { RequestService} from '../../../../core/data/request.service';
import { Observable} from 'rxjs';
import { RemoteData} from '../../../../core/data/remote-data';
import { WorkflowItem} from '../../../../core/submission/models/workflowitem.model';
import { map, switchMap } from 'rxjs/operators';
import { CLAIMED_TASK } from '../../../../core/tasks/models/claimed-task-object.resource-type';
import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
import { Item } from '../../../../core/shared/item.model';
import { MyDSpaceReloadableActionsComponent } from '../../mydspace-reloadable-actions';
/**
* Abstract component for rendering a claimed task's action
@@ -11,28 +23,34 @@ import { ProcessTaskResponse } from '../../../../core/tasks/models/process-task-
* - Add a @rendersWorkflowTaskOption annotation to your component providing the same enum value
* - Optionally overwrite createBody if the request body requires more than just the option
*/
export abstract class ClaimedTaskActionsAbstractComponent {
export abstract class ClaimedTaskActionsAbstractComponent extends MyDSpaceReloadableActionsComponent<ClaimedTask, ClaimedTaskDataService> {
/**
* The workflow task option the child component represents
*/
abstract option: string;
/**
* The Claimed Task to display an action for
*/
@Input() object: ClaimedTask;
object: ClaimedTask;
/**
* Emits the success or failure of a processed action
* Anchor used to reload the pool task.
*/
@Output() processCompleted: EventEmitter<boolean> = new EventEmitter<boolean>();
itemUuid: string;
protected constructor(protected injector: Injector,
protected router: Router,
protected notificationsService: NotificationsService,
protected translate: TranslateService,
protected searchService: SearchService,
protected requestService: RequestService) {
super(CLAIMED_TASK, injector, router, notificationsService, translate, searchService, requestService);
}
/**
* A boolean representing if the operation is pending
* Submit the action on the claimed object.
*/
processing$ = new BehaviorSubject<boolean>(false);
constructor(protected claimedTaskService: ClaimedTaskDataService) {
submitTask() {
this.startActionExecution().subscribe();
}
/**
@@ -45,17 +63,29 @@ export abstract class ClaimedTaskActionsAbstractComponent {
};
}
/**
* Submit the task for this option
* While the task is submitting, processing$ is set to true and processCompleted emits the response's status when
* completed
*/
submitTask() {
this.processing$.next(true);
this.claimedTaskService.submitTask(this.object.id, this.createbody())
.subscribe((res: ProcessTaskResponse) => {
this.processing$.next(false);
this.processCompleted.emit(res.hasSucceeded);
});
reloadObjectExecution(): Observable<RemoteData<DSpaceObject> | DSpaceObject> {
return this.objectDataService.findByItem(this.itemUuid as string);
}
actionExecution(): Observable<any> {
return this.objectDataService.submitTask(this.object.id, this.createbody())
}
initObjects(object: ClaimedTask) {
this.object = object;
}
/**
* Retrieve the itemUuid.
*/
initReloadAnchor() {
(this.object as any).workflowitem.pipe(
getFirstSucceededRemoteDataPayload(),
switchMap((workflowItem: WorkflowItem) => workflowItem.item.pipe(getFirstSucceededRemoteDataPayload())
))
.subscribe((item: Item) => {
this.itemUuid = item.uuid;
})
}
}

View File

@@ -1,18 +1,33 @@
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { ChangeDetectionStrategy, Injector, 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 { of as observableOf } from 'rxjs';
import { of, of as observableOf } from 'rxjs';
import { ClaimedTaskActionsApproveComponent } from './claimed-task-actions-approve.component';
import { TranslateLoaderMock } from '../../../mocks/translate-loader.mock';
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 { getMockSearchService } from '../../../mocks/search-service.mock';
import { getMockRequestService } from '../../../mocks/request.service.mock';
import { PoolTaskDataService } from '../../../../core/tasks/pool-task-data.service';
import { NotificationsService } from '../../../notifications/notifications.service';
import { NotificationsServiceStub } from '../../../testing/notifications-service.stub';
import { Router } from '@angular/router';
import { RouterStub } from '../../../testing/router.stub';
import { SearchService } from '../../../../core/shared/search/search.service';
import { RequestService } from '../../../../core/data/request.service';
let component: ClaimedTaskActionsApproveComponent;
let fixture: ComponentFixture<ClaimedTaskActionsApproveComponent>;
const searchService = getMockSearchService();
const requestService = getMockRequestService();
let mockPoolTaskDataService: PoolTaskDataService;
describe('ClaimedTaskActionsApproveComponent', () => {
const object = Object.assign(new ClaimedTask(), { id: 'claimed-task-1' });
const claimedTaskService = jasmine.createSpyObj('claimedTaskService', {
@@ -20,6 +35,7 @@ describe('ClaimedTaskActionsApproveComponent', () => {
});
beforeEach(async(() => {
mockPoolTaskDataService = new PoolTaskDataService(null, null, null, null, null, null, null, null);
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
@@ -30,7 +46,13 @@ describe('ClaimedTaskActionsApproveComponent', () => {
})
],
providers: [
{ provide: ClaimedTaskDataService, useValue: claimedTaskService }
{ provide: ClaimedTaskDataService, useValue: claimedTaskService },
{ provide: Injector, useValue: {} },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: Router, useValue: new RouterStub() },
{ provide: SearchService, useValue: searchService },
{ provide: RequestService, useValue: requestService },
{ provide: PoolTaskDataService, useValue: mockPoolTaskDataService },
],
declarations: [ClaimedTaskActionsApproveComponent],
schemas: [NO_ERRORS_SCHEMA]
@@ -43,6 +65,7 @@ describe('ClaimedTaskActionsApproveComponent', () => {
fixture = TestBed.createComponent(ClaimedTaskActionsApproveComponent);
component = fixture.componentInstance;
component.object = object;
spyOn(component, 'initReloadAnchor').and.returnValue(undefined);
fixture.detectChanges();
});
@@ -66,6 +89,7 @@ describe('ClaimedTaskActionsApproveComponent', () => {
beforeEach(() => {
spyOn(component.processCompleted, 'emit');
spyOn(component, 'startActionExecution').and.returnValue(of(null));
expectedBody = {
[component.option]: 'true'
@@ -75,12 +99,30 @@ describe('ClaimedTaskActionsApproveComponent', () => {
fixture.detectChanges();
});
it('should call claimedTaskService\'s submitTask with the expected body', () => {
expect(claimedTaskService.submitTask).toHaveBeenCalledWith(object.id, expectedBody)
it('should start the action execution', () => {
expect(component.startActionExecution).toHaveBeenCalled();
});
it('should emit a successful processCompleted event', () => {
expect(component.processCompleted.emit).toHaveBeenCalledWith(true);
describe('actionExecution', () => {
beforeEach(() => {
component.actionExecution().subscribe();
fixture.detectChanges();
});
it('should call claimedTaskService\'s submitTask', () => {
expect(claimedTaskService.submitTask).toHaveBeenCalledWith(object.id, expectedBody)
});
});
});
describe('reloadObjectExecution', () => {
it('should return the component object itself', (done) => {
component.reloadObjectExecution().subscribe((val) => {
expect(val).toEqual(component.object);
done();
});
});
});

View File

@@ -1,7 +1,16 @@
import { Component } from '@angular/core';
import { Component, Injector } from '@angular/core';
import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-actions-abstract.component';
import { rendersWorkflowTaskOption } from '../switcher/claimed-task-actions-decorator';
import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service';
import { Observable } from 'rxjs';
import { RemoteData } from '../../../../core/data/remote-data';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { Router } from '@angular/router';
import { NotificationsService } from '../../../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { SearchService } from '../../../../core/shared/search/search.service';
import { RequestService } from '../../../../core/data/request.service';
import { ClaimedApprovedTaskSearchResult } from '../../../object-collection/shared/claimed-approved-task-search-result.model';
import { of } from 'rxjs/internal/observable/of';
export const WORKFLOW_TASK_OPTION_APPROVE = 'submit_approve';
@@ -20,7 +29,29 @@ export class ClaimedTaskActionsApproveComponent extends ClaimedTaskActionsAbstra
*/
option = WORKFLOW_TASK_OPTION_APPROVE;
constructor(protected claimedTaskService: ClaimedTaskDataService) {
super(claimedTaskService);
constructor(protected injector: Injector,
protected router: Router,
protected notificationsService: NotificationsService,
protected translate: TranslateService,
protected searchService: SearchService,
protected requestService: RequestService) {
super(injector, router, notificationsService, translate, searchService, requestService);
}
reloadObjectExecution(): Observable<RemoteData<DSpaceObject> | DSpaceObject> {
return of(this.object);
}
convertReloadedObject(dso: DSpaceObject): DSpaceObject {
const reloadedObject = Object.assign(new ClaimedApprovedTaskSearchResult(), dso, {
indexableObject: dso
});
return reloadedObject;
}
// mock
actionExecution(): Observable<any> {
return of({ hasSucceeded: true});
}
}

View File

@@ -3,11 +3,11 @@
<ds-claimed-task-actions-loader *ngFor="let option of workflowAction?.options"
[option]="option"
[object]="object"
(processCompleted)="handleActionResponse($event)">
(processCompleted)="this.processCompleted.emit($event)">
</ds-claimed-task-actions-loader>
<ds-claimed-task-actions-loader [option]="returnToPoolOption"
[object]="object"
(processCompleted)="handleActionResponse($event)">
(processCompleted)="this.processCompleted.emit($event)">
</ds-claimed-task-actions-loader>
</div>
</ng-container>

View File

@@ -1,7 +1,7 @@
<a *ngIf="object"
class="btn btn-primary"
ngbTooltip="{{'submission.workflow.tasks.claimed.edit_help' | translate}}"
[routerLink]="['/workflowitems/' + (object.workflowitem | async)?.payload.id + '/edit']"
[routerLink]="['/workflowitems/' + (object.workflowitem | async)?.payload?.id + '/edit']"
role="button">
<i class="fa fa-edit"></i> {{'submission.workflow.tasks.claimed.edit' | translate}}
</a>

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { ChangeDetectionStrategy, Injector, 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';
@@ -7,13 +7,28 @@ import { ClaimedTaskActionsEditMetadataComponent } from './claimed-task-actions-
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service';
import { TranslateLoaderMock } from '../../../testing/translate-loader.mock';
import { getMockSearchService } from '../../../mocks/search-service.mock';
import { getMockRequestService } from '../../../mocks/request.service.mock';
import { PoolTaskDataService } from '../../../../core/tasks/pool-task-data.service';
import { NotificationsService } from '../../../notifications/notifications.service';
import { NotificationsServiceStub } from '../../../testing/notifications-service.stub';
import { Router } from '@angular/router';
import { RouterStub } from '../../../testing/router.stub';
import { SearchService } from '../../../../core/shared/search/search.service';
import { RequestService } from '../../../../core/data/request.service';
let component: ClaimedTaskActionsEditMetadataComponent;
let fixture: ComponentFixture<ClaimedTaskActionsEditMetadataComponent>;
const searchService = getMockSearchService();
const requestService = getMockRequestService();
let mockPoolTaskDataService: PoolTaskDataService;
describe('ClaimedTaskActionsEditMetadataComponent', () => {
const object = Object.assign(new ClaimedTask(), { id: 'claimed-task-1' });
mockPoolTaskDataService = new PoolTaskDataService(null, null, null, null, null, null, null, null);
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
@@ -25,7 +40,13 @@ describe('ClaimedTaskActionsEditMetadataComponent', () => {
})
],
providers: [
{ provide: ClaimedTaskDataService, useValue: {} }
{ provide: ClaimedTaskDataService, useValue: {} },
{ provide: Injector, useValue: {} },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: Router, useValue: new RouterStub() },
{ provide: SearchService, useValue: searchService },
{ provide: RequestService, useValue: requestService },
{ provide: PoolTaskDataService, useValue: mockPoolTaskDataService },
],
declarations: [ClaimedTaskActionsEditMetadataComponent],
schemas: [NO_ERRORS_SCHEMA]
@@ -38,6 +59,7 @@ describe('ClaimedTaskActionsEditMetadataComponent', () => {
fixture = TestBed.createComponent(ClaimedTaskActionsEditMetadataComponent);
component = fixture.componentInstance;
component.object = object;
spyOn(component, 'initReloadAnchor').and.returnValue(undefined);
fixture.detectChanges();
});

View File

@@ -1,7 +1,11 @@
import { Component } from '@angular/core';
import { Component, Injector } from '@angular/core';
import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-actions-abstract.component';
import { rendersWorkflowTaskOption } from '../switcher/claimed-task-actions-decorator';
import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service';
import { Router } from '@angular/router';
import { NotificationsService } from '../../../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { SearchService } from '../../../../core/shared/search/search.service';
import { RequestService } from '../../../../core/data/request.service';
export const WORKFLOW_TASK_OPTION_EDIT_METADATA = 'submit_edit_metadata';
@@ -20,7 +24,12 @@ export class ClaimedTaskActionsEditMetadataComponent extends ClaimedTaskActionsA
*/
option = WORKFLOW_TASK_OPTION_EDIT_METADATA;
constructor(protected claimedTaskService: ClaimedTaskDataService) {
super(claimedTaskService);
constructor(protected injector: Injector,
protected router: Router,
protected notificationsService: NotificationsService,
protected translate: TranslateService,
protected searchService: SearchService,
protected requestService: RequestService) {
super(injector, router, notificationsService, translate, searchService, requestService);
}
}

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
@@ -12,12 +12,29 @@ import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.m
import { of as observableOf } from 'rxjs/internal/observable/of';
import { ProcessTaskResponse } from '../../../../core/tasks/models/process-task-response';
import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service';
import { NotificationsService } from '../../../notifications/notifications.service';
import { NotificationsServiceStub } from '../../../testing/notifications-service.stub';
import { Router } from '@angular/router';
import { RouterStub } from '../../../testing/router.stub';
import { SearchService } from '../../../../core/shared/search/search.service';
import { RequestService } from '../../../../core/data/request.service';
import { PoolTaskDataService } from '../../../../core/tasks/pool-task-data.service';
import { getMockSearchService } from '../../../mocks/search-service.mock';
import { getMockRequestService } from '../../../mocks/request.service.mock';
import { of } from 'rxjs';
import { ClaimedDeclinedTaskSearchResult } from '../../../object-collection/shared/claimed-declined-task-search-result.model';
let component: ClaimedTaskActionsRejectComponent;
let fixture: ComponentFixture<ClaimedTaskActionsRejectComponent>;
let formBuilder: FormBuilder;
let modalService: NgbModal;
const searchService = getMockSearchService();
const requestService = getMockRequestService();
let mockPoolTaskDataService: PoolTaskDataService;
describe('ClaimedTaskActionsRejectComponent', () => {
const object = Object.assign(new ClaimedTask(), { id: 'claimed-task-1' });
const claimedTaskService = jasmine.createSpyObj('claimedTaskService', {
@@ -25,6 +42,7 @@ describe('ClaimedTaskActionsRejectComponent', () => {
});
beforeEach(async(() => {
mockPoolTaskDataService = new PoolTaskDataService(null, null, null, null, null, null, null, null);
TestBed.configureTestingModule({
imports: [
NgbModule,
@@ -39,6 +57,12 @@ describe('ClaimedTaskActionsRejectComponent', () => {
declarations: [ClaimedTaskActionsRejectComponent],
providers: [
{ provide: ClaimedTaskDataService, useValue: claimedTaskService },
{ provide: Injector, useValue: {} },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: Router, useValue: new RouterStub() },
{ provide: SearchService, useValue: searchService },
{ provide: RequestService, useValue: requestService },
{ provide: PoolTaskDataService, useValue: mockPoolTaskDataService },
FormBuilder,
NgbModal
],
@@ -55,6 +79,7 @@ describe('ClaimedTaskActionsRejectComponent', () => {
modalService = TestBed.get(NgbModal);
component.object = object;
component.modalRef = modalService.open('ok');
spyOn(component, 'initReloadAnchor').and.returnValue(undefined);
fixture.detectChanges();
});
@@ -96,6 +121,7 @@ describe('ClaimedTaskActionsRejectComponent', () => {
beforeEach(() => {
spyOn(component.processCompleted, 'emit');
spyOn(component, 'startActionExecution').and.returnValue(of(null));
expectedBody = {
[component.option]: 'true',
@@ -113,12 +139,39 @@ describe('ClaimedTaskActionsRejectComponent', () => {
fixture.detectChanges();
});
it('should call claimedTaskService\'s submitTask with the expected body', () => {
expect(claimedTaskService.submitTask).toHaveBeenCalledWith(object.id, expectedBody)
it('should start the action execution', () => {
expect(component.startActionExecution).toHaveBeenCalled();
});
it('should emit a successful processCompleted event', () => {
expect(component.processCompleted.emit).toHaveBeenCalledWith(true);
describe('actionExecution', () => {
beforeEach(() => {
component.actionExecution().subscribe();
fixture.detectChanges();
});
it('should call claimedTaskService\'s submitTask', () => {
expect(claimedTaskService.submitTask).toHaveBeenCalledWith(object.id, expectedBody)
});
});
});
describe('reloadObjectExecution', () => {
it('should return the component object itself', (done) => {
component.reloadObjectExecution().subscribe((val) => {
expect(val).toEqual(component.object);
done();
});
});
});
describe('convertReloadedObject', () => {
it('should return a ClaimedDeclinedTaskSearchResult instance', () => {
const reloadedObject = component.convertReloadedObject(component.object);
expect(reloadedObject instanceof ClaimedDeclinedTaskSearchResult).toEqual(true);
});
});
});

View File

@@ -1,10 +1,19 @@
import { Component, OnInit } from '@angular/core';
import { Component, Injector, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-actions-abstract.component';
import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service';
import { rendersWorkflowTaskOption } from '../switcher/claimed-task-actions-decorator';
import { Router } from '@angular/router';
import { NotificationsService } from '../../../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { SearchService } from '../../../../core/shared/search/search.service';
import { RequestService } from '../../../../core/data/request.service';
import { Observable } from 'rxjs';
import { RemoteData } from '../../../../core/data/remote-data';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { of } from 'rxjs/internal/observable/of';
import { ClaimedDeclinedTaskSearchResult } from '../../../object-collection/shared/claimed-declined-task-search-result.model';
export const WORKFLOW_TASK_OPTION_REJECT = 'submit_reject';
@@ -33,17 +42,15 @@ export class ClaimedTaskActionsRejectComponent extends ClaimedTaskActionsAbstrac
*/
public modalRef: NgbModalRef;
/**
* Initialize instance variables
*
* @param {FormBuilder} formBuilder
* @param {NgbModal} modalService
* @param claimedTaskService
*/
constructor(protected claimedTaskService: ClaimedTaskDataService,
constructor(protected injector: Injector,
protected router: Router,
protected notificationsService: NotificationsService,
protected translate: TranslateService,
protected searchService: SearchService,
protected requestService: RequestService,
private formBuilder: FormBuilder,
private modalService: NgbModal) {
super(claimedTaskService);
super(injector, router, notificationsService, translate, searchService, requestService);
}
/**
@@ -55,6 +62,14 @@ export class ClaimedTaskActionsRejectComponent extends ClaimedTaskActionsAbstrac
});
}
/**
* Submit a reject option for the task
*/
submitTask() {
this.modalRef.close('Send Button');
super.submitTask();
}
/**
* Create the request body for rejecting a workflow task
* Includes the reason from the form
@@ -64,14 +79,6 @@ export class ClaimedTaskActionsRejectComponent extends ClaimedTaskActionsAbstrac
return Object.assign(super.createbody(), { reason });
}
/**
* Submit a reject option for the task
*/
submitTask() {
this.modalRef.close('Send Button');
super.submitTask();
}
/**
* Open modal
*
@@ -81,4 +88,20 @@ export class ClaimedTaskActionsRejectComponent extends ClaimedTaskActionsAbstrac
this.rejectForm.reset();
this.modalRef = this.modalService.open(content);
}
reloadObjectExecution(): Observable<RemoteData<DSpaceObject> | DSpaceObject> {
return of(this.object);
}
convertReloadedObject(dso: DSpaceObject): DSpaceObject {
const reloadedObject = Object.assign(new ClaimedDeclinedTaskSearchResult(), dso, {
indexableObject: dso
});
return reloadedObject;
}
// mock
actionExecution(): Observable<any> {
return of({ hasSucceeded: true});
}
}

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
@@ -9,10 +9,26 @@ import { of as observableOf } from 'rxjs/internal/observable/of';
import { ProcessTaskResponse } from '../../../../core/tasks/models/process-task-response';
import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service';
import { TranslateLoaderMock } from '../../../mocks/translate-loader.mock';
import { NotificationsService } from '../../../notifications/notifications.service';
import { NotificationsServiceStub } from '../../../testing/notifications-service.stub';
import { Router } from '@angular/router';
import { RouterStub } from '../../../testing/router.stub';
import { PoolTaskDataService } from '../../../../core/tasks/pool-task-data.service';
import { SearchService } from '../../../../core/shared/search/search.service';
import { RequestService } from '../../../../core/data/request.service';
import { getMockSearchService } from '../../../mocks/search-service.mock';
import { getMockRequestService } from '../../../mocks/request.service.mock';
import { of } from 'rxjs';
let component: ClaimedTaskActionsReturnToPoolComponent;
let fixture: ComponentFixture<ClaimedTaskActionsReturnToPoolComponent>;
const searchService = getMockSearchService();
const requestService = getMockRequestService();
let mockPoolTaskDataService: PoolTaskDataService;
describe('ClaimedTaskActionsReturnToPoolComponent', () => {
const object = Object.assign(new ClaimedTask(), { id: 'claimed-task-1' });
const claimedTaskService = jasmine.createSpyObj('claimedTaskService', {
@@ -20,6 +36,7 @@ describe('ClaimedTaskActionsReturnToPoolComponent', () => {
});
beforeEach(async(() => {
mockPoolTaskDataService = new PoolTaskDataService(null, null, null, null, null, null, null, null);
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
@@ -30,7 +47,13 @@ describe('ClaimedTaskActionsReturnToPoolComponent', () => {
})
],
providers: [
{ provide: ClaimedTaskDataService, useValue: claimedTaskService }
{ provide: ClaimedTaskDataService, useValue: claimedTaskService },
{ provide: Injector, useValue: {} },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: Router, useValue: new RouterStub() },
{ provide: SearchService, useValue: searchService },
{ provide: RequestService, useValue: requestService },
{ provide: PoolTaskDataService, useValue: mockPoolTaskDataService },
],
declarations: [ClaimedTaskActionsReturnToPoolComponent],
schemas: [NO_ERRORS_SCHEMA]
@@ -39,12 +62,13 @@ describe('ClaimedTaskActionsReturnToPoolComponent', () => {
}).compileComponents();
}));
beforeEach(() => {
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(ClaimedTaskActionsReturnToPoolComponent);
component = fixture.componentInstance;
component.object = object;
spyOn(component, 'initReloadAnchor').and.returnValue(undefined);
fixture.detectChanges();
});
}));
it('should display return to pool button', () => {
const btn = fixture.debugElement.query(By.css('.btn-secondary'));
@@ -61,11 +85,9 @@ describe('ClaimedTaskActionsReturnToPoolComponent', () => {
expect(span).toBeDefined();
});
describe('submitTask', () => {
describe('actionExecution', () => {
beforeEach(() => {
spyOn(component.processCompleted, 'emit');
component.submitTask();
component.actionExecution().subscribe();
fixture.detectChanges();
});
@@ -73,8 +95,19 @@ describe('ClaimedTaskActionsReturnToPoolComponent', () => {
expect(claimedTaskService.returnToPoolTask).toHaveBeenCalledWith(object.id)
});
it('should emit a successful processCompleted event', () => {
expect(component.processCompleted.emit).toHaveBeenCalledWith(true);
});
describe('reloadObjectExecution', () => {
beforeEach(() => {
spyOn(mockPoolTaskDataService, 'findByItem').and.returnValue(of(null));
component.itemUuid = 'uuid';
component.reloadObjectExecution().subscribe();
fixture.detectChanges();
});
it('should call poolTaskDataService findItem with itemUuid', () => {
expect(mockPoolTaskDataService.findByItem).toHaveBeenCalledWith('uuid');
});
});

View File

@@ -1,8 +1,15 @@
import { Component } from '@angular/core';
import {Component, Injector} from '@angular/core';
import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-actions-abstract.component';
import { rendersWorkflowTaskOption } from '../switcher/claimed-task-actions-decorator';
import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service';
import { ProcessTaskResponse } from '../../../../core/tasks/models/process-task-response';
import { Observable } from 'rxjs';
import { Router } from '@angular/router';
import { NotificationsService } from '../../../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { SearchService } from '../../../../core/shared/search/search.service';
import { RequestService } from '../../../../core/data/request.service';
import { RemoteData } from '../../../../core/data/remote-data';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { PoolTaskDataService } from '../../../../core/tasks/pool-task-data.service';
export const WORKFLOW_TASK_OPTION_RETURN_TO_POOL = 'return_to_pool';
@@ -21,19 +28,22 @@ export class ClaimedTaskActionsReturnToPoolComponent extends ClaimedTaskActionsA
*/
option = WORKFLOW_TASK_OPTION_RETURN_TO_POOL;
constructor(protected claimedTaskService: ClaimedTaskDataService) {
super(claimedTaskService);
constructor(protected injector: Injector,
protected router: Router,
protected notificationsService: NotificationsService,
protected translate: TranslateService,
protected searchService: SearchService,
protected requestService: RequestService,
private poolTaskService: PoolTaskDataService) {
super(injector, router, notificationsService, translate, searchService, requestService);
}
/**
* Submit a return to pool option for the task
*/
submitTask() {
this.processing$.next(true);
this.claimedTaskService.returnToPoolTask(this.object.id)
.subscribe((res: ProcessTaskResponse) => {
this.processing$.next(false);
this.processCompleted.emit(res.hasSucceeded);
});
reloadObjectExecution(): Observable<RemoteData<DSpaceObject> | DSpaceObject> {
return this.poolTaskService.findByItem(this.itemUuid);
}
actionExecution(): Observable<any> {
return this.objectDataService.returnToPoolTask(this.object.id);
}
}

View File

@@ -14,6 +14,7 @@ import { ClaimedTaskActionsDirective } from './claimed-task-actions.directive';
import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-actions-abstract.component';
import { hasValue } from '../../../empty.util';
import { Subscription } from 'rxjs/internal/Subscription';
import { MyDSpaceActionsResult } from '../../mydspace-actions';
@Component({
selector: 'ds-claimed-task-actions-loader',
@@ -38,7 +39,7 @@ export class ClaimedTaskActionsLoaderComponent implements OnInit, OnDestroy {
/**
* Emits the success or failure of a processed action
*/
@Output() processCompleted: EventEmitter<boolean> = new EventEmitter<boolean>();
@Output() processCompleted = new EventEmitter<MyDSpaceActionsResult>();
/**
* Directive to determine where the dynamic child component is located
@@ -69,7 +70,7 @@ export class ClaimedTaskActionsLoaderComponent implements OnInit, OnDestroy {
const componentInstance = (componentRef.instance as ClaimedTaskActionsAbstractComponent);
componentInstance.object = this.object;
if (hasValue(componentInstance.processCompleted)) {
this.subs.push(componentInstance.processCompleted.subscribe((success) => this.processCompleted.emit(success)));
this.subs.push(componentInstance.processCompleted.subscribe((result) => this.processCompleted.emit(result)));
}
}
}

View File

@@ -1,5 +1,5 @@
import { Router } from '@angular/router';
import { Injector, Input } from '@angular/core';
import { EventEmitter, Injector, Input, Output } from '@angular/core';
import { find, take, tap } from 'rxjs/operators';
@@ -12,9 +12,14 @@ import { NotificationOptions } from '../notifications/models/notification-option
import { NotificationsService } from '../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { RequestService } from '../../core/data/request.service';
import { Subscription } from 'rxjs';
import { BehaviorSubject, Subscription } from 'rxjs';
import { SearchService } from '../../core/shared/search/search.service';
export interface MyDSpaceActionsResult {
result: boolean,
reloadedObject: DSpaceObject,
}
/**
* Abstract class for all different representations of mydspace actions
*/
@@ -26,7 +31,18 @@ export abstract class MyDSpaceActionsComponent<T extends DSpaceObject, TService
@Input() abstract object: T;
/**
* Instance of DataService realted to mydspace object
* Emit to notify the instantiator when the action has been performed.
*/
@Output() processCompleted = new EventEmitter<MyDSpaceActionsResult>();
/**
* A boolean representing if an operation is pending
* @type {BehaviorSubject<boolean>}
*/
public processing$ = new BehaviorSubject<boolean>(false);
/**
* Instance of DataService related to mydspace object
*/
protected objectDataService: TService;

View File

@@ -0,0 +1,137 @@
import { Router } from '@angular/router';
import { Injector, OnInit } from '@angular/core';
import { map, switchMap, tap} from 'rxjs/operators';
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';
import { RequestService } from '../../core/data/request.service';
import { SearchService } from '../../core/shared/search/search.service';
import { Observable} from 'rxjs/internal/Observable';
import { of} from 'rxjs/internal/observable/of';
import { ProcessTaskResponse } from '../../core/tasks/models/process-task-response';
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
import { getSearchResultFor } from '../search/search-result-element-decorator';
import { MyDSpaceActionsComponent } from './mydspace-actions';
/**
* Abstract class for all different representations of mydspace actions
*/
export abstract class MyDSpaceReloadableActionsComponent<T extends DSpaceObject, TService extends DataService<T>>
extends MyDSpaceActionsComponent<T, TService> implements OnInit {
protected constructor(
protected objectType: ResourceType,
protected injector: Injector,
protected router: Router,
protected notificationsService: NotificationsService,
protected translate: TranslateService,
protected searchService: SearchService,
protected requestService: RequestService) {
super(objectType, injector, router, notificationsService, translate, searchService, requestService);
}
/**
* Perform the actual implementation of this reloadable action.
*/
abstract actionExecution(): Observable<ProcessTaskResponse>;
/**
* Reload the object (typically by a remote call).
*/
abstract reloadObjectExecution(): Observable<RemoteData<DSpaceObject> | DSpaceObject>;
ngOnInit() {
this.initReloadAnchor();
this.initObjects(this.object);
}
/**
* Start the execution of the action.
* 1. performAction
* 2. reload of the object
* 3. notification
*/
startActionExecution(): Observable<DSpaceObject> {
this.processing$.next(true);
return this.actionExecution().pipe(
switchMap((res: ProcessTaskResponse) => {
if (res.hasSucceeded) {
return this._reloadObject().pipe(
tap(
(reloadedObject) => {
this.processing$.next(false);
this.handleReloadableActionResponse(res.hasSucceeded, reloadedObject);
return reloadedObject;
})
)
} else {
this.processing$.next(false);
this.handleReloadableActionResponse(res.hasSucceeded, null);
return of(null);
}
}));
}
/**
* Handle the action response and show properly notifications.
*
* @param result
* true on success, false otherwise
* @param reloadedObject
* the reloadedObject
*/
handleReloadableActionResponse(result: boolean, reloadedObject: DSpaceObject): void {
if (result) {
if (reloadedObject) {
this.processCompleted.emit({result, reloadedObject});
} else {
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));
}
}
/**
* Hook called on init to initialized the required information used to reload the object.
*/
// tslint:disable-next-line:no-empty
initReloadAnchor() {}
/**
* Convert the reloadedObject to the Type required by this action.
* @param dso
*/
convertReloadedObject(dso: DSpaceObject): DSpaceObject {
const constructor = getSearchResultFor((dso as any).constructor);
const reloadedObject = Object.assign(new constructor(), dso, {
indexableObject: dso
});
return reloadedObject;
}
/**
* Retrieve the refreshed object and transform it to a reloadedObject.
* @param dso
*/
private _reloadObject(): Observable<DSpaceObject> {
return this.reloadObjectExecution().pipe(
switchMap((res) => {
return res instanceof RemoteData ? of(res).pipe(getFirstSucceededRemoteDataPayload()) : of(res)
})).pipe(map((dso) => {
return this.convertReloadedObject(dso);
}))
}
}

View File

@@ -1,8 +1,8 @@
<button type="button"
class="btn btn-info mt-1 mb-3"
ngbTooltip="{{'submission.workflow.tasks.pool.claim_help' | translate}}"
[disabled]="(processingClaim$ | async)"
[disabled]="(processing$ | async)"
(click)="claim()">
<span *ngIf="(processingClaim$ | async)"><i class='fas fa-circle-notch fa-spin'></i> {{'submission.workflow.tasks.generic.processing' | translate}}</span>
<span *ngIf="!(processingClaim$ | async)"><i class="fas fa-hand-paper"></i> {{'submission.workflow.tasks.pool.claim' | translate}}</span>
<span *ngIf="(processing$ | async)"><i class='fas fa-circle-notch fa-spin'></i> {{'submission.workflow.tasks.generic.processing' | translate}}</span>
<span *ngIf="!(processing$ | async)"><i class="fas fa-hand-paper"></i> {{'submission.workflow.tasks.pool.claim' | translate}}</span>
</button>

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { By } from '@angular/platform-browser';
@@ -16,11 +16,17 @@ 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';
import { createSuccessfulRemoteDataObject } from '../../remote-data.utils';
import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from '../../remote-data.utils';
import { getMockRequestService } from '../../mocks/request.service.mock';
import { RequestService } from '../../../core/data/request.service';
import { getMockSearchService } from '../../mocks/search-service.mock';
import { SearchService } from '../../../core/shared/search/search.service';
import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service';
import { PoolTaskSearchResult } from '../../object-collection/shared/pool-task-search-result.model';
import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-response';
let mockDataService: PoolTaskDataService;
let mockClaimedTaskDataService: ClaimedTaskDataService;
let component: PoolTaskActionsComponent;
let fixture: ComponentFixture<PoolTaskActionsComponent>;
@@ -29,10 +35,6 @@ let mockObject: PoolTask;
let notificationsServiceStub: NotificationsServiceStub;
let router: RouterStub;
const mockDataService = jasmine.createSpyObj('PoolTaskDataService', {
claimTask: jasmine.createSpy('claimTask')
});
const searchService = getMockSearchService();
const requestServce = getMockRequestService();
@@ -73,6 +75,8 @@ mockObject = Object.assign(new PoolTask(), { workflowitem: observableOf(rdWorkfl
describe('PoolTaskActionsComponent', () => {
beforeEach(async(() => {
mockDataService = new PoolTaskDataService(null, null, null, null, null, null, null, null)
mockClaimedTaskDataService = new ClaimedTaskDataService(null, null, null, null, null, null, null, null);
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
@@ -88,6 +92,7 @@ describe('PoolTaskActionsComponent', () => {
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: Router, useValue: new RouterStub() },
{ provide: PoolTaskDataService, useValue: mockDataService },
{ provide: ClaimedTaskDataService, useValue: mockClaimedTaskDataService },
{ provide: SearchService, useValue: searchService },
{ provide: RequestService, useValue: requestServce }
],
@@ -128,63 +133,182 @@ describe('PoolTaskActionsComponent', () => {
expect(btn).toBeDefined();
});
it('should call claimTask method on claim', fakeAsync(() => {
spyOn(component, 'reload');
mockDataService.claimTask.and.returnValue(observableOf({hasSucceeded: true}));
it('should call claim task with href of getPoolTaskEndpointById', ((done) => {
component.claim();
fixture.detectChanges();
const poolTaskHref = 'poolTaskHref';
const remoteClaimTaskResponse: any = new ProcessTaskResponse(true, null, null);
const remoteReloadedObjectResponse: any = createSuccessfulRemoteDataObject(new PoolTask());
fixture.whenStable().then(() => {
expect(mockDataService.claimTask).toHaveBeenCalledWith(mockObject.id);
});
spyOn(mockDataService, 'getPoolTaskEndpointById').and.returnValue(observableOf(poolTaskHref));
spyOn(mockClaimedTaskDataService, 'claimTask').and.returnValue(observableOf(remoteClaimTaskResponse));
spyOn(mockClaimedTaskDataService, 'findByItem').and.returnValue(observableOf(remoteReloadedObjectResponse));
}));
(component as any).objectDataService = mockDataService;
it('should display a success notification on claim success', async(() => {
spyOn(component, 'reload');
mockDataService.claimTask.and.returnValue(observableOf({hasSucceeded: true}));
spyOn(component, 'handleReloadableActionResponse').and.callThrough();
component.claim();
fixture.detectChanges();
component.startActionExecution().subscribe( (result) => {
expect(mockDataService.getPoolTaskEndpointById).toHaveBeenCalledWith(mockObject.id);
expect(mockClaimedTaskDataService.claimTask).toHaveBeenCalledWith(mockObject.id, poolTaskHref);
expect(mockClaimedTaskDataService.findByItem).toHaveBeenCalledWith(component.itemUuid);
expect(result instanceof PoolTaskSearchResult).toBeTrue();
expect(component.handleReloadableActionResponse).toHaveBeenCalledWith(true, result);
fixture.whenStable().then(() => {
expect(notificationsServiceStub.success).toHaveBeenCalled();
});
done();
})
}));
it('should reload page on claim success', async(() => {
spyOn(router, 'navigateByUrl');
router.url = 'test.url/test';
mockDataService.claimTask.and.returnValue(observableOf({hasSucceeded: true}));
// general reload assertion
describe('on reload action init', () => {
component.claim();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(router.navigateByUrl).toHaveBeenCalledWith('test.url/test');
beforeEach(() => {
spyOn(component, 'initReloadAnchor').and.returnValue(null);
spyOn(component, 'initObjects');
});
}));
it('should display an error notification on claim failure', async(() => {
mockDataService.claimTask.and.returnValue(observableOf({hasSucceeded: false}));
it('should call initReloadAnchor and initObjects on init', async(() => {
component.ngOnInit();
component.claim();
fixture.detectChanges();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(notificationsServiceStub.error).toHaveBeenCalled();
fixture.whenStable().then(() => {
expect(component.initReloadAnchor).toHaveBeenCalled();
expect(component.initObjects).toHaveBeenCalled();
});
}));
})
describe('on action execution fail', () => {
let remoteClaimTaskErrorResponse;
beforeEach(() => {
mockDataService = new PoolTaskDataService(null, null, null, null, null, null, null, null);
const poolTaskHref = 'poolTaskHref';
remoteClaimTaskErrorResponse = new ProcessTaskResponse(false, null, null);
const remoteReloadedObjectResponse: any = createSuccessfulRemoteDataObject(new PoolTask());
spyOn(mockDataService, 'getPoolTaskEndpointById').and.returnValue(observableOf(poolTaskHref));
spyOn(mockClaimedTaskDataService, 'findByItem').and.returnValue(observableOf(remoteReloadedObjectResponse));
spyOn(mockClaimedTaskDataService, 'claimTask').and.returnValue(observableOf(remoteClaimTaskErrorResponse));
spyOn(component, 'reloadObjectExecution').and.callThrough();
spyOn(component.processCompleted, 'emit').and.callThrough();
(component as any).objectDataService = mockDataService;
});
}));
it('should clear the object cache by href', async(() => {
component.reload();
fixture.detectChanges();
it('should show error notification', (done) => {
fixture.whenStable().then(() => {
expect(searchService.getEndpoint).toHaveBeenCalled();
expect(requestServce.removeByHrefSubstring).toHaveBeenCalledWith('discover/search/objects');
component.startActionExecution().subscribe( (result) => {
expect(notificationsServiceStub.error).toHaveBeenCalled();
done();
})
});
}));
it('should not call reloadObject', (done) => {
component.startActionExecution().subscribe( (result) => {
expect(component.reloadObjectExecution).not.toHaveBeenCalled();
done();
})
});
it('should not emit processCompleted', (done) => {
component.startActionExecution().subscribe( (result) => {
expect(component.processCompleted.emit).not.toHaveBeenCalled();
done();
})
});
});
describe('on action execution success', () => {
beforeEach(() => {
mockDataService = new PoolTaskDataService(null, null, null, null, null, null, null, null);
const poolTaskHref = 'poolTaskHref';
const remoteClaimTaskResponse: any = new ProcessTaskResponse(true, null, null);
const remoteReloadedObjectResponse: any = createSuccessfulRemoteDataObject(new PoolTask());
spyOn(mockDataService, 'getPoolTaskEndpointById').and.returnValue(observableOf(poolTaskHref));
spyOn(mockClaimedTaskDataService, 'findByItem').and.returnValue(observableOf(remoteReloadedObjectResponse));
spyOn(mockClaimedTaskDataService, 'claimTask').and.returnValue(observableOf(remoteClaimTaskResponse));
spyOn(component, 'reloadObjectExecution').and.callThrough();
spyOn(component, 'convertReloadedObject').and.callThrough();
spyOn(component.processCompleted, 'emit').and.callThrough();
(component as any).objectDataService = mockDataService;
});
it('should reloadObject in case of action execution success', (done) => {
component.startActionExecution().subscribe( (result) => {
expect(component.reloadObjectExecution).toHaveBeenCalled();
done();
})
});
it('should convert the reloaded object', (done) => {
component.startActionExecution().subscribe( (result) => {
expect(component.convertReloadedObject).toHaveBeenCalled();
done();
})
});
it('should emit the reloaded object in case of success', (done) => {
component.startActionExecution().subscribe( (result) => {
expect(component.processCompleted.emit).toHaveBeenCalledWith({result: true, reloadedObject: result as any});
done();
})
});
});
describe('on action execution success but without a reloadedObject', () => {
beforeEach(() => {
mockDataService = new PoolTaskDataService(null, null, null, null, null, null, null, null);
const poolTaskHref = 'poolTaskHref';
const remoteClaimTaskResponse: any = new ProcessTaskResponse(true, null, null);
const remoteReloadedObjectResponse: any = createFailedRemoteDataObject();
spyOn(mockDataService, 'getPoolTaskEndpointById').and.returnValue(observableOf(poolTaskHref));
spyOn(mockClaimedTaskDataService, 'findByItem').and.returnValue(observableOf(remoteReloadedObjectResponse));
spyOn(mockClaimedTaskDataService, 'claimTask').and.returnValue(observableOf(remoteClaimTaskResponse));
spyOn(component, 'convertReloadedObject').and.returnValue(null);
spyOn(component, 'reload').and.returnValue(null);
(component as any).objectDataService = mockDataService;
});
it('should call reload method', (done) => {
component.startActionExecution().subscribe( (result) => {
expect(component.reload).toHaveBeenCalled();
done();
})
});
});
});

View File

@@ -1,20 +1,24 @@
import { Component, Injector, Input } from '@angular/core';
import { Router } from '@angular/router';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { filter, map, switchMap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-response';
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 { isNotUndefined } from '../../empty.util';
import { MyDSpaceActionsComponent } from '../mydspace-actions';
import { NotificationsService } from '../../notifications/notifications.service';
import { RequestService } from '../../../core/data/request.service';
import { SearchService } from '../../../core/shared/search/search.service';
import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service';
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
import { Item } from '../../../core/shared/item.model';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { MyDSpaceReloadableActionsComponent } from '../mydspace-reloadable-actions';
import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-response';
/**
* This component represents mydspace actions related to PoolTask object.
@@ -24,24 +28,23 @@ import { SearchService } from '../../../core/shared/search/search.service';
styleUrls: ['./pool-task-actions.component.scss'],
templateUrl: './pool-task-actions.component.html',
})
export class PoolTaskActionsComponent extends MyDSpaceActionsComponent<PoolTask, PoolTaskDataService> {
export class PoolTaskActionsComponent extends MyDSpaceReloadableActionsComponent<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>;
/**
* Anchor used to reload the pool task.
*/
public itemUuid: string;
/**
* Initialize instance variables
*
@@ -55,6 +58,7 @@ export class PoolTaskActionsComponent extends MyDSpaceActionsComponent<PoolTask,
constructor(protected injector: Injector,
protected router: Router,
protected notificationsService: NotificationsService,
protected claimedTaskService: ClaimedTaskDataService,
protected translate: TranslateService,
protected searchService: SearchService,
protected requestService: RequestService) {
@@ -62,10 +66,10 @@ export class PoolTaskActionsComponent extends MyDSpaceActionsComponent<PoolTask,
}
/**
* Initialize objects
* Claim the task.
*/
ngOnInit() {
this.initObjects(this.object);
claim() {
this.startActionExecution().subscribe();
}
/**
@@ -80,15 +84,28 @@ 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.handleActionResponse(res.hasSucceeded);
this.processingClaim$.next(false);
});
actionExecution(): Observable<ProcessTaskResponse> {
return this.objectDataService.getPoolTaskEndpointById(this.object.id)
.pipe(switchMap((poolTaskHref) => {
return this.claimedTaskService.claimTask(this.object.id, poolTaskHref);
}))
}
reloadObjectExecution(): Observable<RemoteData<DSpaceObject> | DSpaceObject> {
return this.claimedTaskService.findByItem(this.itemUuid);
}
/**
* Retrieve the itemUuid.
*/
initReloadAnchor() {
(this.object as any).workflowitem.pipe(
getFirstSucceededRemoteDataPayload(),
switchMap((workflowItem: WorkflowItem) => workflowItem.item.pipe(getFirstSucceededRemoteDataPayload())
))
.subscribe((item: Item) => {
this.itemUuid = item.uuid;
})
}
}

View File

@@ -0,0 +1,8 @@
import { ClaimedTask } from '../../../core/tasks/models/claimed-task-object.model';
import { SearchResult } from '../../search/search-result.model';
/**
* Represents a search result object of an Approved ClaimedTask object
*/
export class ClaimedApprovedTaskSearchResult extends SearchResult<ClaimedTask> {
}

View File

@@ -0,0 +1,8 @@
import { ClaimedTask } from '../../../core/tasks/models/claimed-task-object.model';
import { SearchResult } from '../../search/search-result.model';
/**
* Represents a search result object of a Declined ClaimedTask object
*/
export class ClaimedDeclinedTaskSearchResult extends SearchResult<ClaimedTask> {
}

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
import { ChangeDetectionStrategy, ComponentFactoryResolver, NO_ERRORS_SCHEMA } from '@angular/core';
import { ListableObjectComponentLoaderComponent } from './listable-object-component-loader.component';
import { ListableObject } from '../listable-object.model';
@@ -9,6 +9,7 @@ import * as listableObjectDecorators from './listable-object.decorator';
import { ItemListElementComponent } from '../../../object-list/item-list-element/item-types/item/item-list-element.component';
import { ListableObjectDirective } from './listable-object.directive';
import { spyOnExported } from '../../../testing/utils.test';
import { By } from '@angular/platform-browser';
const testType = 'TestType';
const testContext = Context.Search;
@@ -55,4 +56,20 @@ describe('ListableObjectComponentLoaderComponent', () => {
expect(listableObjectDecorators.getListableObjectComponent).toHaveBeenCalledWith([testType], testViewMode, testContext);
})
});
describe('When a reloadedObject is emitted', () => {
it('should re-instantiate the listable component ', fakeAsync(() => {
spyOn((comp as any), 'instantiateComponent').and.returnValue(null);
const listableComponent = fixture.debugElement.query(By.css('ds-item-list-element')).componentInstance;
const reloadedObject: any = 'object';
(listableComponent as any).reloadedObject.emit(reloadedObject);
tick();
expect((comp as any).instantiateComponent).toHaveBeenCalledWith(reloadedObject);
}))
});
});

View File

@@ -1,4 +1,4 @@
import { Component, ComponentFactoryResolver, Input, OnInit, ViewChild } from '@angular/core';
import { Component, ComponentFactoryResolver, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ListableObject } from '../listable-object.model';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { Context } from '../../../../core/shared/context.model';
@@ -6,16 +6,19 @@ import { getListableObjectComponent } from './listable-object.decorator';
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
import { ListableObjectDirective } from './listable-object.directive';
import { CollectionElementLinkType } from '../../collection-element-link.type';
import { hasValue } from '../../../empty.util';
import { Subscription } from 'rxjs/internal/Subscription';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
@Component({
selector: 'ds-listable-object-component-loader',
// styleUrls: ['./listable-object-component-loader.component.scss'],
styleUrls: ['./listable-object-component-loader.component.scss'],
templateUrl: './listable-object-component-loader.component.html'
})
/**
* Component for determining what component to use depending on the item's relationship type (relationship.type)
*/
export class ListableObjectComponentLoaderComponent implements OnInit {
export class ListableObjectComponentLoaderComponent implements OnInit, OnDestroy {
/**
* The item or metadata to determine the component for
*/
@@ -61,6 +64,12 @@ export class ListableObjectComponentLoaderComponent implements OnInit {
*/
@ViewChild(ListableObjectDirective, {static: true}) listableObjectDirective: ListableObjectDirective;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
protected subs: Subscription[] = [];
constructor(private componentFactoryResolver: ComponentFactoryResolver) {
}
@@ -68,13 +77,23 @@ export class ListableObjectComponentLoaderComponent implements OnInit {
* Setup the dynamic child component
*/
ngOnInit(): void {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent());
this.instantiateComponent(this.object);
}
ngOnDestroy() {
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe());
}
private instantiateComponent(object) {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent(object));
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(componentFactory);
(componentRef.instance as any).object = this.object;
(componentRef.instance as any).object = object;
(componentRef.instance as any).index = this.index;
(componentRef.instance as any).linkType = this.linkType;
(componentRef.instance as any).listID = this.listID;
@@ -82,13 +101,21 @@ export class ListableObjectComponentLoaderComponent implements OnInit {
(componentRef.instance as any).context = this.context;
(componentRef.instance as any).viewMode = this.viewMode;
(componentRef.instance as any).value = this.value;
if ((componentRef.instance as any).reloadedObject) {
this.subs.push((componentRef.instance as any).reloadedObject.subscribe((reloadedObject: DSpaceObject) => {
if (reloadedObject) {
this.instantiateComponent(reloadedObject);
}
}));
}
}
/**
* Fetch the component depending on the item's relationship type, view mode and context
* @returns {GenericConstructor<Component>}
*/
private getComponent(): GenericConstructor<Component> {
return getListableObjectComponent(this.object.getRenderTypes(), this.viewMode, this.context)
private getComponent(object): GenericConstructor<Component> {
return getListableObjectComponent(object.getRenderTypes(), this.viewMode, this.context)
}
}

View File

@@ -3,5 +3,7 @@ export enum MyDspaceItemStatusType {
VALIDATION = 'mydspace.status.validation',
WAITING_CONTROLLER = 'mydspace.status.waiting-for-controller',
WORKSPACE = 'mydspace.status.workspace',
ARCHIVED = 'mydspace.status.archived'
ARCHIVED = 'mydspace.status.archived',
DECLINED = 'mydspace.status.declined',
APPROVED = 'mydspace.status.approved',
}

View File

@@ -1,14 +1,16 @@
import { Component, Input } from '@angular/core';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { ListableObject } from '../listable-object.model';
import { CollectionElementLinkType } from '../../collection-element-link.type';
import { Context } from '../../../../core/shared/context.model';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
@Component({
selector: 'ds-abstract-object-element',
template: ``,
})
export class AbstractListableElementComponent<T extends ListableObject> {
/**
* The object to render in this list element
*/
@@ -49,6 +51,11 @@ export class AbstractListableElementComponent<T extends ListableObject> {
*/
@Input() viewMode: ViewMode;
/**
* Emit when the object has been reloaded.
*/
@Output() reloadedObject = new EventEmitter<DSpaceObject>();
/**
* The available link types
*/

View File

@@ -6,5 +6,5 @@
[status]="status">
</ds-item-detail-preview>
<ds-claimed-task-actions *ngIf="workflowitem" [object]="dso"></ds-claimed-task-actions>
<ds-claimed-task-actions *ngIf="workflowitem" [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-claimed-task-actions>
</ng-container>

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { of as observableOf } from 'rxjs';
@@ -14,6 +14,7 @@ import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claim
import { VarDirective } from '../../../utils/var.directive';
import { LinkService } from '../../../../core/cache/builders/link.service';
import { getMockLinkService } from '../../../mocks/link-service.mock';
import { By } from '@angular/platform-browser';
let component: ClaimedTaskSearchResultDetailElementComponent;
let fixture: ComponentFixture<ClaimedTaskSearchResultDetailElementComponent>;
@@ -98,4 +99,16 @@ describe('ClaimedTaskSearchResultDetailElementComponent', () => {
it('should have properly status', () => {
expect(component.status).toEqual(MyDspaceItemStatusType.VALIDATION);
});
it('should forward claimed-task-actions processComplete event to reloadObject event emitter', fakeAsync(() => {
spyOn(component.reloadedObject, 'emit').and.callThrough();
const actionPayload: any = { reloadedObject: {}};
const actionsComponent = fixture.debugElement.query(By.css('ds-claimed-task-actions'));
actionsComponent.triggerEventHandler('processCompleted', actionPayload)
tick();
expect(component.reloadedObject.emit).toHaveBeenCalledWith(actionPayload.reloadedObject);
}));
});

View File

@@ -5,5 +5,5 @@
[showSubmitter]="showSubmitter"
[status]="status"></ds-item-detail-preview>
<ds-pool-task-actions *ngIf="workflowitem" [object]="dso"></ds-pool-task-actions>
<ds-pool-task-actions *ngIf="workflowitem" [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-pool-task-actions>
</ng-container>

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { of as observableOf } from 'rxjs';
@@ -14,6 +14,7 @@ import { PoolTaskSearchResult } from '../../../object-collection/shared/pool-tas
import { VarDirective } from '../../../utils/var.directive';
import { LinkService } from '../../../../core/cache/builders/link.service';
import { getMockLinkService } from '../../../mocks/link-service.mock';
import { By } from '@angular/platform-browser';
let component: PoolSearchResultDetailElementComponent;
let fixture: ComponentFixture<PoolSearchResultDetailElementComponent>;
@@ -99,4 +100,15 @@ describe('PoolSearchResultDetailElementComponent', () => {
it('should have properly status', () => {
expect(component.status).toEqual(MyDspaceItemStatusType.WAITING_CONTROLLER);
});
it('should forward pool-task-actions processCompleted event to the reloadedObject event emitter', fakeAsync(() => {
spyOn(component.reloadedObject, 'emit').and.callThrough();
const actionPayload: any = { reloadedObject: {}};
const actionsComponents = fixture.debugElement.query(By.css('ds-pool-task-actions'));
actionsComponents.triggerEventHandler('processCompleted', actionPayload)
tick();
expect(component.reloadedObject.emit).toHaveBeenCalledWith(actionPayload.reloadedObject);
}));
});

View File

@@ -0,0 +1,51 @@
import { Observable } from 'rxjs';
import { RemoteData } from '../../../../core/data/remote-data';
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
import { SearchResultListElementComponent } from '../../search-result-list-element/search-result-list-element.component';
import { followLink } from '../../../utils/follow-link-config.model';
import { LinkService } from '../../../../core/cache/builders/link.service';
import { TruncatableService } from '../../../truncatable/truncatable.service';
/**
* This component renders claimed task object for the search result in the list view.
* The task can be claimed, claimed declined or claimed approved.
*/
export abstract class AbstractClaimedSearchResultListElementComponent extends SearchResultListElementComponent<ClaimedTaskSearchResult, ClaimedTask> {
/**
* A boolean representing if to show submitter information
*/
public showSubmitter = true;
/**
* Represent item's status
*/
public status = MyDspaceItemStatusType.VALIDATION;
/**
* The workflowitem object that belonging to the result object
*/
public workflowitemRD$: Observable<RemoteData<WorkflowItem>>;
protected constructor(
protected linkService: LinkService,
protected truncatableService: TruncatableService
) {
super(truncatableService);
}
/**
* Initialize all instance variables
*/
ngOnInit() {
super.ngOnInit();
this.linkService.resolveLinks(this.dso, followLink('workflowitem', null, true,
followLink('item'), followLink('submitter')
), followLink('action'));
this.workflowitemRD$ = this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>;
}
}

View File

@@ -0,0 +1,10 @@
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
<div class="alert alert-success w-100" role="alert">
<h4 class="alert-heading">Approved</h4>
<ds-item-list-preview *ngIf="workflowitem"
[item]="(workflowitem?.item | async)?.payload"
[object]="object"
[status]="status"
[showSubmitter]="showSubmitter"></ds-item-list-preview>
</div>
</ng-container>

View File

@@ -0,0 +1,101 @@
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { of as observableOf } from 'rxjs';
import { Item } from '../../../../../core/shared/item.model';
import { createSuccessfulRemoteDataObject } from '../../../../remote-data.utils';
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
import { ClaimedTask } from '../../../../../core/tasks/models/claimed-task-object.model';
import { getMockLinkService } from '../../../../mocks/link-service.mock';
import { VarDirective } from '../../../../utils/var.directive';
import { TruncatableService } from '../../../../truncatable/truncatable.service';
import { LinkService } from '../../../../../core/cache/builders/link.service';
import { MyDspaceItemStatusType } from '../../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
import { ClaimedApprovedTaskSearchResult } from '../../../../object-collection/shared/claimed-approved-task-search-result.model';
import { ClaimedApprovedSearchResultListElementComponent } from './claimed-approved-search-result-list-element.component';
let component: ClaimedApprovedSearchResultListElementComponent;
let fixture: ComponentFixture<ClaimedApprovedSearchResultListElementComponent>;
const mockResultObject: ClaimedApprovedTaskSearchResult = new ClaimedApprovedTaskSearchResult();
mockResultObject.hitHighlights = {};
const item = Object.assign(new Item(), {
bundles: 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 = createSuccessfulRemoteDataObject(item);
const workflowitem = Object.assign(new WorkflowItem(), { item: observableOf(rdItem) });
const rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
mockResultObject.indexableObject = Object.assign(new ClaimedTask(), { workflowitem: observableOf(rdWorkflowitem) });
const linkService = getMockLinkService();
describe('ClaimedApprovedSearchResultListElementComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule],
declarations: [ClaimedApprovedSearchResultListElementComponent, VarDirective],
providers: [
{ provide: TruncatableService, useValue: {} },
{ provide: LinkService, useValue: linkService }
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(ClaimedApprovedSearchResultListElementComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(ClaimedApprovedSearchResultListElementComponent);
component = fixture.componentInstance;
}));
beforeEach(() => {
component.dso = mockResultObject.indexableObject;
fixture.detectChanges();
});
it('should init workflowitem properly', (done) => {
component.workflowitemRD$.subscribe((workflowitemRD) => {
expect(linkService.resolveLinks).toHaveBeenCalledWith(
component.dso,
jasmine.objectContaining({ name: 'workflowitem' }),
jasmine.objectContaining({ name: 'action' })
);
expect(workflowitemRD.payload).toEqual(workflowitem);
done();
});
});
it('should have properly status', () => {
expect(component.status).toEqual(MyDspaceItemStatusType.APPROVED);
});
});

View File

@@ -0,0 +1,36 @@
import { Component } from '@angular/core';
import { LocationStrategy, PathLocationStrategy } from '@angular/common';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { ClaimedApprovedTaskSearchResult } from '../../../../object-collection/shared/claimed-approved-task-search-result.model';
import { listableObjectComponent } from '../../../../object-collection/shared/listable-object/listable-object.decorator';
import { AbstractClaimedSearchResultListElementComponent } from '../abstract-claimed-search-result-list-element.component';
import { LinkService } from '../../../../../core/cache/builders/link.service';
import { TruncatableService } from '../../../../truncatable/truncatable.service';
import { MyDspaceItemStatusType } from '../../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
/**
* This component renders claimed task approved object for the search result in the list view.
*/
@Component({
selector: 'ds-claimed-approved-search-result-list-element',
styleUrls: ['../../../search-result-list-element/search-result-list-element.component.scss'],
templateUrl: './claimed-approved-search-result-list-element.component.html',
providers: [Location, { provide: LocationStrategy, useClass: PathLocationStrategy }]
})
@listableObjectComponent(ClaimedApprovedTaskSearchResult, ViewMode.ListElement)
export class ClaimedApprovedSearchResultListElementComponent extends AbstractClaimedSearchResultListElementComponent {
/**
* Represent item's status
*/
public status = MyDspaceItemStatusType.APPROVED;
constructor(
protected linkService: LinkService,
protected truncatableService: TruncatableService
) {
super(linkService, truncatableService);
}
}

View File

@@ -0,0 +1,101 @@
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { of as observableOf } from 'rxjs';
import { ClaimedDeclinedSearchResultListElementComponent } from './claimed-declined-search-result-list-element.component';
import { ClaimedDeclinedTaskSearchResult } from '../../../../object-collection/shared/claimed-declined-task-search-result.model';
import { Item } from '../../../../../core/shared/item.model';
import { createSuccessfulRemoteDataObject } from '../../../../remote-data.utils';
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
import { ClaimedTask } from '../../../../../core/tasks/models/claimed-task-object.model';
import { getMockLinkService } from '../../../../mocks/link-service.mock';
import { VarDirective } from '../../../../utils/var.directive';
import { TruncatableService } from '../../../../truncatable/truncatable.service';
import { LinkService } from '../../../../../core/cache/builders/link.service';
import { MyDspaceItemStatusType } from '../../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
let component: ClaimedDeclinedSearchResultListElementComponent;
let fixture: ComponentFixture<ClaimedDeclinedSearchResultListElementComponent>;
const mockResultObject: ClaimedDeclinedTaskSearchResult = new ClaimedDeclinedTaskSearchResult();
mockResultObject.hitHighlights = {};
const item = Object.assign(new Item(), {
bundles: 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 = createSuccessfulRemoteDataObject(item);
const workflowitem = Object.assign(new WorkflowItem(), { item: observableOf(rdItem) });
const rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
mockResultObject.indexableObject = Object.assign(new ClaimedTask(), { workflowitem: observableOf(rdWorkflowitem) });
const linkService = getMockLinkService();
describe('ClaimedDeclinedSearchResultListElementComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule],
declarations: [ClaimedDeclinedSearchResultListElementComponent, VarDirective],
providers: [
{ provide: TruncatableService, useValue: {} },
{ provide: LinkService, useValue: linkService }
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(ClaimedDeclinedSearchResultListElementComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(ClaimedDeclinedSearchResultListElementComponent);
component = fixture.componentInstance;
}));
beforeEach(() => {
component.dso = mockResultObject.indexableObject;
fixture.detectChanges();
});
it('should init workflowitem properly', (done) => {
component.workflowitemRD$.subscribe((workflowitemRD) => {
expect(linkService.resolveLinks).toHaveBeenCalledWith(
component.dso,
jasmine.objectContaining({ name: 'workflowitem' }),
jasmine.objectContaining({ name: 'action' })
);
expect(workflowitemRD.payload).toEqual(workflowitem);
done();
});
});
it('should have properly status', () => {
expect(component.status).toEqual(MyDspaceItemStatusType.DECLINED);
});
});

View File

@@ -0,0 +1,10 @@
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
<div class="alert alert-secondary w-100" role="alert">
<h4 class="alert-heading">Declined</h4>
<ds-item-list-preview *ngIf="workflowitem"
[item]="(workflowitem?.item | async)?.payload"
[object]="object"
[status]="status"
[showSubmitter]="showSubmitter"></ds-item-list-preview>
</div>
</ng-container>

View File

@@ -0,0 +1,37 @@
import { Component } from '@angular/core';
import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common';
import { AbstractClaimedSearchResultListElementComponent } from '../abstract-claimed-search-result-list-element.component';
import { listableObjectComponent } from '../../../../object-collection/shared/listable-object/listable-object.decorator';
import { ClaimedDeclinedTaskSearchResult } from '../../../../object-collection/shared/claimed-declined-task-search-result.model';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { LinkService } from '../../../../../core/cache/builders/link.service';
import { TruncatableService } from '../../../../truncatable/truncatable.service';
import { MyDspaceItemStatusType } from '../../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
/**
* This component renders claimed task declined object for the search result in the list view.
*/
@Component({
selector: 'ds-claimed-declined-search-result-list-element',
styleUrls: ['../../../search-result-list-element/search-result-list-element.component.scss'],
templateUrl: './claimed-declined-search-result-list-element.component.html',
providers: [Location, { provide: LocationStrategy, useClass: PathLocationStrategy }]
})
@listableObjectComponent(ClaimedDeclinedTaskSearchResult, ViewMode.ListElement)
export class ClaimedDeclinedSearchResultListElementComponent extends AbstractClaimedSearchResultListElementComponent {
/**
* Represent item's status
*/
public status = MyDspaceItemStatusType.DECLINED;
constructor(
protected linkService: LinkService,
protected truncatableService: TruncatableService
) {
super(linkService, truncatableService);
}
}

View File

@@ -4,7 +4,6 @@
[object]="object"
[showSubmitter]="showSubmitter"
[status]="status"></ds-item-list-preview>
<ds-claimed-task-actions *ngIf="workflowitem" [object]="dso"></ds-claimed-task-actions>
<ds-claimed-task-actions *ngIf="workflowitem" [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-claimed-task-actions>
</ng-container>

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { of as observableOf } from 'rxjs';
@@ -15,12 +15,11 @@ import { TruncatableService } from '../../../truncatable/truncatable.service';
import { VarDirective } from '../../../utils/var.directive';
import { LinkService } from '../../../../core/cache/builders/link.service';
import { getMockLinkService } from '../../../mocks/link-service.mock';
import { By } from '@angular/platform-browser';
let component: ClaimedSearchResultListElementComponent;
let fixture: ComponentFixture<ClaimedSearchResultListElementComponent>;
const compIndex = 1;
const mockResultObject: ClaimedTaskSearchResult = new ClaimedTaskSearchResult();
mockResultObject.hitHighlights = {};
@@ -99,4 +98,16 @@ describe('ClaimedSearchResultListElementComponent', () => {
it('should have properly status', () => {
expect(component.status).toEqual(MyDspaceItemStatusType.VALIDATION);
});
it('should forward claimed-task-actions processComplete event to reloadObject event emitter', fakeAsync(() => {
spyOn(component.reloadedObject, 'emit').and.callThrough();
const actionPayload: any = { reloadedObject: {}};
const actionsComponent = fixture.debugElement.query(By.css('ds-claimed-task-actions'));
actionsComponent.triggerEventHandler('processCompleted', actionPayload)
tick();
expect(component.reloadedObject.emit).toHaveBeenCalledWith(actionPayload.reloadedObject);
}));
});

View File

@@ -1,63 +1,27 @@
import { Component } from '@angular/core';
import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common';
import { Observable } from 'rxjs';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator';
import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model';
import { SearchResultListElementComponent } from '../../search-result-list-element/search-result-list-element.component';
import { followLink } from '../../../utils/follow-link-config.model';
import { LinkService } from '../../../../core/cache/builders/link.service';
import { TruncatableService } from '../../../truncatable/truncatable.service';
import { AbstractClaimedSearchResultListElementComponent } from './abstract-claimed-search-result-list-element.component';
/**
* This component renders claimed task object for the search result in the list view.
*/
@Component({
selector: 'ds-claimed-search-result-list-element',
styleUrls: ['../../search-result-list-element/search-result-list-element.component.scss'],
templateUrl: './claimed-search-result-list-element.component.html',
providers: [Location, { provide: LocationStrategy, useClass: PathLocationStrategy }]
})
@listableObjectComponent(ClaimedTaskSearchResult, ViewMode.ListElement)
export class ClaimedSearchResultListElementComponent extends SearchResultListElementComponent<ClaimedTaskSearchResult, ClaimedTask> {
/**
* A boolean representing if to show submitter information
*/
public showSubmitter = true;
/**
* Represent item's status
*/
public status = MyDspaceItemStatusType.VALIDATION;
/**
* The workflowitem object that belonging to the result object
*/
public workflowitemRD$: Observable<RemoteData<WorkflowItem>>;
export class ClaimedSearchResultListElementComponent extends AbstractClaimedSearchResultListElementComponent {
constructor(
protected linkService: LinkService,
protected truncatableService: TruncatableService
) {
super(truncatableService);
super(linkService, truncatableService);
}
/**
* Initialize all instance variables
*/
ngOnInit() {
super.ngOnInit();
this.linkService.resolveLinks(this.dso, followLink('workflowitem', null, true,
followLink('item'), followLink('submitter')
), followLink('action'));
this.workflowitemRD$ = this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>;
}
}

View File

@@ -2,4 +2,4 @@
[object]="object"
[status]="status"></ds-item-list-preview>
<ds-item-actions [object]="dso"></ds-item-actions>
<ds-item-actions [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-item-actions>

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { of as observableOf } from 'rxjs';
@@ -9,12 +9,11 @@ import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspa
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
import { ItemSearchResultListElementSubmissionComponent } from './item-search-result-list-element-submission.component';
import { TruncatableService } from '../../../truncatable/truncatable.service';
import { By } from '@angular/platform-browser';
let component: ItemSearchResultListElementSubmissionComponent;
let fixture: ComponentFixture<ItemSearchResultListElementSubmissionComponent>;
const compIndex = 1;
const mockResultObject: ItemSearchResult = new ItemSearchResult();
mockResultObject.hitHighlights = {};
@@ -75,4 +74,16 @@ describe('ItemMyDSpaceResultListElementComponent', () => {
it('should have properly status', () => {
expect(component.status).toEqual(MyDspaceItemStatusType.ARCHIVED);
});
it('should forward item-actions processComplete event to reloadObject event emitter', fakeAsync(() => {
spyOn(component.reloadedObject, 'emit').and.callThrough();
const actionPayload: any = { reloadedObject: {}};
const actionsComponent = fixture.debugElement.query(By.css('ds-item-actions'));
actionsComponent.triggerEventHandler('processCompleted', actionPayload)
tick();
expect(component.reloadedObject.emit).toHaveBeenCalledWith(actionPayload.reloadedObject);
}));
});

View File

@@ -4,6 +4,5 @@
[object]="object"
[showSubmitter]="showSubmitter"
[status]="status"></ds-item-list-preview>
<ds-pool-task-actions *ngIf="workflowitem" [object]="dso"></ds-pool-task-actions>
<ds-pool-task-actions id="actions" *ngIf="workflowitem" [object]="dso" (processCompleted)="this.reloadedObject.emit($event.reloadedObject)"></ds-pool-task-actions>
</ng-container>

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA} from '@angular/core';
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { of as observableOf } from 'rxjs';
@@ -15,12 +15,11 @@ import { TruncatableService } from '../../../truncatable/truncatable.service';
import { VarDirective } from '../../../utils/var.directive';
import { LinkService } from '../../../../core/cache/builders/link.service';
import { getMockLinkService } from '../../../mocks/link-service.mock';
import { By } from '@angular/platform-browser';
let component: PoolSearchResultListElementComponent;
let fixture: ComponentFixture<PoolSearchResultListElementComponent>;
const compIndex = 1;
const mockResultObject: PoolTaskSearchResult = new PoolTaskSearchResult();
mockResultObject.hitHighlights = {};
@@ -99,4 +98,15 @@ describe('PoolSearchResultListElementComponent', () => {
it('should have properly status', () => {
expect(component.status).toEqual(MyDspaceItemStatusType.WAITING_CONTROLLER);
});
it('should forward pool-task-actions processCompleted event to the reloadedObject event emitter', fakeAsync(() => {
spyOn(component.reloadedObject, 'emit').and.callThrough();
const actionPayload: any = { reloadedObject: {}};
const actionsComponents = fixture.debugElement.query(By.css('ds-pool-task-actions'));
actionsComponents.triggerEventHandler('processCompleted', actionPayload)
tick();
expect(component.reloadedObject.emit).toHaveBeenCalledWith(actionPayload.reloadedObject);
}));
});

View File

@@ -63,4 +63,5 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen
), followLink('action'));
this.workflowitemRD$ = this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>;
}
}

View File

@@ -4,7 +4,7 @@
[object]="object"
[status]="status"></ds-item-list-preview>
<ds-workflowitem-actions [object]="dso"></ds-workflowitem-actions>
<ds-workflowitem-actions [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-workflowitem-actions>
</ng-container>
<ds-loading
*ngIf="!(item$ | async)"

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { of as observableOf } from 'rxjs';
@@ -15,12 +15,11 @@ import { WorkflowItemSearchResult } from '../../../object-collection/shared/work
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
import { TruncatableService } from '../../../truncatable/truncatable.service';
import { WorkflowItemSearchResultListElementComponent } from './workflow-item-search-result-list-element.component';
import { By } from '@angular/platform-browser';
let component: WorkflowItemSearchResultListElementComponent;
let fixture: ComponentFixture<WorkflowItemSearchResultListElementComponent>;
const compIndex = 1;
const mockResultObject: WorkflowItemSearchResult = new WorkflowItemSearchResult();
mockResultObject.hitHighlights = {};
@@ -96,4 +95,16 @@ describe('WorkflowItemSearchResultListElementComponent', () => {
it('should have properly status', () => {
expect(component.status).toEqual(MyDspaceItemStatusType.WORKFLOW);
});
it('should forward workflowitem-actions processCompleted event to the reloadedObject event emitter', fakeAsync(() => {
spyOn(component.reloadedObject, 'emit').and.callThrough();
const actionPayload: any = { reloadedObject: {}};
const actionsComponent = fixture.debugElement.query(By.css('ds-workflowitem-actions'));
actionsComponent.triggerEventHandler('processCompleted', actionPayload)
tick();
expect(component.reloadedObject.emit).toHaveBeenCalledWith(actionPayload.reloadedObject);
}));
});

View File

@@ -4,7 +4,7 @@
[object]="object"
[status]="status"></ds-item-list-preview>
<ds-workspaceitem-actions [object]="dso"></ds-workspaceitem-actions>
<ds-workspaceitem-actions [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-workspaceitem-actions>
</ng-container>
<ds-loading
*ngIf="!(item$ | async)"

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA} from '@angular/core';
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { of as observableOf } from 'rxjs';
@@ -15,12 +15,11 @@ import { WorkflowItemSearchResult } from '../../../object-collection/shared/work
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
import { TruncatableService } from '../../../truncatable/truncatable.service';
import { WorkspaceItemSearchResultListElementComponent } from './workspace-item-search-result-list-element.component';
import { By } from '@angular/platform-browser';
let component: WorkspaceItemSearchResultListElementComponent;
let fixture: ComponentFixture<WorkspaceItemSearchResultListElementComponent>;
const compIndex = 1;
const mockResultObject: WorkflowItemSearchResult = new WorkflowItemSearchResult();
mockResultObject.hitHighlights = {};
@@ -95,4 +94,17 @@ describe('WorkspaceItemSearchResultListElementComponent', () => {
it('should have properly status', () => {
expect(component.status).toEqual(MyDspaceItemStatusType.WORKSPACE);
});
it('should forward workspaceitem-actions processCompleted event to the reloadedObject event emitter', fakeAsync(() => {
spyOn(component.reloadedObject, 'emit').and.callThrough();
const actionPayload: any = { reloadedObject: {}};
const actionsComponent = fixture.debugElement.query(By.css('ds-workspaceitem-actions'));
actionsComponent.triggerEventHandler('processCompleted', actionPayload)
tick();
expect(component.reloadedObject.emit).toHaveBeenCalledWith(actionPayload.reloadedObject);
}));
});

View File

@@ -1,4 +1,4 @@
import { Component, Inject, OnInit } from '@angular/core';
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { Observable } from 'rxjs';
import { SearchResult } from '../../search/search-result.model';
@@ -12,7 +12,6 @@ import { Metadata } from '../../../core/shared/metadata.utils';
selector: 'ds-search-result-list-element',
template: ``
})
export class SearchResultListElementComponent<T extends SearchResult<K>, K extends DSpaceObject> extends AbstractListableElementComponent<T> implements OnInit {
/**
* The DSpaceObject of the search result

View File

@@ -22,9 +22,9 @@ export const environment: GlobalConfig = {
// The REST API server settings.
// NOTE: these must be "synced" with the 'dspace.server.url' setting in your backend's local.cfg.
rest: {
ssl: true,
host: 'dspace7.4science.cloud',
port: 443,
ssl: false,
host: 'localhost',
port: 8080,
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
nameSpace: '/server',
},