Merge branch 'CST-5307' into CST-5339

This commit is contained in:
Luca Giamminonni
2022-05-03 16:39:34 +02:00
16 changed files with 169 additions and 123 deletions

View File

@@ -4,7 +4,7 @@ import { HALLink } from '../../shared/hal-link.model';
import { ResourceType } from '../../shared/resource-type';
import { excludeFromEquals } from '../../utilities/equals.decorators';
import { RESEARCHER_PROFILE } from './researcher-profile.resource-type';
import {CacheableObject} from "../../cache/cacheable-object.model";
import {CacheableObject} from '../../cache/cacheable-object.model';
/**
* Class the represents a Researcher Profile.

View File

@@ -1,3 +1,4 @@
/* eslint-disable max-classes-per-file */
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
@@ -30,9 +31,7 @@ import { RESEARCHER_PROFILE } from './model/researcher-profile.resource-type';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { PostRequest } from '../data/request.models';
import { hasValue } from '../../shared/empty.util';
import {CoreState} from "../core-state.model";
/* tslint:disable:max-classes-per-file */
import {CoreState} from '../core-state.model';
/**
* A private DataService implementation to delegate specific methods to.

View File

@@ -2,6 +2,9 @@ import { Component } from '@angular/core';
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { ItemSearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component';
import {TruncatableService} from '../../../../../shared/truncatable/truncatable.service';
import {DSONameService} from '../../../../../core/breadcrumbs/dso-name.service';
import {isNotEmpty} from '../../../../../shared/empty.util';
@listableObjectComponent('PersonSearchResult', ViewMode.ListElement)
@Component({
@@ -14,9 +17,15 @@ import { ItemSearchResultListElementComponent } from '../../../../../shared/obje
*/
export class PersonSearchResultListElementComponent extends ItemSearchResultListElementComponent {
public constructor(protected truncatableService: TruncatableService, protected dsoNameService: DSONameService) {
super(truncatableService, dsoNameService);
}
get name() {
return this.value ?
this.value :
this.firstMetadataValue('person.familyName') + ', ' + this.firstMetadataValue('person.givenName');
let personName = this.dsoNameService.getName(this.dso);
if (isNotEmpty(this.firstMetadataValue('person.familyName')) && isNotEmpty(this.firstMetadataValue('person.givenName'))) {
personName = this.firstMetadataValue('person.familyName') + ', ' + this.firstMetadataValue('person.givenName');
}
return personName;
}
}

View File

@@ -1,10 +1,11 @@
<div class="d-flex flex-row">
<h2 class="item-page-title-field mr-auto">
{{'person.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="[object?.firstMetadata('person.familyName'), object?.firstMetadata('person.givenName')]" [separator]="', '"></ds-metadata-values>
{{'person.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="getTitleMetadataValues()" [separator]="', '"></ds-metadata-values>
</h2>
<div class="pl-2">
<ds-dso-page-orcid-button [pageRoute]="itemPageRoute" [dso]="object"></ds-dso-page-orcid-button>
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'person.page.edit'"></ds-dso-page-edit-button>
<button class="edit-button btn btn-dark btn-sm" *ngIf="(isClaimable() | async)" (click)="claim()"> {{"item.page.claim.button" | translate }} </button>
</div>
</div>
<div class="row">

View File

@@ -54,7 +54,12 @@ const mockItem: Item = Object.assign(new Item(), {
}
]
},
relationships: createRelationshipsObservable()
relationships: createRelationshipsObservable(),
_links: {
self : {
href: 'item-href'
}
}
});
describe('PersonComponent', getItemPageFieldsTest(mockItem, PersonComponent));

View File

@@ -1,7 +1,20 @@
import { Component } from '@angular/core';
import {Component, OnInit} from '@angular/core';
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
import { ViewMode } from '../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import {MetadataValue} from '../../../../core/shared/metadata.models';
import {FeatureID} from '../../../../core/data/feature-authorization/feature-id';
import {mergeMap, take} from 'rxjs/operators';
import {getFirstSucceededRemoteData} from '../../../../core/shared/operators';
import {RemoteData} from '../../../../core/data/remote-data';
import {ResearcherProfile} from '../../../../core/profile/model/researcher-profile.model';
import {isNotUndefined} from '../../../../shared/empty.util';
import {BehaviorSubject, Observable} from 'rxjs';
import {RouteService} from '../../../../core/services/route.service';
import {AuthorizationDataService} from '../../../../core/data/feature-authorization/authorization-data.service';
import {ResearcherProfileService} from '../../../../core/profile/researcher-profile.service';
import {NotificationsService} from '../../../../shared/notifications/notifications.service';
import {TranslateService} from '@ngx-translate/core';
@listableObjectComponent('Person', ViewMode.StandalonePage)
@Component({
@@ -12,5 +25,81 @@ import { listableObjectComponent } from '../../../../shared/object-collection/sh
/**
* The component for displaying metadata and relations of an item of the type Person
*/
export class PersonComponent extends ItemComponent {
export class PersonComponent extends ItemComponent implements OnInit {
claimable$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
constructor(protected routeService: RouteService,
protected authorizationService: AuthorizationDataService,
protected notificationsService: NotificationsService,
protected translate: TranslateService,
protected researcherProfileService: ResearcherProfileService) {
super(routeService);
}
ngOnInit(): void {
super.ngOnInit();
this.authorizationService.isAuthorized(FeatureID.ShowClaimItem, this.object._links.self.href).pipe(
take(1)
).subscribe((isAuthorized: boolean) => {
this.claimable$.next(isAuthorized);
});
}
claim() {
this.authorizationService.isAuthorized(FeatureID.CanClaimItem, this.object._links.self.href).pipe(
take(1)
).subscribe((isAuthorized: boolean) => {
if (!isAuthorized) {
this.notificationsService.warning(this.translate.get('researcherprofile.claim.not-authorized'));
} else {
this.createFromExternalSource();
}
});
}
createFromExternalSource() {
this.researcherProfileService.createFromExternalSource(this.object._links.self.href).pipe(
getFirstSucceededRemoteData(),
mergeMap((rd: RemoteData<ResearcherProfile>) => {
return this.researcherProfileService.findRelatedItemId(rd.payload);
}))
.subscribe((id: string) => {
if (isNotUndefined(id)) {
this.notificationsService.success(this.translate.get('researcherprofile.success.claim.title'),
this.translate.get('researcherprofile.success.claim.body'));
this.claimable$.next(false);
} else {
this.notificationsService.error(
this.translate.get('researcherprofile.error.claim.title'),
this.translate.get('researcherprofile.error.claim.body'));
}
});
}
isClaimable(): Observable<boolean> {
return this.claimable$;
}
getTitleMetadataValues(): MetadataValue[]{
const metadataValues = [];
const familyName = this.object?.firstMetadata('person.familyName');
const givenName = this.object?.firstMetadata('person.givenName');
const title = this.object?.firstMetadata('dc.title');
if (familyName){
metadataValues.push(familyName);
}
if (givenName){
metadataValues.push(givenName);
}
if (metadataValues.length === 0 && title){
metadataValues.push(title);
}
return metadataValues;
}
}

View File

@@ -10,7 +10,6 @@
<div class="pl-2 space-children-mr">
<ds-dso-page-edit-button [pageRoute]="itemPageRoute$ | async" [dso]="item"
[tooltipMsg]="'item.page.edit'"></ds-dso-page-edit-button>
<button class="edit-button btn btn-dark btn-sm" *ngIf="(isClaimable() | async)" (click)="claim()"> {{"item.page.claim.button" | translate }} </button>
</div>
</div>
<div class="simple-view-link my-3" *ngIf="!fromWfi">

View File

@@ -16,10 +16,6 @@ import { hasValue } from '../../shared/empty.util';
import { AuthService } from '../../core/auth/auth.service';
import { Location } from '@angular/common';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { TranslateService } from '@ngx-translate/core';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { ResearcherProfileService } from '../../core/profile/researcher-profile.service';
import { CollectionDataService } from '../../core/data/collection-data.service';
/**
@@ -52,11 +48,8 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit,
items: ItemDataService,
authService: AuthService,
authorizationService: AuthorizationDataService,
translate: TranslateService,
notificationsService: NotificationsService,
researcherProfileService: ResearcherProfileService,
private _location: Location) {
super(route, router, items, authService, authorizationService, translate, notificationsService, researcherProfileService);
super(route, router, items, authService, authorizationService);
}
/*** AoT inheritance fix, will hopefully be resolved in the near future **/

View File

@@ -64,24 +64,13 @@ export class ItemPageComponent implements OnInit {
itemUrl: string;
public claimable$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public isProcessing$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
constructor(
protected route: ActivatedRoute,
private router: Router,
private items: ItemDataService,
private authService: AuthService,
private authorizationService: AuthorizationDataService,
private translate: TranslateService,
private notificationsService: NotificationsService,
private researcherProfileService: ResearcherProfileService
private authorizationService: AuthorizationDataService
) {
this.route.data.pipe(
map((data) => data.dso as RemoteData<Item>)
).subscribe((data: RemoteData<Item>) => {
this.itemUrl = data?.payload?.self
});
}
/**
@@ -99,55 +88,5 @@ export class ItemPageComponent implements OnInit {
this.isAdmin$ = this.authorizationService.isAuthorized(FeatureID.AdministratorOf);
this.authorizationService.isAuthorized(FeatureID.ShowClaimItem, this.itemUrl).pipe(
take(1)
).subscribe((isAuthorized: boolean) => {
this.claimable$.next(isAuthorized)
});
}
claim() {
this.isProcessing$.next(true);
this.authorizationService.isAuthorized(FeatureID.CanClaimItem, this.itemUrl).pipe(
take(1)
).subscribe((isAuthorized: boolean) => {
if (!isAuthorized) {
this.notificationsService.warning(this.translate.get('researcherprofile.claim.not-authorized'));
this.isProcessing$.next(false);
} else {
this.createFromExternalSource();
}
});
}
createFromExternalSource() {
this.researcherProfileService.createFromExternalSource(this.itemUrl).pipe(
tap((rd: any) => {
if (!rd.hasSucceeded) {
this.isProcessing$.next(false);
}
}),
getFirstSucceededRemoteData(),
mergeMap((rd: RemoteData<ResearcherProfile>) => {
return this.researcherProfileService.findRelatedItemId(rd.payload);
}))
.subscribe((id: string) => {
if (isNotUndefined(id)) {
this.notificationsService.success(this.translate.get('researcherprofile.success.claim.title'),
this.translate.get('researcherprofile.success.claim.body'));
this.claimable$.next(false);
this.isProcessing$.next(false);
} else {
this.notificationsService.error(
this.translate.get('researcherprofile.error.claim.title'),
this.translate.get('researcherprofile.error.claim.body'));
}
});
}
isClaimable(): Observable<boolean> {
return this.claimable$;
}
}

View File

@@ -4,7 +4,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Store } from '@ngrx/store';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import {Observable, of as observableOf} from 'rxjs';
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
@@ -32,6 +32,8 @@ import { ItemComponent } from './item.component';
import { createPaginatedList } from '../../../../shared/testing/utils.test';
import { RouteService } from '../../../../core/services/route.service';
import { MetadataValue } from '../../../../core/shared/metadata.models';
import {AuthorizationDataService} from '../../../../core/data/feature-authorization/authorization-data.service';
import {ResearcherProfileService} from '../../../../core/profile/researcher-profile.service';
export const iiifEnabled = Object.assign(new MetadataValue(),{
'value': 'true',
@@ -69,6 +71,11 @@ export function getItemPageFieldsTest(mockItem: Item, component) {
return createSuccessfulRemoteDataObject$(new Bitstream());
}
};
const authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true)
});
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({
loader: {
@@ -92,7 +99,9 @@ export function getItemPageFieldsTest(mockItem: Item, component) {
{ provide: NotificationsService, useValue: {} },
{ provide: DefaultChangeAnalyzer, useValue: {} },
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
{ provide: RouteService, useValue: {} }
{ provide: RouteService, useValue: {} },
{ provide: AuthorizationDataService, useValue: authorizationService },
{ provide: ResearcherProfileService, useValue: {} }
],
schemas: [NO_ERRORS_SCHEMA]

View File

@@ -9,7 +9,7 @@
<div *ngIf="!researcherProfile">
<p>{{'researcher.profile.not.associated' | translate}}</p>
</div>
<button class="btn btn-primary"
<button class="btn btn-primary mr-2"
[disabled]="researcherProfile || (isProcessingCreate() | async)"
(click)="createProfile()">
<span *ngIf="(isProcessingCreate() | async)">
@@ -20,7 +20,7 @@
</span>
</button>
<ng-container *ngIf="researcherProfile">
<button class="btn btn-primary" [disabled]="!researcherProfile" (click)="viewProfile(researcherProfile)">
<button class="btn btn-primary mr-2" [disabled]="!researcherProfile" (click)="viewProfile(researcherProfile)">
<i class="fas fa-info-circle"></i> {{'researcher.profile.view' | translate}}
</button>
<button class="btn btn-danger" [disabled]="!researcherProfile" (click)="deleteProfile(researcherProfile)">

View File

@@ -16,11 +16,6 @@ import { ProfilePageResearcherFormComponent } from './profile-page-researcher-fo
import { ProfileClaimService } from '../profile-claim/profile-claim.service';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { AuthService } from 'src/app/core/auth/auth.service';
import { EditItemDataService } from '../../core/submission/edititem-data.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { EditItemMode } from '../../core/submission/models/edititem-mode.model';
import { EditItem } from '../../core/submission/models/edititem.model';
import { createPaginatedList } from '../../shared/testing/utils.test';
describe('ProfilePageResearcherFormComponent', () => {
@@ -39,17 +34,6 @@ describe('ProfilePageResearcherFormComponent', () => {
let authService: AuthService;
let editItemDataService: any;
const editItemMode: EditItemMode = Object.assign(new EditItemMode(), {
name: 'test',
label: 'test'
});
const editItem: EditItem = Object.assign(new EditItem(), {
modes: createSuccessfulRemoteDataObject$(createPaginatedList([editItemMode]))
});
function init() {
user = Object.assign(new EPerson(), {
@@ -80,10 +64,6 @@ describe('ProfilePageResearcherFormComponent', () => {
canClaimProfiles: observableOf(false),
});
editItemDataService = jasmine.createSpyObj('EditItemDataService', {
findById: createSuccessfulRemoteDataObject$(editItem)
});
}
beforeEach(waitForAsync(() => {
@@ -96,8 +76,7 @@ describe('ProfilePageResearcherFormComponent', () => {
{ provide: ResearcherProfileService, useValue: researcherProfileService },
{ provide: NotificationsService, useValue: notificationsServiceStub },
{ provide: ProfileClaimService, useValue: profileClaimService },
{ provide: AuthService, useValue: authService },
{ provide: EditItemDataService, useValue: editItemDataService }
{ provide: AuthService, useValue: authService }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();

View File

@@ -128,10 +128,10 @@ export class ProfilePageResearcherFormComponent implements OnInit {
* @param researcherProfile the profile to update
*/
toggleProfileVisibility(researcherProfile: ResearcherProfile): void {
/* tslint:disable:no-empty */
this.researcherProfileService.setVisibility(researcherProfile, !researcherProfile.visible)
.subscribe((updatedProfile) => {}); // this.researcherProfile$.next(updatedProfile);
/* tslint:enable:no-empty */
.subscribe((updatedProfile) => {
this.researcherProfile$.next(updatedProfile);
});
}
/**

View File

@@ -1,15 +1,16 @@
<ng-container *ngVar="(user$ | async) as user">
<div class="container" *ngIf="user">
<h3 class="mb-4">{{'profile.head' | translate}}</h3>
<div class="card mb-4">
<div class="card-header">{{'profile.card.researcher' | translate}}</div>
<div class="card-body">
<div class="mb-4">
<ds-profile-page-researcher-form [user]="user"></ds-profile-page-researcher-form>
<ng-container *ngIf="isResearcherProfileEnabled() | async">
<h3 class="mb-4">{{'profile.head' | translate}}</h3>
<div class="card mb-4">
<div class="card-header">{{'profile.card.researcher' | translate}}</div>
<div class="card-body">
<div class="mb-4">
<ds-profile-page-researcher-form [user]="user" ></ds-profile-page-researcher-form>
</div>
</div>
<!-- <ds-suggestions-notification></ds-suggestions-notification> -->
</div>
</div>
</ng-container>
<div class="card mb-4">
<div class="card-header">{{'profile.card.identify' | translate}}</div>
<div class="card-body">

View File

@@ -20,6 +20,8 @@ import { provideMockStore } from '@ngrx/store/testing';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { getTestScheduler } from 'jasmine-marbles';
import { By } from '@angular/platform-browser';
import {ConfigurationDataService} from '../core/data/configuration-data.service';
import {ConfigurationProperty} from '../core/shared/configuration-property.model';
describe('ProfilePageComponent', () => {
let component: ProfilePageComponent;
@@ -80,6 +82,14 @@ describe('ProfilePageComponent', () => {
{ provide: EPersonDataService, useValue: epersonService },
{ provide: NotificationsService, useValue: notificationsService },
{ provide: AuthService, useValue: authService },
{ provide: ConfigurationDataService, useValue: jasmine.createSpyObj('configurationDataService', {
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
name: 'researcher-profile.entity-type',
values: [
'Person'
]
}))
})},
{ provide: AuthorizationDataService, useValue: jasmine.createSpyObj('authorizationService', { isAuthorized: canChangePassword }) },
provideMockStore({ initialState }),
],

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { Observable } from 'rxjs';
import {BehaviorSubject, Observable} from 'rxjs';
import { EPerson } from '../core/eperson/models/eperson.model';
import { ProfilePageMetadataFormComponent } from './profile-page-metadata-form/profile-page-metadata-form.component';
import { NotificationsService } from '../shared/notifications/notifications.service';
@@ -12,7 +12,7 @@ import { EPersonDataService } from '../core/eperson/eperson-data.service';
import {
getAllSucceededRemoteData,
getRemoteDataPayload,
getFirstCompletedRemoteData
getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload
} from '../core/shared/operators';
import { hasValue, isNotEmpty } from '../shared/empty.util';
import { followLink } from '../shared/utils/follow-link-config.model';
@@ -20,6 +20,7 @@ import { AuthService } from '../core/auth/auth.service';
import { Operation } from 'fast-json-patch';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../core/data/feature-authorization/feature-id';
import {ConfigurationDataService} from '../core/data/configuration-data.service';
@Component({
selector: 'ds-profile-page',
@@ -71,11 +72,14 @@ export class ProfilePageComponent implements OnInit {
private currentUser: EPerson;
canChangePassword$: Observable<boolean>;
isResearcherProfileEnabled$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
constructor(private authService: AuthService,
private notificationsService: NotificationsService,
private translate: TranslateService,
private epersonService: EPersonDataService,
private authorizationService: AuthorizationDataService) {
private authorizationService: AuthorizationDataService,
private configurationService: ConfigurationDataService) {
}
ngOnInit(): void {
@@ -88,6 +92,10 @@ export class ProfilePageComponent implements OnInit {
);
this.groupsRD$ = this.user$.pipe(switchMap((user: EPerson) => user.groups));
this.canChangePassword$ = this.user$.pipe(switchMap((user: EPerson) => this.authorizationService.isAuthorized(FeatureID.CanChangePassword, user._links.self.href)));
this.configurationService.findByPropertyName('researcher-profile.entity-type').pipe(
getFirstSucceededRemoteDataPayload()
).subscribe(() => this.isResearcherProfileEnabled$.next(true));
}
/**
@@ -163,4 +171,9 @@ export class ProfilePageComponent implements OnInit {
submit() {
this.updateProfile();
}
isResearcherProfileEnabled(){
return this.isResearcherProfileEnabled$;
}
}