[CST-4320] No way to view an Item when in simple "Approve/Reject" workflow stage

This commit is contained in:
Alessandro Martelli
2021-06-24 15:28:26 +02:00
parent ddb7d5181f
commit a572b0acea
15 changed files with 288 additions and 4 deletions

View File

@@ -0,0 +1,26 @@
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { Component } from '@angular/core';
import { WorkflowItemViewComponent } from './workflow-item-view.component';
/**
* Themed wrapper for WorkflowItemViewComponent
*/
@Component({
selector: 'ds-themed-workflow-item-view',
styleUrls: [],
templateUrl: './../../shared/theme-support/themed.component.html'
})
export class ThemedWorkflowItemViewComponent extends ThemedComponent<WorkflowItemViewComponent> {
protected getComponentName(): string {
return 'WorkflowItemViewComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/+workflowitems-edit-page/workflow-item-view/workflow-item-view.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./workflow-item-view.component`);
}
}

View File

@@ -0,0 +1,9 @@
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
<div class="item-page" *ngIf="itemRD?.hasSucceeded">
<div *ngIf="itemRD?.payload as item">
<ds-untyped-item [object]="item"></ds-untyped-item>
</div>
</div>
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
<ds-loading *ngIf="itemRD?.isLoading" message="{{'loading.item' | translate}}"></ds-loading>
</div>

View File

@@ -0,0 +1,114 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { ActivatedRoute, Router } from '@angular/router';
import { RouteService } from '../../core/services/route.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { WorkflowItem } from '../../core/submission/models/workflowitem.model';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { VarDirective } from '../../shared/utils/var.directive';
import { of as observableOf } from 'rxjs';
import { RequestService } from '../../core/data/request.service';
import {
createFailedRemoteDataObject$,
createPendingRemoteDataObject$,
createSuccessfulRemoteDataObject,
createSuccessfulRemoteDataObject$
} from '../../shared/remote-data.utils';
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
import { RouterStub } from '../../shared/testing/router.stub';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { Item } from '../../core/shared/item.model';
import { createPaginatedList } from '../../shared/testing/utils.test';
import { createRelationshipsObservable } from '../../+item-page/simple/item-types/shared/item.component.spec';
import { WorkflowItemViewComponent } from './workflow-item-view.component';
import { By } from '@angular/platform-browser';
describe('WorkflowItemViewComponent', () => {
const mockItem: Item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
metadata: [],
relationships: createRelationshipsObservable()
});
const mockWfi = new WorkflowItem();
mockWfi.item = createSuccessfulRemoteDataObject$(mockItem);
const mockRoute = Object.assign(new ActivatedRouteStub(), {
data: observableOf({ wfi: createSuccessfulRemoteDataObject(mockWfi) })
});
let comp: WorkflowItemViewComponent;
let fixture: ComponentFixture<WorkflowItemViewComponent>;
let wfi;
let itemRD$;
let id;
function init() {
itemRD$ = createSuccessfulRemoteDataObject$(itemRD$);
wfi = new WorkflowItem();
wfi.item = itemRD$;
id = 'de11b5e5-064a-4e98-a7ac-a1a6a65ddf80';
}
beforeEach(waitForAsync(() => {
init();
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock
}
})],
declarations: [WorkflowItemViewComponent, VarDirective],
providers: [
{ provide: ActivatedRoute, useValue: mockRoute },
{ provide: Router, useClass: RouterStub },
{ provide: RouteService, useValue: {} },
{ provide: NotificationsService, useClass: NotificationsServiceStub },
{ provide: RequestService, useValue: getMockRequestService() },
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(waitForAsync(() => {
fixture = TestBed.createComponent(WorkflowItemViewComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
}));
it('should create', () => {
expect(comp).toBeTruthy();
});
describe('when the item is loading', () => {
beforeEach(() => {
comp.itemRD$ = createPendingRemoteDataObject$();
// comp.itemRD$ = observableOf(new RemoteData(true, true, true, null, undefined));
fixture.detectChanges();
});
it('should display a loading component', () => {
const loading = fixture.debugElement.query(By.css('ds-loading'));
expect(loading.nativeElement).toBeDefined();
});
});
describe('when the item failed loading', () => {
beforeEach(() => {
comp.itemRD$ = createFailedRemoteDataObject$('server error', 500);
fixture.detectChanges();
});
it('should display an error component', () => {
const error = fixture.debugElement.query(By.css('ds-error'));
expect(error.nativeElement).toBeDefined();
});
});
});

View File

@@ -0,0 +1,40 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { ActivatedRoute, Data } from '@angular/router';
import { map, switchMap } from 'rxjs/operators';import { RemoteData } from '../../core/data/remote-data';
import {
getAllSucceededRemoteData,
getRemoteDataPayload
} from '../../core/shared/operators';
import { WorkflowItem } from '../../core/submission/models/workflowitem.model';
import { Item } from '../../core/shared/item.model';
import { ViewMode } from '../../core/shared/view-mode.model';
@Component({
selector: 'ds-workflow-item-view',
templateUrl: './workflow-item-view.component.html'
})
/**
* Component representing a page to delete a workflow item
*/
export class WorkflowItemViewComponent implements OnInit {
public wfi$: Observable<WorkflowItem>;
public itemRD$: Observable<RemoteData<Item>>;
/**
* The view-mode we're currently on
*/
viewMode = ViewMode.StandalonePage;
constructor(protected route: ActivatedRoute) {
}
ngOnInit() {
this.wfi$ = this.route.data.pipe(map((data: Data) => data.wfi as RemoteData<WorkflowItem>), getRemoteDataPayload());
this.itemRD$ = this.wfi$.pipe(switchMap((wfi: WorkflowItem) => (wfi.item as Observable<RemoteData<Item>>).pipe(getAllSucceededRemoteData())));
}
}

View File

@@ -8,6 +8,9 @@ export function getWorkflowItemPageRoute(wfiId: string) {
export function getWorkflowItemEditRoute(wfiId: string) {
return new URLCombiner(getWorkflowItemModuleRoute(), wfiId, WORKFLOW_ITEM_EDIT_PATH).toString();
}
export function getWorkflowItemViewRoute(wfiId: string) {
return new URLCombiner(getWorkflowItemModuleRoute(), wfiId, WORKFLOW_ITEM_VIEW_PATH).toString();
}
export function getWorkflowItemDeleteRoute(wfiId: string) {
return new URLCombiner(getWorkflowItemModuleRoute(), wfiId, WORKFLOW_ITEM_DELETE_PATH).toString();
@@ -19,4 +22,5 @@ export function getWorkflowItemSendBackRoute(wfiId: string) {
export const WORKFLOW_ITEM_EDIT_PATH = 'edit';
export const WORKFLOW_ITEM_DELETE_PATH = 'delete';
export const WORKFLOW_ITEM_VIEW_PATH = 'view';
export const WORKFLOW_ITEM_SEND_BACK_PATH = 'sendback';

View File

@@ -6,12 +6,14 @@ import { WorkflowItemPageResolver } from './workflow-item-page.resolver';
import {
WORKFLOW_ITEM_DELETE_PATH,
WORKFLOW_ITEM_EDIT_PATH,
WORKFLOW_ITEM_SEND_BACK_PATH
WORKFLOW_ITEM_SEND_BACK_PATH,
WORKFLOW_ITEM_VIEW_PATH
} from './workflowitems-edit-page-routing-paths';
import { ThemedSubmissionEditComponent } from '../submission/edit/themed-submission-edit.component';
import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component';
import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/themed-workflow-item-send-back.component';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { ThemedWorkflowItemViewComponent } from './workflow-item-view/themed-workflow-item-view.component';
@NgModule({
imports: [
@@ -29,6 +31,15 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
},
data: { title: 'workflow-item.edit.title', breadcrumbKey: 'workflow-item.edit' }
},
{
canActivate: [AuthenticatedGuard],
path: WORKFLOW_ITEM_VIEW_PATH,
component: ThemedWorkflowItemViewComponent,
resolve: {
breadcrumb: I18nBreadcrumbResolver
},
data: { title: 'workflow-item.view.title', breadcrumbKey: 'workflow-item.view' }
},
{
canActivate: [AuthenticatedGuard],
path: WORKFLOW_ITEM_DELETE_PATH,

View File

@@ -7,6 +7,10 @@ import { WorkflowItemDeleteComponent } from './workflow-item-delete/workflow-ite
import { WorkflowItemSendBackComponent } from './workflow-item-send-back/workflow-item-send-back.component';
import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component';
import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/themed-workflow-item-send-back.component';
import { WorkflowItemViewComponent } from './workflow-item-view/workflow-item-view.component';
import { ThemedWorkflowItemViewComponent } from './workflow-item-view/themed-workflow-item-view.component';
import { StatisticsModule } from '../statistics/statistics.module';
import { ItemPageModule } from '../+item-page/item-page.module';
@NgModule({
imports: [
@@ -14,8 +18,17 @@ import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/t
CommonModule,
SharedModule,
SubmissionModule,
StatisticsModule,
ItemPageModule
],
declarations: [WorkflowItemDeleteComponent, ThemedWorkflowItemDeleteComponent, WorkflowItemSendBackComponent, ThemedWorkflowItemSendBackComponent]
declarations: [
WorkflowItemDeleteComponent,
ThemedWorkflowItemDeleteComponent,
WorkflowItemSendBackComponent,
ThemedWorkflowItemSendBackComponent,
WorkflowItemViewComponent,
ThemedWorkflowItemViewComponent
]
})
/**
* This module handles all modules that need to access the workflowitems edit page.

View File

@@ -5,6 +5,15 @@
[object]="object"
(processCompleted)="this.processCompleted.emit($event)">
</ds-claimed-task-actions-loader>
<ng-container *ngIf="hasViewAction(workflowAction)">
<button class="btn btn-primary workflow-view"
ngbTooltip="{{'submission.workflow.generic.view-help' | translate}}"
[routerLink]="[getWorkflowItemViewRoute((workflowitem$ | async))]">
<i class="fa fa-info-circle"></i> {{"submission.workflow.generic.view" | translate}}
</button>
</ng-container>
<ds-claimed-task-actions-loader [option]="returnToPoolOption"
[object]="object"
(processCompleted)="this.processCompleted.emit($event)">

View File

@@ -23,6 +23,7 @@ import { WorkflowActionDataService } from '../../../core/data/workflow-action-da
import { WorkflowAction } from '../../../core/tasks/models/workflow-action-object.model';
import { VarDirective } from '../../utils/var.directive';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../remote-data.utils';
import { By } from '@angular/platform-browser';
let component: ClaimedTaskActionsComponent;
let fixture: ComponentFixture<ClaimedTaskActionsComponent>;
@@ -81,7 +82,7 @@ function init() {
}
});
rdItem = createSuccessfulRemoteDataObject(item);
workflowitem = Object.assign(new WorkflowItem(), { item: observableOf(rdItem) });
workflowitem = Object.assign(new WorkflowItem(), { item: observableOf(rdItem), id: '333' });
rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
mockObject = Object.assign(new ClaimedTask(), { workflowitem: observableOf(rdWorkflowitem), id: '1234' });
workflowAction = Object.assign(new WorkflowAction(), { id: 'action-1', options: ['option-1', 'option-2'] });
@@ -91,7 +92,7 @@ function init() {
});
}
describe('ClaimedTaskActionsComponent', () => {
fdescribe('ClaimedTaskActionsComponent', () => {
beforeEach(waitForAsync(() => {
init();
TestBed.configureTestingModule({
@@ -159,4 +160,26 @@ describe('ClaimedTaskActionsComponent', () => {
expect(notificationsServiceStub.error).toHaveBeenCalled();
});
}));
describe('when edit options is not available', () => {
it('should display a view button', waitForAsync(() => {
component.object = null;
component.initObjects(mockObject);
fixture.detectChanges();
fixture.whenStable().then(() => {
const debugElement = fixture.debugElement.query(By.css('.workflow-view'));
expect(debugElement).toBeTruthy();
expect(debugElement.nativeElement.innerText).toBe('submission.workflow.generic.view');
});
}));
it('getWorkflowItemViewRoute should return the combined uri to show a workspaceitem', waitForAsync(() => {
const href = component.getWorkflowItemViewRoute(workflowitem);
expect(href).toEqual('/workflowitems/333/view');
}));
});
});

View File

@@ -17,6 +17,7 @@ import { SearchService } from '../../../core/shared/search/search.service';
import { WorkflowAction } from '../../../core/tasks/models/workflow-action-object.model';
import { WorkflowActionDataService } from '../../../core/data/workflow-action-data.service';
import { WORKFLOW_TASK_OPTION_RETURN_TO_POOL } from './return-to-pool/claimed-task-actions-return-to-pool.component';
import { getWorkflowItemViewRoute } from '../../../+workflowitems-edit-page/workflowitems-edit-page-routing-paths';
/**
* This component represents actions related to ClaimedTask object.
@@ -85,6 +86,7 @@ export class ClaimedTaskActionsComponent extends MyDSpaceActionsComponent<Claime
*/
initObjects(object: ClaimedTask) {
this.object = object;
this.workflowitem$ = (this.object.workflowitem as Observable<RemoteData<WorkflowItem>>).pipe(
filter((rd: RemoteData<WorkflowItem>) => ((!rd.isRequestPending) && isNotUndefined(rd.payload))),
map((rd: RemoteData<WorkflowItem>) => rd.payload),
@@ -100,4 +102,19 @@ export class ClaimedTaskActionsComponent extends MyDSpaceActionsComponent<Claime
this.actionRD$ = object.action;
}
/**
* Check if claimed task actions should display a view item button.
* @param workflowAction
*/
hasViewAction(workflowAction: WorkflowAction) {
return !workflowAction?.options.includes('submit_edit_metadata');
}
/**
* Get the workflowitem view route.
*/
getWorkflowItemViewRoute(workflowitem: WorkflowItem): string {
return getWorkflowItemViewRoute(workflowitem?.id);
}
}

View File

@@ -3588,6 +3588,8 @@
"workflow-item.edit.title": "Edit workflowitem",
"workflow-item.edit.title": "View workflowitem",
"workflow-item.delete.notification.success.title": "Deleted",
"workflow-item.delete.notification.success.content": "This workflow item was successfully deleted",

View File

@@ -0,0 +1,14 @@
import { Component } from '@angular/core';
import { WorkflowItemViewComponent as BaseComponent } from '../../../../../app/+workflowitems-edit-page/workflow-item-view/workflow-item-view.component';
@Component({
selector: 'ds-workflow-item-view',
// styleUrls: ['workflow-item-view.component.scss'],
// templateUrl: './workflow-item-view.component.html'
templateUrl: '../../../../../app/+workflowitems-edit-page/workflow-item-view/workflow-item-view.component.html'
})
/**
* Component representing a page to view a workflow item
*/
export class WorkflowItemViewComponent extends BaseComponent {
}

View File

@@ -79,6 +79,7 @@ import { HeaderComponent } from './app/header/header.component';
import { FooterComponent } from './app/footer/footer.component';
import { BreadcrumbsComponent } from './app/breadcrumbs/breadcrumbs.component';
import { HeaderNavbarWrapperComponent } from './app/header-nav-wrapper/header-navbar-wrapper.component';
import { WorkflowItemViewComponent } from './app/+workflowitems-edit-page/workflow-item-view/workflow-item-view.component';
const DECLARATIONS = [
HomePageComponent,
@@ -115,6 +116,7 @@ const DECLARATIONS = [
SubmissionSubmitComponent,
WorkflowItemDeleteComponent,
WorkflowItemSendBackComponent,
WorkflowItemViewComponent,
FooterComponent,
HeaderComponent,
NavbarComponent,