Merge branch 'CST-5307' into CST-5249_suggestion

This commit is contained in:
Luca Giamminonni
2022-05-03 17:50:12 +02:00
12 changed files with 162 additions and 94 deletions

View File

@@ -2,6 +2,9 @@ import { Component } from '@angular/core';
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { ViewMode } from '../../../../../core/shared/view-mode.model'; 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 { 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) @listableObjectComponent('PersonSearchResult', ViewMode.ListElement)
@Component({ @Component({
@@ -14,9 +17,15 @@ import { ItemSearchResultListElementComponent } from '../../../../../shared/obje
*/ */
export class PersonSearchResultListElementComponent extends ItemSearchResultListElementComponent { export class PersonSearchResultListElementComponent extends ItemSearchResultListElementComponent {
public constructor(protected truncatableService: TruncatableService, protected dsoNameService: DSONameService) {
super(truncatableService, dsoNameService);
}
get name() { get name() {
return this.value ? let personName = this.dsoNameService.getName(this.dso);
this.value : if (isNotEmpty(this.firstMetadataValue('person.familyName')) && isNotEmpty(this.firstMetadataValue('person.givenName'))) {
this.firstMetadataValue('person.familyName') + ', ' + this.firstMetadataValue('person.givenName'); personName = this.firstMetadataValue('person.familyName') + ', ' + this.firstMetadataValue('person.givenName');
}
return personName;
} }
} }

View File

@@ -1,9 +1,10 @@
<div class="d-flex flex-row"> <div class="d-flex flex-row">
<h2 class="item-page-title-field mr-auto"> <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> </h2>
<div class="pl-2 space-children-mr"> <div class="pl-2 space-children-mr">
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'person.page.edit'"></ds-dso-page-edit-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> </div>
<div class="row"> <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)); 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 { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
import { ViewMode } from '../../../../core/shared/view-mode.model'; import { ViewMode } from '../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; 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) @listableObjectComponent('Person', ViewMode.StandalonePage)
@Component({ @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 * 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"> <div class="pl-2 space-children-mr">
<ds-dso-page-edit-button [pageRoute]="itemPageRoute$ | async" [dso]="item" <ds-dso-page-edit-button [pageRoute]="itemPageRoute$ | async" [dso]="item"
[tooltipMsg]="'item.page.edit'"></ds-dso-page-edit-button> [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> </div>
<div class="simple-view-link my-3" *ngIf="!fromWfi"> <div class="simple-view-link my-3" *ngIf="!fromWfi">
@@ -43,4 +42,4 @@
</div> </div>
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error> <ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>
<ds-loading *ngIf="itemRD?.isLoading" message="{{'loading.item' | translate}}"></ds-loading> <ds-loading *ngIf="itemRD?.isLoading" message="{{'loading.item' | translate}}"></ds-loading>
</div> </div>

View File

@@ -16,10 +16,6 @@ import { hasValue } from '../../shared/empty.util';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; 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, items: ItemDataService,
authService: AuthService, authService: AuthService,
authorizationService: AuthorizationDataService, authorizationService: AuthorizationDataService,
translate: TranslateService,
notificationsService: NotificationsService,
researcherProfileService: ResearcherProfileService,
private _location: Location) { 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 **/ /*** AoT inheritance fix, will hopefully be resolved in the near future **/

View File

@@ -64,24 +64,13 @@ export class ItemPageComponent implements OnInit {
itemUrl: string; itemUrl: string;
public claimable$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public isProcessing$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
constructor( constructor(
protected route: ActivatedRoute, protected route: ActivatedRoute,
private router: Router, private router: Router,
private items: ItemDataService, private items: ItemDataService,
private authService: AuthService, private authService: AuthService,
private authorizationService: AuthorizationDataService, private authorizationService: AuthorizationDataService
private translate: TranslateService,
private notificationsService: NotificationsService,
private researcherProfileService: ResearcherProfileService
) { ) {
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.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 { By } from '@angular/platform-browser';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; 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 { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.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 { createPaginatedList } from '../../../../shared/testing/utils.test';
import { RouteService } from '../../../../core/services/route.service'; import { RouteService } from '../../../../core/services/route.service';
import { MetadataValue } from '../../../../core/shared/metadata.models'; 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(),{ export const iiifEnabled = Object.assign(new MetadataValue(),{
'value': 'true', 'value': 'true',
@@ -69,6 +71,11 @@ export function getItemPageFieldsTest(mockItem: Item, component) {
return createSuccessfulRemoteDataObject$(new Bitstream()); return createSuccessfulRemoteDataObject$(new Bitstream());
} }
}; };
const authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true)
});
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({ imports: [TranslateModule.forRoot({
loader: { loader: {
@@ -92,7 +99,9 @@ export function getItemPageFieldsTest(mockItem: Item, component) {
{ provide: NotificationsService, useValue: {} }, { provide: NotificationsService, useValue: {} },
{ provide: DefaultChangeAnalyzer, useValue: {} }, { provide: DefaultChangeAnalyzer, useValue: {} },
{ provide: BitstreamDataService, useValue: mockBitstreamDataService }, { provide: BitstreamDataService, useValue: mockBitstreamDataService },
{ provide: RouteService, useValue: {} } { provide: RouteService, useValue: {} },
{ provide: AuthorizationDataService, useValue: authorizationService },
{ provide: ResearcherProfileService, useValue: {} }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]

View File

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

View File

@@ -1,15 +1,16 @@
<ng-container *ngVar="(user$ | async) as user"> <ng-container *ngVar="(user$ | async) as user">
<div class="container" *ngIf="user"> <div class="container" *ngIf="user">
<h3 class="mb-4">{{'profile.head' | translate}}</h3> <ng-container *ngIf="isResearcherProfileEnabled() | async">
<div class="card mb-4"> <h3 class="mb-4">{{'profile.head' | translate}}</h3>
<div class="card-header">{{'profile.card.researcher' | translate}}</div> <div class="card mb-4">
<div class="card-body"> <div class="card-header">{{'profile.card.researcher' | translate}}</div>
<div class="mb-4"> <div class="card-body">
<ds-profile-page-researcher-form [user]="user"></ds-profile-page-researcher-form> <div class="mb-4">
<ds-profile-page-researcher-form [user]="user" ></ds-profile-page-researcher-form>
</div>
</div> </div>
<!-- <ds-suggestions-notification></ds-suggestions-notification> -->
</div> </div>
</div> </ng-container>
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header">{{'profile.card.identify' | translate}}</div> <div class="card-header">{{'profile.card.identify' | translate}}</div>
<div class="card-body"> <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 { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { getTestScheduler } from 'jasmine-marbles'; import { getTestScheduler } from 'jasmine-marbles';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import {ConfigurationDataService} from '../core/data/configuration-data.service';
import {ConfigurationProperty} from '../core/shared/configuration-property.model';
describe('ProfilePageComponent', () => { describe('ProfilePageComponent', () => {
let component: ProfilePageComponent; let component: ProfilePageComponent;
@@ -80,6 +82,14 @@ describe('ProfilePageComponent', () => {
{ provide: EPersonDataService, useValue: epersonService }, { provide: EPersonDataService, useValue: epersonService },
{ provide: NotificationsService, useValue: notificationsService }, { provide: NotificationsService, useValue: notificationsService },
{ provide: AuthService, useValue: authService }, { 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 }) }, { provide: AuthorizationDataService, useValue: jasmine.createSpyObj('authorizationService', { isAuthorized: canChangePassword }) },
provideMockStore({ initialState }), provideMockStore({ initialState }),
], ],

View File

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