diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 967cc55d9bc..0c02926c3f7 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -265,6 +265,11 @@ import { STATIC_PAGE_PATH } from './static-page/static-page-routing-paths'; path: STATIC_PAGE_PATH, loadChildren: () => import('./static-page/static-page.module').then((m) => m.StaticPageModule), }, + { + path: 'share-submission', + loadChildren: () => import('./share-submission/share-submission.module').then((m) => m.ShareSubmissionModule), + canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard] + }, { path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent } ] } diff --git a/src/app/change-submitter-page/change-submitter-page.component.html b/src/app/change-submitter-page/change-submitter-page.component.html new file mode 100644 index 00000000000..1c9a9b3d7af --- /dev/null +++ b/src/app/change-submitter-page/change-submitter-page.component.html @@ -0,0 +1,16 @@ +
+

{{'share.submission.page.title' | translate}}

+ {{'change.submitter.page.message' | translate}} + {{getSubmitterName(sub)}} + ({{sub?.email}}) +
+ +
+
+ + diff --git a/src/app/change-submitter-page/change-submitter-page.component.scss b/src/app/change-submitter-page/change-submitter-page.component.scss new file mode 100644 index 00000000000..46dea8e2a2a --- /dev/null +++ b/src/app/change-submitter-page/change-submitter-page.component.scss @@ -0,0 +1,3 @@ +/** +The file for styling the ChangeSubmitterPageComponent. + */ diff --git a/src/app/change-submitter-page/change-submitter-page.component.spec.ts b/src/app/change-submitter-page/change-submitter-page.component.spec.ts new file mode 100644 index 00000000000..1e1e18351b2 --- /dev/null +++ b/src/app/change-submitter-page/change-submitter-page.component.spec.ts @@ -0,0 +1,72 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ChangeSubmitterPageComponent } from './change-submitter-page.component'; +import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { HALEndpointService } from '../core/shared/hal-endpoint.service'; +import { RemoteDataBuildService } from '../core/cache/builders/remote-data-build.service'; +import { RequestService } from '../core/data/request.service'; +import { NotificationsService } from '../shared/notifications/notifications.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { NotificationsServiceStub } from '../shared/testing/notifications-service.stub'; +import { RouterStub } from '../shared/testing/router.stub'; +import { of as observableOf } from 'rxjs'; +import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { getMockRequestService } from '../shared/mocks/request.service.mock'; +import { createPaginatedList } from '../shared/testing/utils.test'; +import { HALEndpointServiceStub } from '../shared/testing/hal-endpoint-service.stub'; +import { getMockRemoteDataBuildService } from '../shared/mocks/remote-data-build.service.mock'; +import { DSONameService } from '../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../shared/mocks/dso-name.service.mock'; + +describe('ChangeSubmitterPageComponent', () => { + let component: ChangeSubmitterPageComponent; + let fixture: ComponentFixture; + + let activatedRoute; + let requestService: RequestService; + let mockDataService: WorkspaceitemDataService; + let halService: HALEndpointService; + let rdbService: RemoteDataBuildService; + + beforeEach(async () => { + activatedRoute = { + snapshot: { + queryParams: new Map([ + ['shareToken', 'fake-share-token'], + ]) + } + }; + requestService = getMockRequestService(); + mockDataService = jasmine.createSpyObj('WorkspaceitemDataService', { + searchBy: observableOf(createSuccessfulRemoteDataObject$(createPaginatedList([]))), + }); + halService = Object.assign(new HALEndpointServiceStub('some-url')); + rdbService = getMockRemoteDataBuildService(); + + await TestBed.configureTestingModule({ + declarations: [ ChangeSubmitterPageComponent ], + imports: [ + TranslateModule.forRoot() + ], + providers: [ + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: Router, useValue: new RouterStub() }, + { provide: RequestService, useValue: requestService }, + { provide: WorkspaceitemDataService, useValue: mockDataService }, + { provide: HALEndpointService, useValue: halService }, + { provide: RemoteDataBuildService, useValue: rdbService }, + { provide: DSONameService, useValue: DSONameServiceMock }, + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ChangeSubmitterPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/change-submitter-page/change-submitter-page.component.ts b/src/app/change-submitter-page/change-submitter-page.component.ts new file mode 100644 index 00000000000..cf84c9f86ba --- /dev/null +++ b/src/app/change-submitter-page/change-submitter-page.component.ts @@ -0,0 +1,152 @@ +import { Component, OnInit } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { WorkspaceItem } from '../core/submission/models/workspaceitem.model'; +import { RequestParam } from '../core/cache/models/request-param.model'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload, + getFirstSucceededRemoteListPayload +} from '../core/shared/operators'; +import { map } from 'rxjs/operators'; +import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; +import { ActivatedRoute } from '@angular/router'; +import { followLink } from '../shared/utils/follow-link-config.model'; +import { EPerson } from '../core/eperson/models/eperson.model'; +import { DSONameService } from '../core/breadcrumbs/dso-name.service'; +import { isNullOrUndef } from 'chart.js/helpers'; +import { HALEndpointService } from '../core/shared/hal-endpoint.service'; +import { RemoteDataBuildService } from '../core/cache/builders/remote-data-build.service'; +import { RequestService } from '../core/data/request.service'; +import { PostRequest } from '../core/data/request.models'; +import { RemoteData } from '../core/data/remote-data'; +import { NotificationsService } from '../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; + +@Component({ + selector: 'ds-change-submitter-page', + templateUrl: './change-submitter-page.component.html', + styleUrls: ['./change-submitter-page.component.scss'] +}) +export class ChangeSubmitterPageComponent implements OnInit { + + /** + * Share token from the url. This token is used to retrieve the WorkspaceItem. + */ + private shareToken = ''; + + /** + * BehaviorSubject that contains the submitter of the WorkspaceItem. + */ + submitter: BehaviorSubject = new BehaviorSubject(null); + + /** + * BehaviorSubject that contains the WorkspaceItem. + */ + workspaceItem: BehaviorSubject = new BehaviorSubject(null); + + /** + * Boolean that indicates if the spinner should be shown when the submitter is being changed. + */ + changeSubmitterSpinner = false; + + constructor(private workspaceItemService: WorkspaceitemDataService, + private route: ActivatedRoute, + public dsoNameService: DSONameService, + protected halService: HALEndpointService, + protected rdbService: RemoteDataBuildService, + protected requestService: RequestService, + protected notificationsService: NotificationsService, + protected translate: TranslateService) {} + + ngOnInit(): void { + // Load `share_token` param value from the url + this.shareToken = this.route.snapshot.queryParams.share_token; + this.loadWorkspaceItemAndAssignSubmitter(this.shareToken); + } + + /** + * Load the WorkspaceItem using the shareToken and assign the submitter from the retrieved WorkspaceItem. + */ + loadWorkspaceItemAndAssignSubmitter(shareToken: string) { + this.findWorkspaceItemByShareToken(shareToken)?.subscribe((workspaceItem: WorkspaceItem) => { + this.workspaceItem.next(workspaceItem); + this.loadAndAssignSubmitter(workspaceItem); + }); + } + + /** + * Find a WorkspaceItem by its shareToken. + */ + findWorkspaceItemByShareToken(shareToken: string): Observable { + return this.workspaceItemService.searchBy('shareToken', { + searchParams: [Object.assign(new RequestParam('shareToken', shareToken))] + }, false, false, followLink('submitter')).pipe(getFirstSucceededRemoteListPayload(), + map((workspaceItems: WorkspaceItem[]) => workspaceItems?.[0])); + } + + /** + * Load the submitter from the WorkspaceItem and assign it to the submitter BehaviorSubject. + */ + loadAndAssignSubmitter(workspaceItem: WorkspaceItem) { + if (isNullOrUndef(workspaceItem)) { + console.error('Cannot load submitter because WorkspaceItem is null or undefined'); + return; + } + + if (workspaceItem.submitter instanceof Observable) { + workspaceItem.submitter + .pipe(getFirstSucceededRemoteDataPayload()) + .subscribe((submitter: any) => { + this.assignSubmitter(submitter); + }); + } else { + this.assignSubmitter(workspaceItem.submitter); + } + } + + /** + * Assign a new submitter to the submitter BehaviorSubject. + */ + assignSubmitter(eperson: EPerson) { + this.submitter.next(eperson); + } + + /** + * Get the name of the submitter using the DSONameService. + * @param submitter + */ + getSubmitterName(submitter: EPerson): string { + if (isNullOrUndef(submitter)) { + return ''; + } + return this.dsoNameService.getName(submitter); + } + + /** + * Change the submitter of the WorkspaceItem using the shareToken. This will send a POST request to the backend when + * the submitter of the Item is changed. + */ + changeSubmitter() { + const requestId = this.requestService.generateRequestId(); + + const url = this.halService.getRootHref() + '/submission/setOwner?shareToken=' + this.shareToken; + const postRequest = new PostRequest(requestId, url); + // Send POST request + this.requestService.send(postRequest); + this.changeSubmitterSpinner = true; + // Get response + const response = this.rdbService.buildFromRequestUUID(requestId); + response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData) => { + if (rd.hasSucceeded) { + this.notificationsService.success( + this.translate.instant('change.submitter.page.changed-successfully')); + // Update the submitter + this.loadWorkspaceItemAndAssignSubmitter(this.shareToken); + } else { + this.notificationsService.error( + this.translate.instant('change.submitter.page.changed-error')); + } + this.changeSubmitterSpinner = false; + }); + } +} diff --git a/src/app/share-submission/share-submission-page/share-submission-page.component.html b/src/app/share-submission/share-submission-page/share-submission-page.component.html new file mode 100644 index 00000000000..cb6d4b6663e --- /dev/null +++ b/src/app/share-submission/share-submission-page/share-submission-page.component.html @@ -0,0 +1,6 @@ +
+

{{'share.submission.page.title' | translate}}

+ {{'share.submission.page.share-link.message.start' | translate}} + {{changeSubmitterLink}} + {{'share.submission.page.share-link.message.end' | translate}} +
diff --git a/src/app/share-submission/share-submission-page/share-submission-page.component.scss b/src/app/share-submission/share-submission-page/share-submission-page.component.scss new file mode 100644 index 00000000000..7fd1808a33f --- /dev/null +++ b/src/app/share-submission/share-submission-page/share-submission-page.component.scss @@ -0,0 +1,3 @@ +/** +The file for styling the ShareSubmissionPageComponent. + */ diff --git a/src/app/share-submission/share-submission-page/share-submission-page.component.spec.ts b/src/app/share-submission/share-submission-page/share-submission-page.component.spec.ts new file mode 100644 index 00000000000..04a1ca32bc5 --- /dev/null +++ b/src/app/share-submission/share-submission-page/share-submission-page.component.spec.ts @@ -0,0 +1,40 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ShareSubmissionPageComponent } from './share-submission-page.component'; +import { ActivatedRoute } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; + +describe('ShareSubmissionPageComponent', () => { + let component: ShareSubmissionPageComponent; + let fixture: ComponentFixture; + let activatedRoute; + + beforeEach(async () => { + activatedRoute = { + snapshot: { + queryParams: new Map([ + ['shareToken', 'fake-share-token'], + ]) + } + }; + + await TestBed.configureTestingModule({ + declarations: [ ShareSubmissionPageComponent ], + imports: [ + TranslateModule.forRoot() + ], + providers: [ + { provide: ActivatedRoute, useValue: activatedRoute } + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ShareSubmissionPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/share-submission/share-submission-page/share-submission-page.component.ts b/src/app/share-submission/share-submission-page/share-submission-page.component.ts new file mode 100644 index 00000000000..0c230c93796 --- /dev/null +++ b/src/app/share-submission/share-submission-page/share-submission-page.component.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + selector: 'ds-share-submission-page', + templateUrl: './share-submission-page.component.html', + styleUrls: ['./share-submission-page.component.scss'] +}) +export class ShareSubmissionPageComponent { + + /** + * Share token from the url. This token is used to retrieve the WorkspaceItem. + * With this link, the submitter can be changed. + */ + changeSubmitterLink: string; + + constructor(private route: ActivatedRoute) {} + + ngOnInit(): void { + // Load `share-token` param value from the url + this.changeSubmitterLink = this.route.snapshot.queryParams.changeSubmitterLink; + } +} diff --git a/src/app/share-submission/share-submission-page/share-submission-routing.module.ts b/src/app/share-submission/share-submission-page/share-submission-routing.module.ts new file mode 100644 index 00000000000..b590ef38d24 --- /dev/null +++ b/src/app/share-submission/share-submission-page/share-submission-routing.module.ts @@ -0,0 +1,28 @@ +import { RouterModule, Routes } from '@angular/router'; +import { NgModule } from '@angular/core'; +import { ShareSubmissionPageComponent } from './share-submission-page.component'; +import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { ChangeSubmitterPageComponent } from '../../change-submitter-page/change-submitter-page.component'; + +const routes: Routes = [ + { path: '', + component: ShareSubmissionPageComponent, + resolve: { breadcrumb: I18nBreadcrumbResolver }, + data: { + breadcrumbKey: 'share.submission', + }, + }, + { path: 'change-submitter', + component: ChangeSubmitterPageComponent, + resolve: { breadcrumb: I18nBreadcrumbResolver }, + data: { + breadcrumbKey: 'change.submitter', + }, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class ShareSubmissionPageModule { } diff --git a/src/app/share-submission/share-submission.module.ts b/src/app/share-submission/share-submission.module.ts new file mode 100644 index 00000000000..debb292dc73 --- /dev/null +++ b/src/app/share-submission/share-submission.module.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ShareSubmissionPageComponent } from './share-submission-page/share-submission-page.component'; +import { SharedModule } from '../shared/shared.module'; +import { ShareSubmissionPageModule } from './share-submission-page/share-submission-routing.module'; +import { ChangeSubmitterPageComponent } from '../change-submitter-page/change-submitter-page.component'; + +@NgModule({ + declarations: [ + ShareSubmissionPageComponent, + ChangeSubmitterPageComponent + ], + imports: [ + CommonModule, + ShareSubmissionPageModule, + SharedModule, + ] +}) +export class ShareSubmissionModule { } diff --git a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.html b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.html index 6e958c7a8bf..1a2f253ceef 100644 --- a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.html +++ b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.html @@ -25,6 +25,20 @@ {{'submission.workflow.generic.delete' | translate}} + + diff --git a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.spec.ts b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.spec.ts index c76cce3982b..a22774748bf 100644 --- a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.spec.ts +++ b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.spec.ts @@ -27,6 +27,10 @@ import { getMockSearchService } from '../../mocks/search-service.mock'; import { SearchService } from '../../../core/shared/search/search.service'; import { AuthService } from '../../../core/auth/auth.service'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; +import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; +import { HALEndpointServiceStub } from '../../testing/hal-endpoint-service.stub'; +import { getMockRemoteDataBuildService } from '../../mocks/remote-data-build.service.mock'; let component: WorkspaceitemActionsComponent; let fixture: ComponentFixture; @@ -35,6 +39,8 @@ let mockObject: WorkspaceItem; let notificationsServiceStub: NotificationsServiceStub; let authorizationService; let authService; +let halService: HALEndpointService; +let rdbService: RemoteDataBuildService; const mockDataService = jasmine.createSpyObj('WorkspaceitemDataService', { delete: jasmine.createSpy('delete') @@ -155,6 +161,9 @@ authService = jasmine.createSpyObj('authService', { getAuthenticatedUserFromStore: jasmine.createSpy('getAuthenticatedUserFromStore') }); +halService = Object.assign(new HALEndpointServiceStub('url')); +rdbService = getMockRemoteDataBuildService(); + describe('WorkspaceitemActionsComponent', () => { beforeEach(waitForAsync(async () => { authorizationService = jasmine.createSpyObj('authorizationService', { @@ -180,6 +189,8 @@ describe('WorkspaceitemActionsComponent', () => { { provide: RequestService, useValue: requestServce }, { provide: AuthService, useValue: authService }, { provide: AuthorizationDataService, useValue: authorizationService}, + { provide: HALEndpointService, useValue: halService }, + { provide: RemoteDataBuildService, useValue: rdbService }, NgbModal ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.ts b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.ts index 05afacf0daa..3be989a6aa0 100644 --- a/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.ts +++ b/src/app/shared/mydspace-actions/workspaceitem/workspaceitem-actions.component.ts @@ -19,6 +19,9 @@ import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../../../core import { RemoteData } from '../../../core/data/remote-data'; import { NoContent } from '../../../core/shared/NoContent.model'; import { getWorkspaceItemViewRoute } from '../../../workspaceitems-edit-page/workspaceitems-edit-page-routing-paths'; +import { GetRequest } from '../../../core/data/request.models'; +import { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; +import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; /** * This component represents actions related to WorkspaceItem object. @@ -35,6 +38,11 @@ export class WorkspaceitemActionsComponent extends MyDSpaceActionsComponent} @@ -69,6 +77,8 @@ export class WorkspaceitemActionsComponent extends MyDSpaceActionsComponent