mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
[CST-5307] Migrate Researcher Profile (Angular).
This commit is contained in:
@@ -48,6 +48,7 @@
|
|||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/styles/startup.scss",
|
"src/styles/startup.scss",
|
||||||
|
"./node_modules/ngx-ui-switch/ui-switch.component.css",
|
||||||
{
|
{
|
||||||
"input": "src/styles/base-theme.scss",
|
"input": "src/styles/base-theme.scss",
|
||||||
"inject": false,
|
"inject": false,
|
||||||
|
@@ -119,7 +119,8 @@
|
|||||||
"url-parse": "^1.5.6",
|
"url-parse": "^1.5.6",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"webfontloader": "1.6.28",
|
"webfontloader": "1.6.28",
|
||||||
"zone.js": "^0.10.3"
|
"zone.js": "^0.10.3",
|
||||||
|
"ngx-ui-switch": "^11.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "10.0.1",
|
"@angular-builders/custom-webpack": "10.0.1",
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue, isEmpty } from '../../shared/empty.util';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
@@ -27,7 +27,13 @@ export class DSONameService {
|
|||||||
*/
|
*/
|
||||||
private readonly factories = {
|
private readonly factories = {
|
||||||
Person: (dso: DSpaceObject): string => {
|
Person: (dso: DSpaceObject): string => {
|
||||||
return `${dso.firstMetadataValue('person.familyName')}, ${dso.firstMetadataValue('person.givenName')}`;
|
const familyName = dso.firstMetadataValue('person.familyName');
|
||||||
|
const givenName = dso.firstMetadataValue('person.givenName');
|
||||||
|
if (isEmpty(familyName) && isEmpty(givenName)) {
|
||||||
|
return dso.firstMetadataValue('dc.title') || dso.name;
|
||||||
|
} else {
|
||||||
|
return `${familyName}, ${givenName}`;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
OrgUnit: (dso: DSpaceObject): string => {
|
OrgUnit: (dso: DSpaceObject): string => {
|
||||||
return dso.firstMetadataValue('organization.legalName');
|
return dso.firstMetadataValue('organization.legalName');
|
||||||
|
@@ -162,6 +162,9 @@ import { SearchConfig } from './shared/search/search-filters/search-config.model
|
|||||||
import { SequenceService } from './shared/sequence.service';
|
import { SequenceService } from './shared/sequence.service';
|
||||||
import { GroupDataService } from './eperson/group-data.service';
|
import { GroupDataService } from './eperson/group-data.service';
|
||||||
import { SubmissionAccessesModel } from './config/models/config-submission-accesses.model';
|
import { SubmissionAccessesModel } from './config/models/config-submission-accesses.model';
|
||||||
|
import { ResearcherProfileService } from './profile/researcher-profile.service';
|
||||||
|
import { ProfileClaimService } from '../profile-page/profile-claim/profile-claim.service';
|
||||||
|
import { ResearcherProfile } from './profile/model/researcher-profile.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When not in production, endpoint responses can be mocked for testing purposes
|
* When not in production, endpoint responses can be mocked for testing purposes
|
||||||
@@ -286,6 +289,8 @@ const PROVIDERS = [
|
|||||||
SequenceService,
|
SequenceService,
|
||||||
GroupDataService,
|
GroupDataService,
|
||||||
FeedbackDataService,
|
FeedbackDataService,
|
||||||
|
ResearcherProfileService,
|
||||||
|
ProfileClaimService
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -345,7 +350,8 @@ export const models =
|
|||||||
UsageReport,
|
UsageReport,
|
||||||
Root,
|
Root,
|
||||||
SearchConfig,
|
SearchConfig,
|
||||||
SubmissionAccessesModel
|
SubmissionAccessesModel,
|
||||||
|
ResearcherProfile
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
49
src/app/core/profile/model/researcher-profile.model.ts
Normal file
49
src/app/core/profile/model/researcher-profile.model.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { autoserialize, deserialize, deserializeAs } from 'cerialize';
|
||||||
|
import { typedObject } from '../../cache/builders/build-decorators';
|
||||||
|
import { CacheableObject } from '../../cache/object-cache.reducer';
|
||||||
|
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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class the represents a Researcher Profile.
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
export class ResearcherProfile extends CacheableObject {
|
||||||
|
|
||||||
|
static type = RESEARCHER_PROFILE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The object type
|
||||||
|
*/
|
||||||
|
@excludeFromEquals
|
||||||
|
@autoserialize
|
||||||
|
type: ResourceType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier of this Researcher Profile
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@deserializeAs('id')
|
||||||
|
uuid: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The visibility of this Researcher Profile
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
visible: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link HALLink}s for this Researcher Profile
|
||||||
|
*/
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
self: HALLink,
|
||||||
|
item: HALLink,
|
||||||
|
eperson: HALLink
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,9 @@
|
|||||||
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for ResearcherProfile
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
export const RESEARCHER_PROFILE = new ResourceType('profile');
|
267
src/app/core/profile/researcher-profile.service.ts
Normal file
267
src/app/core/profile/researcher-profile.service.ts
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { Operation, RemoveOperation, ReplaceOperation } from 'fast-json-patch';
|
||||||
|
import { combineLatest, Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { catchError, find, map, switchMap, tap } from 'rxjs/operators';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { dataService } from '../cache/builders/build-decorators';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { ConfigurationDataService } from '../data/configuration-data.service';
|
||||||
|
import { DataService } from '../data/data.service';
|
||||||
|
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
|
||||||
|
import { ItemDataService } from '../data/item-data.service';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { ConfigurationProperty } from '../shared/configuration-property.model';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { Item } from '../shared/item.model';
|
||||||
|
import { NoContent } from '../shared/NoContent.model';
|
||||||
|
import {
|
||||||
|
getFinishedRemoteData,
|
||||||
|
getFirstCompletedRemoteData,
|
||||||
|
getFirstSucceededRemoteDataPayload
|
||||||
|
} from '../shared/operators';
|
||||||
|
import { ResearcherProfile } from './model/researcher-profile.model';
|
||||||
|
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';
|
||||||
|
|
||||||
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A private DataService implementation to delegate specific methods to.
|
||||||
|
*/
|
||||||
|
class ResearcherProfileServiceImpl extends DataService<ResearcherProfile> {
|
||||||
|
protected linkPath = 'profiles';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<ResearcherProfile>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service that provides methods to make REST requests with researcher profile endpoint.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
@dataService(RESEARCHER_PROFILE)
|
||||||
|
export class ResearcherProfileService {
|
||||||
|
|
||||||
|
dataService: ResearcherProfileServiceImpl;
|
||||||
|
|
||||||
|
responseMsToLive: number = 10 * 1000;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected router: Router,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<ResearcherProfile>,
|
||||||
|
protected itemService: ItemDataService,
|
||||||
|
protected configurationService: ConfigurationDataService ) {
|
||||||
|
|
||||||
|
this.dataService = new ResearcherProfileServiceImpl(requestService, rdbService, store, objectCache, halService,
|
||||||
|
notificationsService, http, comparator);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the researcher profile with the given uuid.
|
||||||
|
*
|
||||||
|
* @param uuid the profile uuid
|
||||||
|
*/
|
||||||
|
findById(uuid: string): Observable<ResearcherProfile> {
|
||||||
|
return this.dataService.findById(uuid, false)
|
||||||
|
.pipe ( getFinishedRemoteData(),
|
||||||
|
map((remoteData) => remoteData.payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new researcher profile for the current user.
|
||||||
|
*/
|
||||||
|
create(): Observable<RemoteData<ResearcherProfile>> {
|
||||||
|
return this.dataService.create( new ResearcherProfile());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a researcher profile.
|
||||||
|
*
|
||||||
|
* @param researcherProfile the profile to delete
|
||||||
|
*/
|
||||||
|
delete(researcherProfile: ResearcherProfile): Observable<boolean> {
|
||||||
|
return this.dataService.delete(researcherProfile.id).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
tap((response: RemoteData<NoContent>) => {
|
||||||
|
if (response.isSuccess) {
|
||||||
|
this.requestService.setStaleByHrefSubstring(researcherProfile._links.self.href);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
map((response: RemoteData<NoContent>) => response.isSuccess)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the item id related to the given researcher profile.
|
||||||
|
*
|
||||||
|
* @param researcherProfile the profile to find for
|
||||||
|
*/
|
||||||
|
findRelatedItemId( researcherProfile: ResearcherProfile ): Observable<string> {
|
||||||
|
return this.itemService.findByHref(researcherProfile._links.item.href, false)
|
||||||
|
.pipe (getFirstSucceededRemoteDataPayload(),
|
||||||
|
catchError((error) => {
|
||||||
|
console.debug(error);
|
||||||
|
return observableOf(null);
|
||||||
|
}),
|
||||||
|
map((item) => item != null ? item.id : null ));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the visibility of the given researcher profile setting the given value.
|
||||||
|
*
|
||||||
|
* @param researcherProfile the profile to update
|
||||||
|
* @param visible the visibility value to set
|
||||||
|
*/
|
||||||
|
setVisibility(researcherProfile: ResearcherProfile, visible: boolean): Observable<ResearcherProfile> {
|
||||||
|
|
||||||
|
const replaceOperation: ReplaceOperation<boolean> = {
|
||||||
|
path: '/visible',
|
||||||
|
op: 'replace',
|
||||||
|
value: visible
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.patch(researcherProfile, [replaceOperation]).pipe (
|
||||||
|
switchMap( ( ) => this.findById(researcherProfile.id))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
patch(researcherProfile: ResearcherProfile, operations: Operation[]): Observable<RemoteData<ResearcherProfile>> {
|
||||||
|
return this.dataService.patch(researcherProfile, operations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given item is linked to an ORCID profile.
|
||||||
|
*
|
||||||
|
* @param item the item to check
|
||||||
|
* @returns the check result
|
||||||
|
*/
|
||||||
|
isLinkedToOrcid(item: Item): boolean {
|
||||||
|
return item.hasMetadata('cris.orcid.authenticated');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if only the admin users can disconnect a researcher profile from ORCID.
|
||||||
|
*
|
||||||
|
* @returns the check result
|
||||||
|
*/
|
||||||
|
onlyAdminCanDisconnectProfileFromOrcid(): Observable<boolean> {
|
||||||
|
return this.getOrcidDisconnectionAllowedUsersConfiguration().pipe(
|
||||||
|
map((property) => property.values.map( (value) => value.toLowerCase()).includes('only_admin'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the profile's owner can disconnect that profile from ORCID.
|
||||||
|
*
|
||||||
|
* @returns the check result
|
||||||
|
*/
|
||||||
|
ownerCanDisconnectProfileFromOrcid(): Observable<boolean> {
|
||||||
|
return this.getOrcidDisconnectionAllowedUsersConfiguration().pipe(
|
||||||
|
map((property) => {
|
||||||
|
const values = property.values.map( (value) => value.toLowerCase());
|
||||||
|
return values.includes('only_owner') || values.includes('admin_and_owner');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the admin users can disconnect a researcher profile from ORCID.
|
||||||
|
*
|
||||||
|
* @returns the check result
|
||||||
|
*/
|
||||||
|
adminCanDisconnectProfileFromOrcid(): Observable<boolean> {
|
||||||
|
return this.getOrcidDisconnectionAllowedUsersConfiguration().pipe(
|
||||||
|
map((property) => {
|
||||||
|
const values = property.values.map( (value) => value.toLowerCase());
|
||||||
|
return values.includes('only_admin') || values.includes('admin_and_owner');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the given item represents a profile unlink it from ORCID.
|
||||||
|
*/
|
||||||
|
unlinkOrcid(item: Item): Observable<RemoteData<ResearcherProfile>> {
|
||||||
|
|
||||||
|
const operations: RemoveOperation[] = [{
|
||||||
|
path:'/orcid',
|
||||||
|
op:'remove'
|
||||||
|
}];
|
||||||
|
|
||||||
|
return this.findById(item.firstMetadata('cris.owner').authority).pipe(
|
||||||
|
switchMap((profile) => this.patch(profile, operations)),
|
||||||
|
getFinishedRemoteData()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrcidAuthorizeUrl(profile: Item): Observable<string> {
|
||||||
|
return combineLatest([
|
||||||
|
this.configurationService.findByPropertyName('orcid.authorize-url').pipe(getFirstSucceededRemoteDataPayload()),
|
||||||
|
this.configurationService.findByPropertyName('orcid.application-client-id').pipe(getFirstSucceededRemoteDataPayload()),
|
||||||
|
this.configurationService.findByPropertyName('orcid.scope').pipe(getFirstSucceededRemoteDataPayload())]
|
||||||
|
).pipe(
|
||||||
|
map(([authorizeUrl, clientId, scopes]) => {
|
||||||
|
const redirectUri = environment.rest.baseUrl + '/api/cris/orcid/' + profile.id + '/?url=' + encodeURIComponent(this.router.url);
|
||||||
|
return authorizeUrl.values[0] + '?client_id=' + clientId.values[0] + '&redirect_uri=' + redirectUri + '&response_type=code&scope='
|
||||||
|
+ scopes.values.join(' ');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a researcher profile starting from an external source URI
|
||||||
|
* @param sourceUri URI of source item of researcher profile.
|
||||||
|
*/
|
||||||
|
public createFromExternalSource(sourceUri: string): Observable<RemoteData<ResearcherProfile>> {
|
||||||
|
const options: HttpOptions = Object.create({});
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
headers = headers.append('Content-Type', 'text/uri-list');
|
||||||
|
options.headers = headers;
|
||||||
|
|
||||||
|
const requestId = this.requestService.generateRequestId();
|
||||||
|
const href$ = this.halService.getEndpoint(this.dataService.getLinkPath());
|
||||||
|
|
||||||
|
href$.pipe(
|
||||||
|
find((href: string) => hasValue(href)),
|
||||||
|
map((href: string) => {
|
||||||
|
const request = new PostRequest(requestId, href, sourceUri, options);
|
||||||
|
this.requestService.send(request);
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
|
return this.rdbService.buildFromRequestUUID(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOrcidDisconnectionAllowedUsersConfiguration(): Observable<ConfigurationProperty> {
|
||||||
|
return this.configurationService.findByPropertyName('orcid.disconnection.allowed-users').pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
76
src/app/profile-page/profile-claim/profile-claim.service.ts
Normal file
76
src/app/profile-page/profile-claim/profile-claim.service.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { mergeMap, switchMap, take } from 'rxjs/operators';
|
||||||
|
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
import { SearchService } from '../../core/shared/search/search.service';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||||
|
import { SearchResult } from '../../shared/search/models/search-result.model';
|
||||||
|
import { getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload } from './../../core/shared/operators';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ProfileClaimService {
|
||||||
|
|
||||||
|
constructor(private searchService: SearchService,
|
||||||
|
private configurationService: ConfigurationDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
canClaimProfiles(eperson: EPerson): Observable<boolean> {
|
||||||
|
|
||||||
|
const query = this.personQueryData(eperson);
|
||||||
|
|
||||||
|
if (!hasValue(query) || query.length === 0) {
|
||||||
|
return of(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.configurationService.findByPropertyName('claimable.entityType').pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
switchMap((claimableTypes) => {
|
||||||
|
if (!claimableTypes.values || claimableTypes.values.length === 0) {
|
||||||
|
return of(false);
|
||||||
|
} else {
|
||||||
|
return this.lookup(query).pipe(
|
||||||
|
mergeMap((rd: RemoteData<PaginatedList<SearchResult<DSpaceObject>>>) => of(rd.payload.totalElements > 0))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
search(eperson: EPerson): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
||||||
|
const query = this.personQueryData(eperson);
|
||||||
|
if (!hasValue(query) || query.length === 0) {
|
||||||
|
return of(null);
|
||||||
|
}
|
||||||
|
return this.lookup(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
private lookup(query: string): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
||||||
|
if (!hasValue(query)) {
|
||||||
|
return of(null);
|
||||||
|
}
|
||||||
|
return this.searchService.search(new PaginatedSearchOptions({
|
||||||
|
configuration: 'eperson_claims',
|
||||||
|
query: query
|
||||||
|
}))
|
||||||
|
.pipe(
|
||||||
|
getFirstSucceededRemoteData(),
|
||||||
|
take(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private personQueryData(eperson: EPerson): string {
|
||||||
|
const querySections = [];
|
||||||
|
this.queryParam(querySections, 'dc.title', eperson.name);
|
||||||
|
this.queryParam(querySections, 'crisrp.name', eperson.name);
|
||||||
|
return querySections.join(' OR ');
|
||||||
|
}
|
||||||
|
|
||||||
|
private queryParam(query: string[], metadata: string, value: string) {
|
||||||
|
if (!hasValue(value)) {return;}
|
||||||
|
query.push(metadata + ':' + value);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,35 @@
|
|||||||
|
<div *ngVar="(researcherProfile$ | async) as researcherProfile">
|
||||||
|
<div *ngIf="researcherProfile">
|
||||||
|
<p>{{'researcher.profile.associated' | translate}}</p>
|
||||||
|
<p class="align-items-center researcher-profile-switch" >
|
||||||
|
<span class="mr-3">{{'researcher.profile.status' | translate}}</span>
|
||||||
|
<ui-switch [checkedLabel]="'researcher.profile.public.visibility' | translate" [uncheckedLabel]="'researcher.profile.private.visibility' | translate" (change)="toggleProfileVisibility(researcherProfile)" [checked]="researcherProfile.visible"></ui-switch>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!researcherProfile">
|
||||||
|
<p>{{'researcher.profile.not.associated' | translate}}</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary"
|
||||||
|
[disabled]="researcherProfile || (isProcessingCreate() | async)"
|
||||||
|
(click)="createProfile()">
|
||||||
|
<span *ngIf="(isProcessingCreate() | async)">
|
||||||
|
<i class='fas fa-circle-notch fa-spin'></i> {{'researcher.profile.action.processing' | translate}}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="!(isProcessingCreate() | async)">
|
||||||
|
<i class="fas fa-plus"></i> {{'researcher.profile.create.new' | translate}}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<ng-container *ngIf="researcherProfile">
|
||||||
|
<button class="btn btn-primary" [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)">
|
||||||
|
<span *ngIf="(isProcessingDelete() | async)">
|
||||||
|
<i class='fas fa-circle-notch fa-spin'></i> {{'researcher.profile.action.processing' | translate}}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="!(isProcessingDelete() | async)">
|
||||||
|
<i class="fas fa-trash-alt"></i> {{'researcher.profile.delete' | translate}}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
@@ -0,0 +1,162 @@
|
|||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
|
|
||||||
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model';
|
||||||
|
import { ResearcherProfileService } from '../../core/profile/researcher-profile.service';
|
||||||
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
|
import { ProfilePageResearcherFormComponent } from './profile-page-researcher-form.component';
|
||||||
|
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', () => {
|
||||||
|
|
||||||
|
let component: ProfilePageResearcherFormComponent;
|
||||||
|
let fixture: ComponentFixture<ProfilePageResearcherFormComponent>;
|
||||||
|
let router: Router;
|
||||||
|
|
||||||
|
let user: EPerson;
|
||||||
|
let profile: ResearcherProfile;
|
||||||
|
|
||||||
|
let researcherProfileService: ResearcherProfileService;
|
||||||
|
|
||||||
|
let notificationsServiceStub: NotificationsServiceStub;
|
||||||
|
|
||||||
|
let profileClaimService: ProfileClaimService;
|
||||||
|
|
||||||
|
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(), {
|
||||||
|
id: 'beef9946-f4ce-479e-8f11-b90cbe9f7241'
|
||||||
|
});
|
||||||
|
|
||||||
|
profile = Object.assign(new ResearcherProfile(), {
|
||||||
|
id: 'beef9946-f4ce-479e-8f11-b90cbe9f7241',
|
||||||
|
visible: false,
|
||||||
|
type: 'profile'
|
||||||
|
});
|
||||||
|
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
getAuthenticatedUserFromStore: observableOf(user)
|
||||||
|
});
|
||||||
|
|
||||||
|
researcherProfileService = jasmine.createSpyObj('researcherProfileService', {
|
||||||
|
findById: observableOf(profile),
|
||||||
|
create: observableOf(profile),
|
||||||
|
setVisibility: observableOf(profile),
|
||||||
|
delete: observableOf(true),
|
||||||
|
findRelatedItemId: observableOf('a42557ca-cbb8-4442-af9c-3bb5cad2d075')
|
||||||
|
});
|
||||||
|
|
||||||
|
notificationsServiceStub = new NotificationsServiceStub();
|
||||||
|
|
||||||
|
profileClaimService = jasmine.createSpyObj('profileClaimService', {
|
||||||
|
canClaimProfiles: observableOf(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
editItemDataService = jasmine.createSpyObj('EditItemDataService', {
|
||||||
|
findById: createSuccessfulRemoteDataObject$(editItem)
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ProfilePageResearcherFormComponent, VarDirective],
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
|
providers: [
|
||||||
|
NgbModal,
|
||||||
|
{ provide: ResearcherProfileService, useValue: researcherProfileService },
|
||||||
|
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
||||||
|
{ provide: ProfileClaimService, useValue: profileClaimService },
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: EditItemDataService, useValue: editItemDataService }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ProfilePageResearcherFormComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.user = user;
|
||||||
|
router = TestBed.inject(Router);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should search the researcher profile for the current user', () => {
|
||||||
|
expect(researcherProfileService.findById).toHaveBeenCalledWith(user.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createProfile', () => {
|
||||||
|
|
||||||
|
it('should create the profile', () => {
|
||||||
|
component.createProfile();
|
||||||
|
expect(researcherProfileService.create).toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toggleProfileVisibility', () => {
|
||||||
|
|
||||||
|
it('should set the profile visibility to true', () => {
|
||||||
|
profile.visible = false;
|
||||||
|
component.toggleProfileVisibility(profile);
|
||||||
|
expect(researcherProfileService.setVisibility).toHaveBeenCalledWith(profile, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the profile visibility to false', () => {
|
||||||
|
profile.visible = true;
|
||||||
|
component.toggleProfileVisibility(profile);
|
||||||
|
expect(researcherProfileService.setVisibility).toHaveBeenCalledWith(profile, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteProfile', () => {
|
||||||
|
|
||||||
|
it('should delete the profile', () => {
|
||||||
|
component.deleteProfile(profile);
|
||||||
|
expect(researcherProfileService.delete).toHaveBeenCalledWith(profile);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('viewProfile', () => {
|
||||||
|
|
||||||
|
it('should open the item details page', () => {
|
||||||
|
spyOn(router, 'navigate');
|
||||||
|
component.viewProfile(profile);
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['items', 'a42557ca-cbb8-4442-af9c-3bb5cad2d075']);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,181 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
import { filter, mergeMap, switchMap, take, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||||
|
import { ClaimItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model';
|
||||||
|
import { ResearcherProfileService } from '../../core/profile/researcher-profile.service';
|
||||||
|
import { ProfileClaimService } from '../profile-claim/profile-claim.service';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-profile-page-researcher-form',
|
||||||
|
templateUrl: './profile-page-researcher-form.component.html',
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component for a user to create/delete or change his researcher profile.
|
||||||
|
*/
|
||||||
|
export class ProfilePageResearcherFormComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user to display the form for.
|
||||||
|
*/
|
||||||
|
@Input() user: EPerson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The researcher profile to show.
|
||||||
|
*/
|
||||||
|
researcherProfile$: BehaviorSubject<ResearcherProfile> = new BehaviorSubject<ResearcherProfile>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if a delete operation is pending
|
||||||
|
* @type {BehaviorSubject<boolean>}
|
||||||
|
*/
|
||||||
|
processingDelete$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if a create delete operation is pending
|
||||||
|
* @type {BehaviorSubject<boolean>}
|
||||||
|
*/
|
||||||
|
processingCreate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If exists The uuid of the item associated to the researcher profile
|
||||||
|
*/
|
||||||
|
researcherProfileItemId: string;
|
||||||
|
|
||||||
|
constructor(protected researcherProfileService: ResearcherProfileService,
|
||||||
|
protected profileClaimService: ProfileClaimService,
|
||||||
|
protected translationService: TranslateService,
|
||||||
|
protected notificationService: NotificationsService,
|
||||||
|
protected authService: AuthService,
|
||||||
|
protected router: Router,
|
||||||
|
protected modalService: NgbModal) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the component searching the current user researcher profile.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
// Retrieve researcherProfile if exists
|
||||||
|
this.initResearchProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new profile for the current user.
|
||||||
|
*/
|
||||||
|
createProfile(): void {
|
||||||
|
this.processingCreate$.next(true);
|
||||||
|
|
||||||
|
this.authService.getAuthenticatedUserFromStore().pipe(
|
||||||
|
switchMap((currentUser) => this.profileClaimService.canClaimProfiles(currentUser)))
|
||||||
|
.subscribe((canClaimProfiles) => {
|
||||||
|
|
||||||
|
if (canClaimProfiles) {
|
||||||
|
this.processingCreate$.next(false);
|
||||||
|
const modal = this.modalService.open(ClaimItemSelectorComponent);
|
||||||
|
modal.componentInstance.dso = this.user;
|
||||||
|
modal.componentInstance.create.pipe(take(1)).subscribe(() => {
|
||||||
|
this.createProfileFromScratch();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.createProfileFromScratch();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the items section to show the profile item details.
|
||||||
|
*
|
||||||
|
* @param researcherProfile the current researcher profile
|
||||||
|
*/
|
||||||
|
viewProfile(researcherProfile: ResearcherProfile): void {
|
||||||
|
if (this.researcherProfileItemId != null) {
|
||||||
|
this.router.navigate(['items', this.researcherProfileItemId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the given researcher profile.
|
||||||
|
*
|
||||||
|
* @param researcherProfile the profile to delete
|
||||||
|
*/
|
||||||
|
deleteProfile(researcherProfile: ResearcherProfile): void {
|
||||||
|
this.processingDelete$.next(true);
|
||||||
|
this.researcherProfileService.delete(researcherProfile)
|
||||||
|
.subscribe((deleted) => {
|
||||||
|
if (deleted) {
|
||||||
|
this.researcherProfile$.next(null);
|
||||||
|
this.researcherProfileItemId = null;
|
||||||
|
}
|
||||||
|
this.processingDelete$.next(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the visibility of the given researcher profile.
|
||||||
|
*
|
||||||
|
* @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 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a boolean representing if a delete operation is pending.
|
||||||
|
*
|
||||||
|
* @return {Observable<boolean>}
|
||||||
|
*/
|
||||||
|
isProcessingDelete(): Observable<boolean> {
|
||||||
|
return this.processingDelete$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a boolean representing if a create operation is pending.
|
||||||
|
*
|
||||||
|
* @return {Observable<boolean>}
|
||||||
|
*/
|
||||||
|
isProcessingCreate(): Observable<boolean> {
|
||||||
|
return this.processingCreate$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
createProfileFromScratch() {
|
||||||
|
this.processingCreate$.next(true);
|
||||||
|
this.researcherProfileService.create().pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
).subscribe((remoteData) => {
|
||||||
|
this.processingCreate$.next(false);
|
||||||
|
if (remoteData.isSuccess) {
|
||||||
|
this.initResearchProfile();
|
||||||
|
this.notificationService.success(this.translationService.get('researcher.profile.create.success'));
|
||||||
|
} else {
|
||||||
|
this.notificationService.error(this.translationService.get('researcher.profile.create.fail'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private initResearchProfile(): void {
|
||||||
|
this.researcherProfileService.findById(this.user.id).pipe(
|
||||||
|
take(1),
|
||||||
|
filter((researcherProfile) => isNotEmpty(researcherProfile)),
|
||||||
|
tap((researcherProfile) => this.researcherProfile$.next(researcherProfile)),
|
||||||
|
mergeMap((researcherProfile) => this.researcherProfileService.findRelatedItemId(researcherProfile)),
|
||||||
|
).subscribe((itemId: string) => {
|
||||||
|
this.researcherProfileItemId = itemId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
<div class="container-fluid mb-4">{{FORM_PREFIX + 'info' | translate}}</div>
|
<ds-alert class="mb-4" [type]="'alert-info'">{{FORM_PREFIX + 'info' | translate}}</ds-alert>
|
||||||
<ds-form *ngIf="formModel"
|
<ds-form *ngIf="formModel"
|
||||||
[formId]="FORM_PREFIX"
|
[formId]="FORM_PREFIX"
|
||||||
[formModel]="formModel"
|
[formModel]="formModel"
|
||||||
|
@@ -1,6 +1,15 @@
|
|||||||
<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>
|
<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>
|
||||||
|
<!-- <ds-suggestions-notification></ds-suggestions-notification> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<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">
|
||||||
|
@@ -5,25 +5,33 @@ import { ProfilePageRoutingModule } from './profile-page-routing.module';
|
|||||||
import { ProfilePageComponent } from './profile-page.component';
|
import { ProfilePageComponent } from './profile-page.component';
|
||||||
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 { ProfilePageSecurityFormComponent } from './profile-page-security-form/profile-page-security-form.component';
|
import { ProfilePageSecurityFormComponent } from './profile-page-security-form/profile-page-security-form.component';
|
||||||
|
import { ProfilePageResearcherFormComponent } from './profile-page-researcher-form/profile-page-researcher-form.component';
|
||||||
import { ThemedProfilePageComponent } from './themed-profile-page.component';
|
import { ThemedProfilePageComponent } from './themed-profile-page.component';
|
||||||
import { FormModule } from '../shared/form/form.module';
|
import { FormModule } from '../shared/form/form.module';
|
||||||
|
import { UiSwitchModule } from 'ngx-ui-switch';
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
ProfilePageRoutingModule,
|
ProfilePageRoutingModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
FormModule
|
FormModule,
|
||||||
|
UiSwitchModule
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
|
ProfilePageComponent,
|
||||||
|
ThemedProfilePageComponent,
|
||||||
|
ProfilePageMetadataFormComponent,
|
||||||
ProfilePageSecurityFormComponent,
|
ProfilePageSecurityFormComponent,
|
||||||
ProfilePageMetadataFormComponent
|
ProfilePageResearcherFormComponent
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
ProfilePageComponent,
|
ProfilePageComponent,
|
||||||
ThemedProfilePageComponent,
|
ThemedProfilePageComponent,
|
||||||
ProfilePageMetadataFormComponent,
|
ProfilePageMetadataFormComponent,
|
||||||
ProfilePageSecurityFormComponent
|
ProfilePageSecurityFormComponent,
|
||||||
|
ProfilePageResearcherFormComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class ProfilePageModule {
|
export class ProfilePageModule {
|
||||||
|
@@ -0,0 +1,37 @@
|
|||||||
|
<div>
|
||||||
|
<div class="modal-header"><h4>{{'dso-selector.claim.item.head' | translate}}</h4>
|
||||||
|
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
{{'dso-selector.claim.item.body' | translate}}
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<div class="scrollable-menu list-group container">
|
||||||
|
<div *ngFor="let listEntry of (listEntries$ | async)?.payload.page" class="row">
|
||||||
|
<button class="list-group-item list-group-item-action border-0 list-entry"
|
||||||
|
title="{{ listEntry.indexableObject.name }}"
|
||||||
|
(click)="selectItem(listEntry.indexableObject)" #listEntryElement>
|
||||||
|
<ds-listable-object-component-loader [object]="listEntry" [viewMode]="viewMode"
|
||||||
|
[linkType]=linkTypes.None></ds-listable-object-component-loader>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md mt-2">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="mr-5">
|
||||||
|
<input type="checkbox" [checked]="false" (change)="toggleCheckbox()"/>
|
||||||
|
{{ 'dso-selector.claim.item.not-mine-label' | translate }}
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary ml-5 mr-2" (click)="createFromScratch()" [disabled]="!checked">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
{{ 'dso-selector.claim.item.create-from-scratch' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,45 @@
|
|||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
/* tslint:disable:no-unused-variable */
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { ClaimItemSelectorComponent } from './claim-item-selector.component';
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { ProfileClaimService } from '../../../../profile-page/profile-claim/profile-claim.service';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
|
describe('ClaimItemSelectorComponent', () => {
|
||||||
|
let component: ClaimItemSelectorComponent;
|
||||||
|
let fixture: ComponentFixture<ClaimItemSelectorComponent>;
|
||||||
|
|
||||||
|
const profileClaimService = jasmine.createSpyObj('profileClaimService', {
|
||||||
|
search: of({ payload: {page: []}})
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot()],
|
||||||
|
declarations: [ ClaimItemSelectorComponent ],
|
||||||
|
providers: [
|
||||||
|
{ provide: NgbActiveModal, useValue: {} },
|
||||||
|
{ provide: ActivatedRoute, useValue: {} },
|
||||||
|
{ provide: Router, useValue: {} },
|
||||||
|
{ provide: ProfileClaimService, useValue: profileClaimService }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ClaimItemSelectorComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,68 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { SearchResult } from '../../../search/models/search-result.model';
|
||||||
|
import { DSOSelectorModalWrapperComponent } from '../dso-selector-modal-wrapper.component';
|
||||||
|
import { getItemPageRoute } from '../../../../item-page/item-page-routing-paths';
|
||||||
|
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
||||||
|
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||||
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
|
import { ProfileClaimService } from '../../../../profile-page/profile-claim/profile-claim.service';
|
||||||
|
import { CollectionElementLinkType } from '../../../object-collection/collection-element-link.type';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-claim-item-selector',
|
||||||
|
templateUrl: './claim-item-selector.component.html'
|
||||||
|
})
|
||||||
|
export class ClaimItemSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
|
||||||
|
|
||||||
|
@Input() dso: DSpaceObject;
|
||||||
|
|
||||||
|
listEntries$: BehaviorSubject<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> = new BehaviorSubject(null);
|
||||||
|
|
||||||
|
viewMode = ViewMode.ListElement;
|
||||||
|
|
||||||
|
// enum to be exposed
|
||||||
|
linkTypes = CollectionElementLinkType;
|
||||||
|
|
||||||
|
checked = false;
|
||||||
|
|
||||||
|
@Output() create: EventEmitter<any> = new EventEmitter<any>();
|
||||||
|
|
||||||
|
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router,
|
||||||
|
private profileClaimService: ProfileClaimService) {
|
||||||
|
super(activeModal, route);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.profileClaimService.search(this.dso as EPerson).subscribe(
|
||||||
|
(result) => this.listEntries$.next(result)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// triggered when an item is selected
|
||||||
|
selectItem(dso: DSpaceObject): void {
|
||||||
|
this.close();
|
||||||
|
this.navigate(dso);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(dso: DSpaceObject) {
|
||||||
|
this.router.navigate([getItemPageRoute(dso as Item)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleCheckbox() {
|
||||||
|
this.checked = !this.checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
createFromScratch() {
|
||||||
|
this.create.emit();
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -173,6 +173,7 @@ import { BitstreamRequestACopyPageComponent } from './bitstream-request-a-copy-p
|
|||||||
import { DsSelectComponent } from './ds-select/ds-select.component';
|
import { DsSelectComponent } from './ds-select/ds-select.component';
|
||||||
import { LogInOidcComponent } from './log-in/methods/oidc/log-in-oidc.component';
|
import { LogInOidcComponent } from './log-in/methods/oidc/log-in-oidc.component';
|
||||||
import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component';
|
import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component';
|
||||||
|
import { ClaimItemSelectorComponent } from './dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component';
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||||
@@ -343,6 +344,8 @@ const COMPONENTS = [
|
|||||||
CommunitySidebarSearchListElementComponent,
|
CommunitySidebarSearchListElementComponent,
|
||||||
SearchNavbarComponent,
|
SearchNavbarComponent,
|
||||||
ScopeSelectorModalComponent,
|
ScopeSelectorModalComponent,
|
||||||
|
|
||||||
|
ClaimItemSelectorComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
@@ -399,6 +402,7 @@ const ENTRY_COMPONENTS = [
|
|||||||
OnClickMenuItemComponent,
|
OnClickMenuItemComponent,
|
||||||
TextMenuItemComponent,
|
TextMenuItemComponent,
|
||||||
ScopeSelectorModalComponent,
|
ScopeSelectorModalComponent,
|
||||||
|
ClaimItemSelectorComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const SHARED_ITEM_PAGE_COMPONENTS = [
|
const SHARED_ITEM_PAGE_COMPONENTS = [
|
||||||
|
@@ -1311,7 +1311,13 @@
|
|||||||
|
|
||||||
"dso-selector.set-scope.community.input-header": "Search for a community or collection",
|
"dso-selector.set-scope.community.input-header": "Search for a community or collection",
|
||||||
|
|
||||||
|
"dso-selector.claim.item.head": "Profile tips",
|
||||||
|
|
||||||
|
"dso-selector.claim.item.body": "These are existing profiles that may be related to you. If you recognize yourself in one of these profiles, select it and on the detail page, among the options, choose to claim it. Otherwise you can create a new profile from scratch using the button below.",
|
||||||
|
|
||||||
|
"dso-selector.claim.item.not-mine-label": "None of these are mine",
|
||||||
|
|
||||||
|
"dso-selector.claim.item.create-from-scratch": "Create a new one",
|
||||||
|
|
||||||
"confirmation-modal.export-metadata.header": "Export metadata for {{ dsoName }}",
|
"confirmation-modal.export-metadata.header": "Export metadata for {{ dsoName }}",
|
||||||
|
|
||||||
@@ -2907,7 +2913,7 @@
|
|||||||
|
|
||||||
"profile.title": "Update Profile",
|
"profile.title": "Update Profile",
|
||||||
|
|
||||||
|
"profile.card.researcher": "Researcher Profile",
|
||||||
|
|
||||||
"project.listelement.badge": "Research Project",
|
"project.listelement.badge": "Research Project",
|
||||||
|
|
||||||
@@ -4165,5 +4171,41 @@
|
|||||||
|
|
||||||
"idle-modal.log-out": "Log out",
|
"idle-modal.log-out": "Log out",
|
||||||
|
|
||||||
"idle-modal.extend-session": "Extend session"
|
"idle-modal.extend-session": "Extend session",
|
||||||
|
|
||||||
|
"researcher.profile.action.processing" : "Processing...",
|
||||||
|
|
||||||
|
"researcher.profile.associated": "Researcher profile associated",
|
||||||
|
|
||||||
|
"researcher.profile.create.new": "Create new",
|
||||||
|
|
||||||
|
"researcher.profile.create.success": "Researcher profile created successfully",
|
||||||
|
|
||||||
|
"researcher.profile.create.fail": "An error occurs during the researcher profile creation",
|
||||||
|
|
||||||
|
"researcher.profile.delete": "Delete",
|
||||||
|
|
||||||
|
"researcher.profile.expose": "Expose",
|
||||||
|
|
||||||
|
"researcher.profile.hide": "Hide",
|
||||||
|
|
||||||
|
"researcher.profile.not.associated": "Researcher profile not yet associated",
|
||||||
|
|
||||||
|
"researcher.profile.view": "View",
|
||||||
|
|
||||||
|
"researcher.profile.private.visibility" : "PRIVATE",
|
||||||
|
|
||||||
|
"researcher.profile.public.visibility" : "PUBLIC",
|
||||||
|
|
||||||
|
"researcher.profile.status": "Status:",
|
||||||
|
|
||||||
|
"researcherprofile.claim.not-authorized": "You are not authorized to claim this item. For more details contact the administrator(s).",
|
||||||
|
|
||||||
|
"researcherprofile.error.claim.body" : "An error occurred while claiming the profile, please try again later",
|
||||||
|
|
||||||
|
"researcherprofile.error.claim.title" : "Error",
|
||||||
|
|
||||||
|
"researcherprofile.success.claim.body" : "Profile claimed with success",
|
||||||
|
|
||||||
|
"researcherprofile.success.claim.title" : "Success",
|
||||||
}
|
}
|
||||||
|
@@ -92,3 +92,9 @@ ngb-modal-backdrop {
|
|||||||
hyphens: auto;
|
hyphens: auto;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
.researcher-profile-switch button:focus{
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
.researcher-profile-switch .switch.checked{
|
||||||
|
color: #fff;
|
||||||
|
}
|
@@ -9434,6 +9434,11 @@ ngx-sortablejs@^11.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.0.0"
|
tslib "^2.0.0"
|
||||||
|
|
||||||
|
ngx-ui-switch@^11.0.1:
|
||||||
|
version "11.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ngx-ui-switch/-/ngx-ui-switch-11.0.1.tgz#c7f1e97ebe698f827a26f49951b50492b22c7839"
|
||||||
|
integrity sha512-N8QYT/wW+xJdyh/aeebTSLPA6Sgrwp69H6KAcW0XZueg/LF+FKiqyG6Po/gFHq2gDhLikwyJEMpny8sudTI08w==
|
||||||
|
|
||||||
nice-try@^1.0.4:
|
nice-try@^1.0.4:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
|
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
|
||||||
|
Reference in New Issue
Block a user