Skip to content
5 changes: 5 additions & 0 deletions src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
]
}
Expand Down
16 changes: 16 additions & 0 deletions src/app/change-submitter-page/change-submitter-page.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<div class="container pt-2 pb-4" *ngVar="submitter | async as sub">
<h3>{{'share.submission.page.title' | translate}}</h3>
<span>{{'change.submitter.page.message' | translate}}</span>
<span>{{getSubmitterName(sub)}}</span>
<span> ({{sub?.email}})</span>
<div class="pt-4">
<button class="btn btn-info" (click)="changeSubmitter()">
<span *ngIf="changeSubmitterSpinner"
class="spinner-border spinner-border-sm spinner-button mr-1" role="status" aria-hidden="true">
</span>
<span>{{'change.submitter.page.button.take-it' | translate}}</span>
</button>
</div>
</div>


Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/**
The file for styling the ChangeSubmitterPageComponent.
*/
Original file line number Diff line number Diff line change
@@ -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<ChangeSubmitterPageComponent>;

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();
});
});
152 changes: 152 additions & 0 deletions src/app/change-submitter-page/change-submitter-page.component.ts
Original file line number Diff line number Diff line change
@@ -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<EPerson> = new BehaviorSubject(null);

/**
* BehaviorSubject that contains the WorkspaceItem.
*/
workspaceItem: BehaviorSubject<WorkspaceItem> = 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<WorkspaceItem> {
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<EPerson>) {
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<WorkspaceItem>) => {
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;
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div class="container pt-2 pb-4">
<h3>{{'share.submission.page.title' | translate}}</h3>
<span>{{'share.submission.page.share-link.message.start' | translate}}</span>
<a [href]="changeSubmitterLink">{{changeSubmitterLink}}</a>
<span>{{'share.submission.page.share-link.message.end' | translate}}</span>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/**
The file for styling the ShareSubmissionPageComponent.
*/
Original file line number Diff line number Diff line change
@@ -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<ShareSubmissionPageComponent>;
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();
});
});
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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 { }
19 changes: 19 additions & 0 deletions src/app/share-submission/share-submission.module.ts
Original file line number Diff line number Diff line change
@@ -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 { }
Loading