diff --git a/package.json b/package.json index 6fd39420b5..4c6bd31cac 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "clean:prod": "yarn run clean:coverage && yarn run clean:doc && yarn run clean:dist && yarn run clean:log && yarn run clean:json && yarn run clean:bld", "clean": "yarn run clean:prod && yarn run clean:node && yarn run clean:env", "clean:env": "rimraf src/environments/environment.ts", - "sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts" + "sync-i18n": "yarn run config:dev && ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts" }, "browser": { "fs": false, diff --git a/src/app/+admin/admin-routing.module.ts b/src/app/+admin/admin-routing.module.ts index aa47c93102..3d910761b8 100644 --- a/src/app/+admin/admin-routing.module.ts +++ b/src/app/+admin/admin-routing.module.ts @@ -3,6 +3,8 @@ import { RouterModule } from '@angular/router'; import { getAdminModulePath } from '../app-routing.module'; import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; +import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; import { URLCombiner } from '../core/url-combiner/url-combiner'; const REGISTRIES_MODULE_PATH = 'registries'; @@ -32,8 +34,18 @@ export function getAccessControlModulePath() { resolve: { breadcrumb: I18nBreadcrumbResolver }, component: AdminSearchPageComponent, data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' } - } - ]), + }, + { + path: 'workflow', + resolve: { breadcrumb: I18nBreadcrumbResolver }, + component: AdminWorkflowPageComponent, + data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' } + }, + ]) + ], + providers: [ + I18nBreadcrumbResolver, + I18nBreadcrumbsService ] }) export class AdminRoutingModule { diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts index 79aad4599d..5d885b918b 100644 --- a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts @@ -439,6 +439,19 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { icon: 'cogs', index: 9 }, + /* Workflow */ + { + id: 'workflow', + active: false, + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.workflow', + link: '/admin/workflow' + } as LinkMenuItemModel, + icon: 'user-check', + index: 10 + }, ]; menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection)); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-page.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-page.component.html new file mode 100644 index 0000000000..404af131d1 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-page.component.html @@ -0,0 +1 @@ + diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-page.component.scss b/src/app/+admin/admin-workflow-page/admin-workflow-page.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-page.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-page.component.spec.ts new file mode 100644 index 0000000000..d329497473 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-page.component.spec.ts @@ -0,0 +1,27 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminWorkflowPageComponent } from './admin-workflow-page.component'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +describe('AdminSearchPageComponent', () => { + let component: AdminWorkflowPageComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ AdminWorkflowPageComponent ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AdminWorkflowPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-page.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-page.component.ts new file mode 100644 index 0000000000..8c86c8ec98 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-page.component.ts @@ -0,0 +1,18 @@ +import { Component, OnInit } from '@angular/core'; +import { Context } from '../../core/shared/context.model'; + +@Component({ + selector: 'ds-admin-workflow-page', + templateUrl: './admin-workflow-page.component.html', + styleUrls: ['./admin-workflow-page.component.scss'] +}) + +/** + * Component that represents a workflow item search page for administrators + */ +export class AdminWorkflowPageComponent { + /** + * The context of this page + */ + context: Context = Context.AdminWorkflowSearch; +} diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.html new file mode 100644 index 0000000000..87bae0c261 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.html @@ -0,0 +1,12 @@ + + + + + {{ "admin.workflow.item.workflow" | translate }} + + + + + + + diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.scss b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts new file mode 100644 index 0000000000..2f3f88fa70 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.spec.ts @@ -0,0 +1,84 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { TranslateModule } from '@ngx-translate/core'; +import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { RouterTestingModule } from '@angular/router/testing'; +import { WorkflowItemSearchResultAdminWorkflowGridElementComponent } from './workflow-item-search-result-admin-workflow-grid-element.component'; +import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; +import { LinkService } from '../../../../../core/cache/builders/link.service'; +import { followLink } from '../../../../../shared/utils/follow-link-config.model'; +import { Item } from '../../../../../core/shared/item.model'; +import { PublicationGridElementComponent } from '../../../../../shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component'; +import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive'; +import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; +import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; +import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock'; + +describe('WorkflowItemAdminWorkflowGridElementComponent', () => { + let component: WorkflowItemSearchResultAdminWorkflowGridElementComponent; + let fixture: ComponentFixture; + let id; + let wfi; + let itemRD$; + let linkService; + let object; + + function init() { + itemRD$ = createSuccessfulRemoteDataObject$(new Item()); + id = '780b2588-bda5-4112-a1cd-0b15000a5339'; + object = new WorkflowItemSearchResult() + wfi = new WorkflowItem(); + wfi.item = itemRD$; + object.indexableObject = wfi; + linkService = getMockLinkService(); + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule( + { + declarations: [WorkflowItemSearchResultAdminWorkflowGridElementComponent, PublicationGridElementComponent, ListableObjectDirective], + imports: [ + NoopAnimationsModule, + TranslateModule.forRoot(), + RouterTestingModule.withRoutes([]), + ], + providers: [ + { provide: LinkService, useValue: linkService }, + { provide: TruncatableService, useValue: {} }, + { provide: BitstreamDataService, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .overrideComponent(WorkflowItemSearchResultAdminWorkflowGridElementComponent, { + set: { + entryComponents: [PublicationGridElementComponent] + } + }) + .compileComponents(); + })); + + beforeEach(() => { + linkService.resolveLink.and.callFake((a) => a); + fixture = TestBed.createComponent(WorkflowItemSearchResultAdminWorkflowGridElementComponent); + component = fixture.componentInstance; + component.object = object; + component.linkTypes = CollectionElementLinkType; + component.index = 0; + component.viewModes = ViewMode; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should retrieve the item using the link service', () => { + expect(linkService.resolveLink).toHaveBeenCalledWith(wfi, followLink('item')); + }); +}); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts new file mode 100644 index 0000000000..7abe99cf52 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts @@ -0,0 +1,98 @@ +import { Component, ComponentFactoryResolver, ElementRef, ViewChild } from '@angular/core'; +import { Item } from '../../../../../core/shared/item.model'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { getListableObjectComponent, listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { Context } from '../../../../../core/shared/context.model'; +import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component'; +import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; +import { GenericConstructor } from '../../../../../core/shared/generic-constructor'; +import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive'; +import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; +import { Observable } from 'rxjs'; +import { LinkService } from '../../../../../core/cache/builders/link.service'; +import { followLink } from '../../../../../shared/utils/follow-link-config.model'; +import { RemoteData } from '../../../../../core/data/remote-data'; +import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators'; +import { take } from 'rxjs/operators'; +import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; + +@listableObjectComponent(WorkflowItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch) +@Component({ + selector: 'ds-workflow-item-search-result-admin-workflow-grid-element', + styleUrls: ['./workflow-item-search-result-admin-workflow-grid-element.component.scss'], + templateUrl: './workflow-item-search-result-admin-workflow-grid-element.component.html' +}) +/** + * The component for displaying a grid element for an workflow item on the admin workflow search page + */ +export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent { + /** + * Directive used to render the dynamic component in + */ + @ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective; + + /** + * The html child that contains the badges html + */ + @ViewChild('badges', { static: true }) badges: ElementRef; + + /** + * The html child that contains the button html + */ + @ViewChild('buttons', { static: true }) buttons: ElementRef; + + /** + * The item linked to the workflow item + */ + public item$: Observable; + + constructor( + private componentFactoryResolver: ComponentFactoryResolver, + private linkService: LinkService, + protected truncatableService: TruncatableService, + protected bitstreamDataService: BitstreamDataService + ) { + super(truncatableService, bitstreamDataService); + } + + /** + * Setup the dynamic child component + * Initialize the item object from the workflow item + */ + ngOnInit(): void { + super.ngOnInit(); + this.dso = this.linkService.resolveLink(this.dso, followLink('item')); + this.item$ = (this.dso.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()); + this.item$.pipe(take(1)).subscribe((item: Item) => { + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent(item)); + + const viewContainerRef = this.listableObjectDirective.viewContainerRef; + viewContainerRef.clear(); + + const componentRef = viewContainerRef.createComponent( + componentFactory, + 0, + undefined, + [ + [this.badges.nativeElement], + [this.buttons.nativeElement] + ]); + (componentRef.instance as any).object = item; + (componentRef.instance as any).index = this.index; + (componentRef.instance as any).linkType = this.linkType; + (componentRef.instance as any).listID = this.listID; + componentRef.changeDetectorRef.detectChanges(); + } + ) + } + + /** + * Fetch the component depending on the item's relationship type, view mode and context + * @returns {GenericConstructor} + */ + private getComponent(item: Item): GenericConstructor { + return getListableObjectComponent(item.getRenderTypes(), ViewMode.GridElement, undefined) + } + +} diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.html new file mode 100644 index 0000000000..192cc751f2 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.html @@ -0,0 +1,10 @@ + + {{ "admin.workflow.item.workflow" | translate }} + + + diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.scss b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts new file mode 100644 index 0000000000..53f81f96db --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts @@ -0,0 +1,76 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { TranslateModule } from '@ngx-translate/core'; +import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service'; +import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { RouterTestingModule } from '@angular/router/testing'; +import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; +import { WorkflowItemSearchResultAdminWorkflowListElementComponent } from './workflow-item-search-result-admin-workflow-list-element.component'; +import { LinkService } from '../../../../../core/cache/builders/link.service'; +import { followLink } from '../../../../../shared/utils/follow-link-config.model'; +import { Item } from '../../../../../core/shared/item.model'; +import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; +import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock'; + +describe('WorkflowItemAdminWorkflowListElementComponent', () => { + let component: WorkflowItemSearchResultAdminWorkflowListElementComponent; + let fixture: ComponentFixture; + let id; + let wfi; + let itemRD$; + let linkService; + let object; + + function init() { + itemRD$ = createSuccessfulRemoteDataObject$(new Item()); + id = '780b2588-bda5-4112-a1cd-0b15000a5339'; + object = new WorkflowItemSearchResult() + wfi = new WorkflowItem(); + wfi.item = itemRD$; + object.indexableObject = wfi; + linkService = getMockLinkService(); + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule( + { + declarations: [WorkflowItemSearchResultAdminWorkflowListElementComponent], + imports: [ + NoopAnimationsModule, + TranslateModule.forRoot(), + RouterTestingModule.withRoutes([]), + ], + providers: [ + { provide: TruncatableService, useValue: mockTruncatableService }, + { provide: LinkService, useValue: linkService }, + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + linkService.resolveLink.and.callFake((a) => a); + fixture = TestBed.createComponent(WorkflowItemSearchResultAdminWorkflowListElementComponent); + component = fixture.componentInstance; + component.object = object; + component.linkTypes = CollectionElementLinkType; + component.index = 0; + component.viewModes = ViewMode; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should retrieve the item using the link service', () => { + expect(linkService.resolveLink).toHaveBeenCalledWith(wfi, followLink('item')); + }); +}); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.ts new file mode 100644 index 0000000000..80225db09f --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.ts @@ -0,0 +1,44 @@ +import { Component, OnInit } from '@angular/core'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { Context } from '../../../../../core/shared/context.model'; +import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; +import { Observable } from 'rxjs'; +import { LinkService } from '../../../../../core/cache/builders/link.service'; +import { followLink } from '../../../../../shared/utils/follow-link-config.model'; +import { RemoteData } from '../../../../../core/data/remote-data'; +import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators'; +import { Item } from '../../../../../core/shared/item.model'; +import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component'; +import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; + +@listableObjectComponent(WorkflowItemSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch) +@Component({ + selector: 'ds-workflow-item-search-result-admin-workflow-list-element', + styleUrls: ['./workflow-item-search-result-admin-workflow-list-element.component.scss'], + templateUrl: './workflow-item-search-result-admin-workflow-list-element.component.html' +}) +/** + * The component for displaying a list element for an workflow item on the admin workflow search page + */ +export class WorkflowItemSearchResultAdminWorkflowListElementComponent extends SearchResultListElementComponent implements OnInit { + + /** + * The item linked to the workflow item + */ + public item$: Observable; + + constructor(private linkService: LinkService, protected truncatableService: TruncatableService) { + super(truncatableService); + } + + /** + * Initialize the item object from the workflow item + */ + ngOnInit(): void { + super.ngOnInit(); + this.dso = this.linkService.resolveLink(this.dso, followLink('item')); + this.item$ = (this.dso.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()); + } +} diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.html b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.html new file mode 100644 index 0000000000..1a90a4cff4 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.html @@ -0,0 +1,7 @@ + + {{"admin.workflow.item.delete" | translate}} + + + + {{"admin.workflow.item.send-back" | translate}} + diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.scss b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.scss new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.scss @@ -0,0 +1 @@ + diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.spec.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.spec.ts new file mode 100644 index 0000000000..bca2684364 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.spec.ts @@ -0,0 +1,68 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Item } from '../../../core/shared/item.model'; +import { + ITEM_EDIT_DELETE_PATH, + ITEM_EDIT_MOVE_PATH, + ITEM_EDIT_PRIVATE_PATH, + ITEM_EDIT_PUBLIC_PATH, + ITEM_EDIT_REINSTATE_PATH, + ITEM_EDIT_WITHDRAW_PATH +} from '../../../+item-page/edit-item-page/edit-item-page.routing.module'; +import { getItemEditPath } from '../../../+item-page/item-page-routing.module'; +import { URLCombiner } from '../../../core/url-combiner/url-combiner'; +import { WorkflowItemAdminWorkflowActionsComponent } from './workflow-item-admin-workflow-actions.component'; +import { WorkflowItem } from '../../../core/submission/models/workflowitem.model'; +import { getWorkflowItemDeletePath, getWorkflowItemSendBackPath } from '../../../+workflowitems-edit-page/workflowitems-edit-page-routing.module'; + +describe('WorkflowItemAdminWorkflowActionsComponent', () => { + let component: WorkflowItemAdminWorkflowActionsComponent; + let fixture: ComponentFixture; + let id; + let wfi; + + function init() { + id = '780b2588-bda5-4112-a1cd-0b15000a5339'; + wfi = new WorkflowItem(); + wfi.id = id; + } + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + RouterTestingModule.withRoutes([]) + ], + declarations: [WorkflowItemAdminWorkflowActionsComponent], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(WorkflowItemAdminWorkflowActionsComponent); + component = fixture.componentInstance; + component.wfi = wfi; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should render a delete button with the correct link', () => { + const button = fixture.debugElement.query(By.css('a.delete-link')); + const link = button.nativeElement.href; + expect(link).toContain(new URLCombiner(getWorkflowItemDeletePath(wfi.id)).toString()); + }); + + it('should render a move button with the correct link', () => { + const a = fixture.debugElement.query(By.css('a.send-back-link')); + const link = a.nativeElement.href; + expect(link).toContain(new URLCombiner(getWorkflowItemSendBackPath(wfi.id)).toString()); + }); +}); diff --git a/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.ts b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.ts new file mode 100644 index 0000000000..d44f870b14 --- /dev/null +++ b/src/app/+admin/admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component.ts @@ -0,0 +1,39 @@ +import { Component, Input } from '@angular/core'; +import { WorkflowItem } from '../../../core/submission/models/workflowitem.model'; +import { getWorkflowItemDeletePath, getWorkflowItemSendBackPath } from '../../../+workflowitems-edit-page/workflowitems-edit-page-routing.module'; + +@Component({ + selector: 'ds-workflow-item-admin-workflow-actions-element', + styleUrls: ['./workflow-item-admin-workflow-actions.component.scss'], + templateUrl: './workflow-item-admin-workflow-actions.component.html' +}) +/** + * The component for displaying the actions for a list element for an item on the admin workflow search page + */ +export class WorkflowItemAdminWorkflowActionsComponent { + + /** + * The workflow item to perform the actions on + */ + @Input() public wfi: WorkflowItem; + + /** + * Whether or not to use small buttons + */ + @Input() public small: boolean; + + /** + * Returns the path to the delete page of this workflow item + */ + getDeletePath(): string { + + return getWorkflowItemDeletePath(this.wfi.id) + } + + /** + * Returns the path to the send back page of this workflow item + */ + getSendBackPath(): string { + return getWorkflowItemSendBackPath(this.wfi.id); + } +} diff --git a/src/app/+admin/admin.module.ts b/src/app/+admin/admin.module.ts index fa2480a6ad..25b8bd4648 100644 --- a/src/app/+admin/admin.module.ts +++ b/src/app/+admin/admin.module.ts @@ -12,6 +12,10 @@ import { ItemAdminSearchResultGridElementComponent } from './admin-search-page/a import { CommunityAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component'; import { CollectionAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component'; import { ItemAdminSearchResultActionsComponent } from './admin-search-page/admin-search-results/item-admin-search-result-actions.component'; +import { WorkflowItemSearchResultAdminWorkflowGridElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component'; +import { WorkflowItemAdminWorkflowActionsComponent } from './admin-workflow-page/admin-workflow-search-results/workflow-item-admin-workflow-actions.component'; +import { WorkflowItemSearchResultAdminWorkflowListElementComponent } from './admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component'; +import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; @NgModule({ imports: [ @@ -23,13 +27,19 @@ import { ItemAdminSearchResultActionsComponent } from './admin-search-page/admin ], declarations: [ AdminSearchPageComponent, + AdminWorkflowPageComponent, ItemAdminSearchResultListElementComponent, CommunityAdminSearchResultListElementComponent, CollectionAdminSearchResultListElementComponent, ItemAdminSearchResultGridElementComponent, CommunityAdminSearchResultGridElementComponent, CollectionAdminSearchResultGridElementComponent, - ItemAdminSearchResultActionsComponent + ItemAdminSearchResultActionsComponent, + + WorkflowItemSearchResultAdminWorkflowListElementComponent, + WorkflowItemSearchResultAdminWorkflowGridElementComponent, + WorkflowItemAdminWorkflowActionsComponent + ], entryComponents: [ ItemAdminSearchResultListElementComponent, @@ -38,7 +48,11 @@ import { ItemAdminSearchResultActionsComponent } from './admin-search-page/admin ItemAdminSearchResultGridElementComponent, CommunityAdminSearchResultGridElementComponent, CollectionAdminSearchResultGridElementComponent, - ItemAdminSearchResultActionsComponent + ItemAdminSearchResultActionsComponent, + + WorkflowItemSearchResultAdminWorkflowListElementComponent, + WorkflowItemSearchResultAdminWorkflowGridElementComponent, + WorkflowItemAdminWorkflowActionsComponent ] }) export class AdminModule { diff --git a/src/app/+item-page/edit-item-page/edit-item-page.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.module.ts index d02aafcfa1..ac6125fb1c 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.module.ts @@ -5,7 +5,6 @@ import { EditItemPageRoutingModule } from './edit-item-page.routing.module'; import { EditItemPageComponent } from './edit-item-page.component'; import { ItemStatusComponent } from './item-status/item-status.component'; import { ItemOperationComponent } from './item-operation/item-operation.component'; -import { ModifyItemOverviewComponent } from './modify-item-overview/modify-item-overview.component'; import { ItemWithdrawComponent } from './item-withdraw/item-withdraw.component'; import { ItemReinstateComponent } from './item-reinstate/item-reinstate.component'; import { AbstractSimpleItemActionComponent } from './simple-item-action/abstract-simple-item-action.component'; @@ -47,7 +46,6 @@ import { ItemVersionHistoryComponent } from './item-version-history/item-version ItemOperationComponent, AbstractSimpleItemActionComponent, AbstractItemUpdateComponent, - ModifyItemOverviewComponent, ItemWithdrawComponent, ItemReinstateComponent, ItemPrivateComponent, diff --git a/src/app/+login-page/login-page-routing.module.ts b/src/app/+login-page/login-page-routing.module.ts index cd023da55c..9fa4a9e5ad 100644 --- a/src/app/+login-page/login-page-routing.module.ts +++ b/src/app/+login-page/login-page-routing.module.ts @@ -3,12 +3,17 @@ import { RouterModule } from '@angular/router'; import { LoginPageComponent } from './login-page.component'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; @NgModule({ imports: [ RouterModule.forChild([ { path: '', pathMatch: 'full', component: LoginPageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { breadcrumbKey: 'login', title: 'login.title' } } ]) + ], + providers: [ + I18nBreadcrumbResolver, + I18nBreadcrumbsService ] }) export class LoginPageRoutingModule { diff --git a/src/app/+search-page/search-page-routing.module.ts b/src/app/+search-page/search-page-routing.module.ts index 6e36883394..f71c7b45ee 100644 --- a/src/app/+search-page/search-page-routing.module.ts +++ b/src/app/+search-page/search-page-routing.module.ts @@ -6,9 +6,11 @@ import { ConfigurationSearchPageComponent } from './configuration-search-page.co import { SearchPageComponent } from './search-page.component'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; +import { SearchPageModule } from './search-page.module'; @NgModule({ imports: [ + SearchPageModule, RouterModule.forChild([{ path: '', resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { title: 'search.title', breadcrumbKey: 'search' }, diff --git a/src/app/+search-page/search-page.module.ts b/src/app/+search-page/search-page.module.ts index b69dcaf935..00c990c665 100644 --- a/src/app/+search-page/search-page.module.ts +++ b/src/app/+search-page/search-page.module.ts @@ -2,10 +2,8 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { CoreModule } from '../core/core.module'; import { SharedModule } from '../shared/shared.module'; -import { SearchPageRoutingModule } from './search-page-routing.module'; import { SearchComponent } from './search.component'; import { SidebarService } from '../shared/sidebar/sidebar.service'; -import { EffectsModule } from '@ngrx/effects'; import { ConfigurationSearchPageComponent } from './configuration-search-page.component'; import { ConfigurationSearchPageGuard } from './configuration-search-page.guard'; import { SearchTrackerComponent } from './search-tracker.component'; @@ -14,7 +12,6 @@ import { SearchPageComponent } from './search-page.component'; import { SidebarFilterService } from '../shared/sidebar/filter/sidebar-filter.service'; import { SearchFilterService } from '../core/shared/search/search-filter.service'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; -import { TranslateModule } from '@ngx-translate/core'; const components = [ SearchPageComponent, @@ -25,7 +22,6 @@ const components = [ @NgModule({ imports: [ - SearchPageRoutingModule, CommonModule, SharedModule, CoreModule.forRoot(), diff --git a/src/app/+workflowitems-edit-page/workflow-item-action-page.component.html b/src/app/+workflowitems-edit-page/workflow-item-action-page.component.html new file mode 100644 index 0000000000..76808c7e14 --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-action-page.component.html @@ -0,0 +1,6 @@ + + {{'workflow-item.' + type + '.header' | translate}} + + {{'workflow-item.' + type + '.button.cancel' | translate}} + {{'workflow-item.' + type + '.button.confirm' | translate}} + diff --git a/src/app/+workflowitems-edit-page/workflow-item-action-page.component.spec.ts b/src/app/+workflowitems-edit-page/workflow-item-action-page.component.spec.ts new file mode 100644 index 0000000000..979476bf03 --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-action-page.component.spec.ts @@ -0,0 +1,124 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; +import { WorkflowItemActionPageComponent } from './workflow-item-action-page.component'; +import { NotificationsService } from '../shared/notifications/notifications.service'; +import { RouteService } from '../core/services/route.service'; +import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; +import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { WorkflowItem } from '../core/submission/models/workflowitem.model'; +import { Observable, of as observableOf } from 'rxjs'; +import { VarDirective } from '../shared/utils/var.directive'; +import { By } from '@angular/platform-browser'; +import { 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'; + +const type = 'testType'; +describe('WorkflowItemActionPageComponent', () => { + let component: WorkflowItemActionPageComponent; + let fixture: ComponentFixture; + let wfiService; + let wfi; + let itemRD$; + let id; + + function init() { + wfiService = jasmine.createSpyObj('workflowItemService', { + sendBack: observableOf(true) + }); + itemRD$ = createSuccessfulRemoteDataObject$(itemRD$); + wfi = new WorkflowItem(); + wfi.item = itemRD$; + id = 'de11b5e5-064a-4e98-a7ac-a1a6a65ddf80'; + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + })], + declarations: [TestComponent, VarDirective], + providers: [ + { provide: ActivatedRoute, useValue: new ActivatedRouteStub({}, { wfi: createSuccessfulRemoteDataObject(wfi) }) }, + { provide: Router, useClass: RouterStub }, + { provide: RouteService, useValue: {} }, + { provide: NotificationsService, useClass: NotificationsServiceStub }, + { provide: WorkflowItemDataService, useValue: wfiService }, + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set the initial type correctly', () => { + expect(component.type).toEqual(type); + }); + + describe('clicking the button with class btn-danger', () => { + beforeEach(() => { + spyOn(component, 'performAction'); + }); + + it('should call performAction on clicking the btn-danger', () => { + const button = fixture.debugElement.query(By.css('.btn-danger')).nativeElement; + button.click(); + fixture.detectChanges(); + expect(component.performAction).toHaveBeenCalled(); + }); + }); + + describe('clicking the button with class btn-default', () => { + beforeEach(() => { + spyOn(component, 'previousPage'); + }); + + it('should call performAction on clicking the btn-default', () => { + const button = fixture.debugElement.query(By.css('.btn-default')).nativeElement; + button.click(); + fixture.detectChanges(); + expect(component.previousPage).toHaveBeenCalled(); + }); + }); +}); + +@Component({ + selector: 'ds-workflow-item-test-action-page', + templateUrl: 'workflow-item-action-page.component.html' + } +) +class TestComponent extends WorkflowItemActionPageComponent { + constructor(protected route: ActivatedRoute, + protected workflowItemService: WorkflowItemDataService, + protected router: Router, + protected routeService: RouteService, + protected notificationsService: NotificationsService, + protected translationService: TranslateService) { + super(route, workflowItemService, router, routeService, notificationsService, translationService); + } + + getType(): string { + return type; + } + + sendRequest(id: string): Observable { + return observableOf(true); + } +} diff --git a/src/app/+workflowitems-edit-page/workflow-item-action-page.component.ts b/src/app/+workflowitems-edit-page/workflow-item-action-page.component.ts new file mode 100644 index 0000000000..2859ca3e44 --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-action-page.component.ts @@ -0,0 +1,86 @@ +import { OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map, switchMap, take } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; +import { WorkflowItem } from '../core/submission/models/workflowitem.model'; +import { Item } from '../core/shared/item.model'; +import { ActivatedRoute, Data, Router } from '@angular/router'; +import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; +import { RouteService } from '../core/services/route.service'; +import { NotificationsService } from '../shared/notifications/notifications.service'; +import { RemoteData } from '../core/data/remote-data'; +import { getAllSucceededRemoteData, getRemoteDataPayload } from '../core/shared/operators'; +import { isEmpty } from '../shared/empty.util'; + +/** + * Abstract component representing a page to perform an action on a workflow item + */ +export abstract class WorkflowItemActionPageComponent implements OnInit { + public type; + public wfi$: Observable; + public item$: Observable; + + constructor(protected route: ActivatedRoute, + protected workflowItemService: WorkflowItemDataService, + protected router: Router, + protected routeService: RouteService, + protected notificationsService: NotificationsService, + protected translationService: TranslateService) { + } + + /** + * Sets up the type, workflow item and its item object + */ + ngOnInit() { + this.type = this.getType(); + this.wfi$ = this.route.data.pipe(map((data: Data) => data.wfi as RemoteData), getRemoteDataPayload()); + this.item$ = this.wfi$.pipe(switchMap((wfi: WorkflowItem) => (wfi.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()))); + } + + /** + * Performs the action and shows a notification based on the outcome of the action + */ + performAction() { + this.wfi$.pipe( + take(1), + switchMap((wfi: WorkflowItem) => this.sendRequest(wfi.id)) + ).subscribe((successful: boolean) => { + if (successful) { + const title = this.translationService.get('workflow-item.' + this.type + '.notification.success.title'); + const content = this.translationService.get('workflow-item.' + this.type + '.notification.success.content'); + this.notificationsService.success(title, content) + } else { + const title = this.translationService.get('workflow-item.' + this.type + '.notification.error.title'); + const content = this.translationService.get('workflow-item.' + this.type + '.notification.error.content'); + this.notificationsService.error(title, content) + } + this.previousPage(); + }) + } + + /** + * Navigates to the previous url + * If there's not previous url, it continues to the mydspace page instead + */ + previousPage() { + this.routeService.getPreviousUrl().pipe(take(1)) + .subscribe((url) => { + if (isEmpty(url)) { + url = '/mydspace'; + } + this.router.navigateByUrl(url); + } + ); + } + + /** + * Performs the action of this workflow item action page + * @param id The id of the WorkflowItem + */ + abstract sendRequest(id: string): Observable; + + /** + * Returns the type of page + */ + abstract getType(): string; +} diff --git a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts new file mode 100644 index 0000000000..a70005776b --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts @@ -0,0 +1,76 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { WorkflowItemDeleteComponent } from './workflow-item-delete.component'; +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 { WorkflowItemDataService } from '../../core/submission/workflowitem-data.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 { 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'; + +describe('WorkflowItemDeleteComponent', () => { + let component: WorkflowItemDeleteComponent; + let fixture: ComponentFixture; + let wfiService; + let wfi; + let itemRD$; + let id; + + function init() { + wfiService = jasmine.createSpyObj('workflowItemService', { + delete: observableOf(true) + }); + itemRD$ = createSuccessfulRemoteDataObject$(itemRD$); + wfi = new WorkflowItem(); + wfi.item = itemRD$; + id = 'de11b5e5-064a-4e98-a7ac-a1a6a65ddf80'; + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + })], + declarations: [WorkflowItemDeleteComponent, VarDirective], + providers: [ + { provide: ActivatedRoute, useValue: new ActivatedRouteStub({}, { wfi: createSuccessfulRemoteDataObject(wfi) }) }, + { provide: Router, useClass: RouterStub }, + { provide: RouteService, useValue: {} }, + { provide: NotificationsService, useClass: NotificationsServiceStub }, + { provide: WorkflowItemDataService, useValue: wfiService }, + { provide: RequestService, useValue: getMockRequestService() }, + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(WorkflowItemDeleteComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should call delete on the workflow-item service when sendRequest is called', () => { + component.sendRequest(id); + expect(wfiService.delete).toHaveBeenCalledWith(id); + }); +}); diff --git a/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts new file mode 100644 index 0000000000..43c3e90152 --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts @@ -0,0 +1,44 @@ +import { Component } from '@angular/core'; +import { Observable } from 'rxjs'; +import { WorkflowItemActionPageComponent } from '../workflow-item-action-page.component'; +import { ActivatedRoute, Router } from '@angular/router'; +import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; +import { RouteService } from '../../core/services/route.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { RequestService } from '../../core/data/request.service'; + +@Component({ + selector: 'ds-workflow-item-delete', + templateUrl: '../workflow-item-action-page.component.html' +}) +/** + * Component representing a page to delete a workflow item + */ +export class WorkflowItemDeleteComponent extends WorkflowItemActionPageComponent { + constructor(protected route: ActivatedRoute, + protected workflowItemService: WorkflowItemDataService, + protected router: Router, + protected routeService: RouteService, + protected notificationsService: NotificationsService, + protected translationService: TranslateService, + protected requestService: RequestService) { + super(route, workflowItemService, router, routeService, notificationsService, translationService); + } + + /** + * Returns the type of page + */ + getType(): string { + return 'delete'; + } + + /** + * Performs the action of this workflow item action page + * @param id The id of the WorkflowItem + */ + sendRequest(id: string): Observable { + this.requestService.removeByHrefSubstring('/discover'); + return this.workflowItemService.delete(id); + } +} diff --git a/src/app/+workflowitems-edit-page/workflow-item-page.resolver.spec.ts b/src/app/+workflowitems-edit-page/workflow-item-page.resolver.spec.ts new file mode 100644 index 0000000000..792c642ec7 --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-page.resolver.spec.ts @@ -0,0 +1,29 @@ +import { first } from 'rxjs/operators'; +import { of as observableOf } from 'rxjs'; +import { WorkflowItemPageResolver } from './workflow-item-page.resolver'; +import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; + +describe('WorkflowItemPageResolver', () => { + describe('resolve', () => { + let resolver: WorkflowItemPageResolver; + let wfiService: WorkflowItemDataService; + const uuid = '1234-65487-12354-1235'; + + beforeEach(() => { + wfiService = { + findById: (id: string) => observableOf({ payload: { id }, hasSucceeded: true }) as any + } as any; + resolver = new WorkflowItemPageResolver(wfiService); + }); + + it('should resolve a workflow item with the correct id', () => { + resolver.resolve({ params: { id: uuid } } as any, undefined) + .pipe(first()) + .subscribe( + (resolved) => { + expect(resolved.payload.id).toEqual(uuid); + } + ); + }); + }); +}); diff --git a/src/app/+workflowitems-edit-page/workflow-item-page.resolver.ts b/src/app/+workflowitems-edit-page/workflow-item-page.resolver.ts new file mode 100644 index 0000000000..19cc4b4914 --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-page.resolver.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; +import { Observable } from 'rxjs'; +import { RemoteData } from '../core/data/remote-data'; +import { hasValue } from '../shared/empty.util'; +import { find } from 'rxjs/operators'; +import { followLink } from '../shared/utils/follow-link-config.model'; +import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; +import { WorkflowItem } from '../core/submission/models/workflowitem.model'; + +/** + * This class represents a resolver that requests a specific workflow item before the route is activated + */ +@Injectable() +export class WorkflowItemPageResolver implements Resolve> { + constructor(private workflowItemService: WorkflowItemDataService) { + } + + /** + * Method for resolving a workflow item based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns Observable<> Emits the found workflow item based on the parameters in the current route, + * or an error if something went wrong + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { + return this.workflowItemService.findById(route.params.id, + followLink('item'), + ).pipe( + find((RD) => hasValue(RD.error) || RD.hasSucceeded), + ); + } +} diff --git a/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts b/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts new file mode 100644 index 0000000000..fde48b59e4 --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts @@ -0,0 +1,76 @@ +import { async, 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 { WorkflowItemDataService } from '../../core/submission/workflowitem-data.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 { WorkflowItemSendBackComponent } from './workflow-item-send-back.component'; +import { RequestService } from '../../core/data/request.service'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; +import { RouterStub } from '../../shared/testing/router.stub'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; + +describe('WorkflowItemSendBackComponent', () => { + let component: WorkflowItemSendBackComponent; + let fixture: ComponentFixture; + let wfiService; + let wfi; + let itemRD$; + let id; + + function init() { + wfiService = jasmine.createSpyObj('workflowItemService', { + sendBack: observableOf(true) + }); + itemRD$ = createSuccessfulRemoteDataObject$(itemRD$); + wfi = new WorkflowItem(); + wfi.item = itemRD$; + id = 'de11b5e5-064a-4e98-a7ac-a1a6a65ddf80'; + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + })], + declarations: [WorkflowItemSendBackComponent, VarDirective], + providers: [ + { provide: ActivatedRoute, useValue: new ActivatedRouteStub({}, { wfi: createSuccessfulRemoteDataObject(wfi) }) }, + { provide: Router, useClass: RouterStub }, + { provide: RouteService, useValue: {} }, + { provide: NotificationsService, useClass: NotificationsServiceStub }, + { provide: WorkflowItemDataService, useValue: wfiService }, + { provide: RequestService, useValue: getMockRequestService() }, + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(WorkflowItemSendBackComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should call sendBack on the workflow-item service when sendRequest is called', () => { + component.sendRequest(id); + expect(wfiService.sendBack).toHaveBeenCalledWith(id); + }); +}); diff --git a/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts b/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts new file mode 100644 index 0000000000..002e5dcc9a --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts @@ -0,0 +1,44 @@ +import { Component } from '@angular/core'; +import { WorkflowItemActionPageComponent } from '../workflow-item-action-page.component'; +import { Observable } from 'rxjs'; +import { ActivatedRoute, Router } from '@angular/router'; +import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; +import { RouteService } from '../../core/services/route.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { RequestService } from '../../core/data/request.service'; + +@Component({ + selector: 'ds-workflow-item-send-back', + templateUrl: '../workflow-item-action-page.component.html' +}) +/** + * Component representing a page to send back a workflow item to the submitter + */ +export class WorkflowItemSendBackComponent extends WorkflowItemActionPageComponent { + constructor(protected route: ActivatedRoute, + protected workflowItemService: WorkflowItemDataService, + protected router: Router, + protected routeService: RouteService, + protected notificationsService: NotificationsService, + protected translationService: TranslateService, + protected requestService: RequestService) { + super(route, workflowItemService, router, routeService, notificationsService, translationService); + } + + /** + * Returns the type of page + */ + getType(): string { + return 'send-back'; + } + + /** + * Performs the action of this workflow item action page + * @param id The id of the WorkflowItem + */ + sendRequest(id: string): Observable { + this.requestService.removeByHrefSubstring('/discover'); + return this.workflowItemService.sendBack(id); + } +} diff --git a/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts b/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts index d5df70698c..e9989bf947 100644 --- a/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts +++ b/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts @@ -3,21 +3,65 @@ import { RouterModule } from '@angular/router'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; import { SubmissionEditComponent } from '../submission/edit/submission-edit.component'; +import { URLCombiner } from '../core/url-combiner/url-combiner'; +import { getWorkflowItemModulePath } from '../app-routing.module'; +import { WorkflowItemDeleteComponent } from './workflow-item-delete/workflow-item-delete.component'; +import { WorkflowItemPageResolver } from './workflow-item-page.resolver'; +import { WorkflowItemSendBackComponent } from './workflow-item-send-back/workflow-item-send-back.component'; + +export function getWorkflowItemPageRoute(wfiId: string) { + return new URLCombiner(getWorkflowItemModulePath(), wfiId).toString(); +} + +export function getWorkflowItemEditPath(wfiId: string) { + return new URLCombiner(getWorkflowItemModulePath(), wfiId, WORKFLOW_ITEM_EDIT_PATH).toString() +} + +export function getWorkflowItemDeletePath(wfiId: string) { + return new URLCombiner(getWorkflowItemModulePath(), wfiId, WORKFLOW_ITEM_DELETE_PATH).toString() +} + +export function getWorkflowItemSendBackPath(wfiId: string) { + return new URLCombiner(getWorkflowItemModulePath(), wfiId, WORKFLOW_ITEM_SEND_BACK_PATH).toString() +} + +const WORKFLOW_ITEM_EDIT_PATH = 'edit'; +const WORKFLOW_ITEM_DELETE_PATH = 'delete'; +const WORKFLOW_ITEM_SEND_BACK_PATH = 'sendback'; @NgModule({ imports: [ RouterModule.forChild([ - { path: '', redirectTo: '/home', pathMatch: 'full' }, { - canActivate: [AuthenticatedGuard], - path: ':id/edit', - component: SubmissionEditComponent, - data: { title: 'submission.edit.title' } - } - ]) - ] + path: ':id', + resolve: { wfi: WorkflowItemPageResolver }, + children: [ + { + canActivate: [AuthenticatedGuard], + path: WORKFLOW_ITEM_EDIT_PATH, + component: SubmissionEditComponent, + data: { title: 'submission.edit.title' } + }, + { + canActivate: [AuthenticatedGuard], + path: WORKFLOW_ITEM_DELETE_PATH, + component: WorkflowItemDeleteComponent, + data: { title: 'workflow-item.delete.title' } + }, + { + canActivate: [AuthenticatedGuard], + path: WORKFLOW_ITEM_SEND_BACK_PATH, + component: WorkflowItemSendBackComponent, + data: { title: 'workflow-item.send-back.title' } + } + ] + }] + ) + ], + providers: [WorkflowItemPageResolver] }) /** * This module defines the default component to load when navigating to the workflowitems edit page path. */ -export class WorkflowItemsEditPageRoutingModule { } +export class WorkflowItemsEditPageRoutingModule { +} diff --git a/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts b/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts index 7a89f18c7d..ef1e49abf5 100644 --- a/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts +++ b/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts @@ -3,6 +3,8 @@ import { NgModule } from '@angular/core'; import { SharedModule } from '../shared/shared.module'; import { WorkflowItemsEditPageRoutingModule } from './workflowitems-edit-page-routing.module'; import { SubmissionModule } from '../submission/submission.module'; +import { WorkflowItemDeleteComponent } from './workflow-item-delete/workflow-item-delete.component'; +import { WorkflowItemSendBackComponent } from './workflow-item-send-back/workflow-item-send-back.component'; @NgModule({ imports: [ @@ -11,7 +13,7 @@ import { SubmissionModule } from '../submission/submission.module'; SharedModule, SubmissionModule, ], - declarations: [] + declarations: [WorkflowItemDeleteComponent, WorkflowItemSendBackComponent] }) /** * This module handles all modules that need to access the workflowitems edit page. diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 168c828c79..f4b02d8774 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -45,6 +45,12 @@ export function getProfileModulePath() { return `/${PROFILE_MODULE_PATH}`; } +const WORKFLOW_ITEM_MODULE_PATH = 'workflowitems'; + +export function getWorkflowItemModulePath() { + return `/${WORKFLOW_ITEM_MODULE_PATH}`; +} + export function getDSOPath(dso: DSpaceObject): string { switch ((dso as any).type) { case Community.type.value: @@ -74,7 +80,7 @@ export function getDSOPath(dso: DSpaceObject): string { loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule', canActivate: [AuthenticatedGuard] }, - { path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' }, + { path: 'search', loadChildren: './+search-page/search-page-routing.module#SearchPageRoutingModule' }, { path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule'}, { path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] }, { path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' }, @@ -85,7 +91,7 @@ export function getDSOPath(dso: DSpaceObject): string { loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule' }, { - path: 'workflowitems', + path: WORKFLOW_ITEM_MODULE_PATH, loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule' }, { diff --git a/src/app/core/shared/context.model.ts b/src/app/core/shared/context.model.ts index 6bb3d77140..ff24b7d090 100644 --- a/src/app/core/shared/context.model.ts +++ b/src/app/core/shared/context.model.ts @@ -11,4 +11,5 @@ export enum Context { AdminMenu = 'adminMenu', SubmissionModal = 'submissionModal', AdminSearch = 'adminSearch', + AdminWorkflowSearch = 'adminWorkflowSearch', } diff --git a/src/app/core/submission/workflowitem-data.service.ts b/src/app/core/submission/workflowitem-data.service.ts index a2dfca5eb3..c82f7bf0b5 100644 --- a/src/app/core/submission/workflowitem-data.service.ts +++ b/src/app/core/submission/workflowitem-data.service.ts @@ -9,13 +9,17 @@ import { DataService } from '../data/data.service'; import { RequestService } from '../data/request.service'; import { WorkflowItem } from './models/workflowitem.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { FindListOptions } from '../data/request.models'; +import { DeleteByIDRequest, FindListOptions } from '../data/request.models'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; +import { Observable } from 'rxjs'; +import { find, map } from 'rxjs/operators'; +import { hasValue } from '../../shared/empty.util'; +import { RequestEntry } from '../data/request.reducer'; /** - * A service that provides methods to make REST requests with workflowitems endpoint. + * A service that provides methods to make REST requests with workflow items endpoint. */ @Injectable() @dataService(WorkflowItem.type) @@ -35,4 +39,50 @@ export class WorkflowItemDataService extends DataService { super(); } + /** + * Delete an existing Workflow Item on the server + * @param id The Workflow Item's id to be removed + * @return an observable that emits true when the deletion was successful, false when it failed + */ + delete(id: string): Observable { + return this.deleteWFI(id, true) + } + + /** + * Send an existing Workflow Item back to the workflow on the server + * @param id The Workspace Item's id to be sent back + * @return an observable that emits true when sending back the item was successful, false when it failed + */ + sendBack(id: string): Observable { + return this.deleteWFI(id, false) + } + + /** + * Method to delete a workflow item from the server + * @param id The identifier of the server + * @param expunge Whether or not to expunge: + * When true, the workflow item and its item will be permanently expunged on the server + * When false, the workflow item will be removed, but the item will still be available as a workspace item + */ + private deleteWFI(id: string, expunge: boolean): Observable { + const requestId = this.requestService.generateRequestId(); + + const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( + map((endpoint: string) => this.getIDHref(endpoint, id)), + map((endpoint: string) => endpoint + '?expunge=' + expunge) + ); + + hrefObs.pipe( + find((href: string) => hasValue(href)), + map((href: string) => { + const request = new DeleteByIDRequest(requestId, href, id); + this.requestService.configure(request); + }) + ).subscribe(); + + return this.requestService.getByUUID(requestId).pipe( + find((request: RequestEntry) => request.completed), + map((request: RequestEntry) => request.response.isSuccessful) + ); + } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts index 472fb3af61..c77aabfeed 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts @@ -6,11 +6,7 @@ import { async, ComponentFixture, fakeAsync, inject, TestBed, tick, } from '@ang import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model'; -import { - DynamicFormLayoutService, - DynamicFormsCoreModule, - DynamicFormValidationService -} from '@ng-dynamic-forms/core'; +import { DynamicFormLayoutService, DynamicFormsCoreModule, DynamicFormValidationService } from '@ng-dynamic-forms/core'; import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap'; import { AuthorityService } from '../../../../../../core/integration/authority.service'; import { AuthorityServiceStub } from '../../../../../testing/authority-service.stub'; @@ -25,7 +21,6 @@ import { createTestComponent } from '../../../../../testing/utils.test'; import { DynamicLookupNameModel } from './dynamic-lookup-name.model'; import { AuthorityConfidenceStateDirective } from '../../../../../authority-confidence/authority-confidence-state.directive'; import { ObjNgFor } from '../../../../../utils/object-ngfor.pipe'; -import { GlobalConfig } from '../../../../../../../config/global-config.interface'; let LOOKUP_TEST_MODEL_CONFIG = { authorityOptions: { diff --git a/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html index 81ee3ebcce..a9b5d0bdaa 100644 --- a/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html +++ b/src/app/shared/object-grid/item-grid-element/item-types/publication/publication-grid-element.component.html @@ -1 +1,4 @@ - + + + + diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index fcae80a3f2..1d089cf647 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -189,6 +189,7 @@ import { CustomSwitchComponent } from './form/builder/ds-dynamic-form-ui/models/ import { BundleListElementComponent } from './object-list/bundle-list-element/bundle-list-element.component'; import { MissingTranslationHelper } from './translate/missing-translation.helper'; import { ItemVersionsNoticeComponent } from './item/item-versions/notice/item-versions-notice.component'; +import { ModifyItemOverviewComponent } from '../+item-page/edit-item-page/modify-item-overview/modify-item-overview.component'; import { ClaimedTaskActionsLoaderComponent } from './mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component'; import { ClaimedTaskActionsDirective } from './mydspace-actions/claimed-task/switcher/claimed-task-actions.directive'; import { ClaimedTaskActionsEditMetadataComponent } from './mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component'; @@ -371,6 +372,7 @@ const COMPONENTS = [ ItemVersionsComponent, PublicationSearchResultListElementComponent, ItemVersionsNoticeComponent, + ModifyItemOverviewComponent, ImpersonateNavbarComponent ]; diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 36a7c4e263..03dc00bd81 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -407,6 +407,20 @@ + + "admin.workflow.breadcrumbs": "Administer Workflow", + + "admin.workflow.title": "Administer Workflow", + + "admin.workflow.item.workflow": "Workflow", + + "admin.workflow.item.delete": "Delete", + + "admin.workflow.item.send-back": "Send back", + + + + "auth.errors.invalid-user": "Invalid email address or password.", "auth.messages.expired": "Your session has expired. Please log in again.", @@ -1713,6 +1727,8 @@ "menu.section.toggle.statistics_task": "Toggle Statistics Task section", + "menu.section.workflow": "Administer Workflow", + "mydspace.description": "", @@ -2604,5 +2620,44 @@ "virtual-metadata.delete-item.modal-head": "The virtual metadata of this relation", "virtual-metadata.delete-relationship.modal-head": "Select the items for which you want to save the virtual metadata as real metadata", + + + + "workflowAdmin.search.results.head": "Administer Workflow", + + + + "workflow-item.delete.notification.success.title": "Deleted", + + "workflow-item.delete.notification.success.content": "This workflow item was successfully deleted", + + "workflow-item.delete.notification.error.title": "Something went wrong", + + "workflow-item.delete.notification.error.content": "The workflow item could not be deleted", + + "workflow-item.delete.title": "Delete workflow item", + + "workflow-item.delete.header": "Delete workflow item", + + "workflow-item.delete.button.cancel": "Cancel", + + "workflow-item.delete.button.confirm": "Delete", + + + "workflow-item.send-back.notification.success.title": "Sent back to submitter", + + "workflow-item.send-back.notification.success.content": "This workflow item was successfully sent back to the submitter", + + "workflow-item.send-back.notification.error.title": "Something went wrong", + + "workflow-item.send-back.notification.error.content": "The workflow item could not be sent back to the submitter", + + "workflow-item.send-back.title": "Send workflow item back to submitter", + + "workflow-item.send-back.header": "Send workflow item back to submitter", + + "workflow-item.send-back.button.cancel": "Cancel", + + "workflow-item.send-back.button.confirm": "Send back", }