From e68e605211fa01678eaa4ce1f019ebba5d1777c1 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Wed, 14 Apr 2021 15:17:18 +0200 Subject: [PATCH 01/29] [CST-5309] Added LoginOrcid component --- src/app/core/auth/models/auth.method-type.ts | 3 +- src/app/core/auth/models/auth.method.ts | 5 + .../methods/orcid/log-in-orcid.component.html | 3 + .../orcid/log-in-orcid.component.spec.ts | 155 ++++++++++++++++++ .../methods/orcid/log-in-orcid.component.ts | 110 +++++++++++++ src/app/shared/shared.module.ts | 3 + src/assets/i18n/en.json5 | 2 + 7 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 src/app/shared/log-in/methods/orcid/log-in-orcid.component.html create mode 100644 src/app/shared/log-in/methods/orcid/log-in-orcid.component.spec.ts create mode 100644 src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts diff --git a/src/app/core/auth/models/auth.method-type.ts b/src/app/core/auth/models/auth.method-type.ts index 9d999c4c3f..594d6d8b39 100644 --- a/src/app/core/auth/models/auth.method-type.ts +++ b/src/app/core/auth/models/auth.method-type.ts @@ -4,5 +4,6 @@ export enum AuthMethodType { Ldap = 'ldap', Ip = 'ip', X509 = 'x509', - Oidc = 'oidc' + Oidc = 'oidc', + Orcid = 'orcid' } diff --git a/src/app/core/auth/models/auth.method.ts b/src/app/core/auth/models/auth.method.ts index 5a362e8606..0579ae0cd1 100644 --- a/src/app/core/auth/models/auth.method.ts +++ b/src/app/core/auth/models/auth.method.ts @@ -34,6 +34,11 @@ export class AuthMethod { this.location = location; break; } + case 'orcid': { + this.authMethodType = AuthMethodType.Orcid; + this.location = location; + break; + } default: { break; diff --git a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.html b/src/app/shared/log-in/methods/orcid/log-in-orcid.component.html new file mode 100644 index 0000000000..6f5453fd60 --- /dev/null +++ b/src/app/shared/log-in/methods/orcid/log-in-orcid.component.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.spec.ts b/src/app/shared/log-in/methods/orcid/log-in-orcid.component.spec.ts new file mode 100644 index 0000000000..001f0a4959 --- /dev/null +++ b/src/app/shared/log-in/methods/orcid/log-in-orcid.component.spec.ts @@ -0,0 +1,155 @@ +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { provideMockStore } from '@ngrx/store/testing'; +import { Store, StoreModule } from '@ngrx/store'; +import { TranslateModule } from '@ngx-translate/core'; + +import { EPerson } from '../../../../core/eperson/models/eperson.model'; +import { EPersonMock } from '../../../testing/eperson.mock'; +import { authReducer } from '../../../../core/auth/auth.reducer'; +import { AuthService } from '../../../../core/auth/auth.service'; +import { AuthServiceStub } from '../../../testing/auth-service.stub'; +import { storeModuleConfig } from '../../../../app.reducer'; +import { AuthMethod } from '../../../../core/auth/models/auth.method'; +import { AuthMethodType } from '../../../../core/auth/models/auth.method-type'; +import { LogInOrcidComponent } from './log-in-orcid.component'; +import { NativeWindowService } from '../../../../core/services/window.service'; +import { RouterStub } from '../../../testing/router.stub'; +import { ActivatedRouteStub } from '../../../testing/active-router.stub'; +import { NativeWindowMockFactory } from '../../../mocks/mock-native-window-ref'; +import { HardRedirectService } from '../../../../core/services/hard-redirect.service'; + + +describe('LogInOrcidComponent', () => { + + let component: LogInOrcidComponent; + let fixture: ComponentFixture; + let page: Page; + let user: EPerson; + let componentAsAny: any; + let setHrefSpy; + let orcidBaseUrl; + let location; + let initialState: any; + let hardRedirectService: HardRedirectService; + + beforeEach(() => { + user = EPersonMock; + orcidBaseUrl = 'dspace-rest.test/orcid?redirectUrl='; + location = orcidBaseUrl + 'http://dspace-angular.test/home'; + + hardRedirectService = jasmine.createSpyObj('hardRedirectService', { + getCurrentRoute: {}, + redirect: {} + }); + + initialState = { + core: { + auth: { + authenticated: false, + loaded: false, + blocking: false, + loading: false, + authMethods: [] + } + } + }; + }); + + beforeEach(waitForAsync(() => { + // refine the test module by declaring the test component + TestBed.configureTestingModule({ + imports: [ + StoreModule.forRoot({ auth: authReducer }, storeModuleConfig), + TranslateModule.forRoot() + ], + declarations: [ + LogInOrcidComponent + ], + providers: [ + { provide: AuthService, useClass: AuthServiceStub }, + { provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Orcid, location) }, + { provide: 'isStandalonePage', useValue: true }, + { provide: NativeWindowService, useFactory: NativeWindowMockFactory }, + { provide: Router, useValue: new RouterStub() }, + { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, + { provide: HardRedirectService, useValue: hardRedirectService }, + provideMockStore({ initialState }), + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA + ] + }) + .compileComponents(); + + })); + + beforeEach(() => { + // create component and test fixture + fixture = TestBed.createComponent(LogInOrcidComponent); + + // get test component from the fixture + component = fixture.componentInstance; + componentAsAny = component; + + // create page + page = new Page(component, fixture); + setHrefSpy = spyOnProperty(componentAsAny._window.nativeWindow.location, 'href', 'set').and.callThrough(); + + }); + + it('should set the properly a new redirectUrl', () => { + const currentUrl = 'http://dspace-angular.test/collections/12345'; + componentAsAny._window.nativeWindow.location.href = currentUrl; + + fixture.detectChanges(); + + expect(componentAsAny.injectedAuthMethodModel.location).toBe(location); + expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl); + + component.redirectToOrcid(); + + expect(setHrefSpy).toHaveBeenCalledWith(currentUrl); + + }); + + it('should not set a new redirectUrl', () => { + const currentUrl = 'http://dspace-angular.test/home'; + componentAsAny._window.nativeWindow.location.href = currentUrl; + + fixture.detectChanges(); + + expect(componentAsAny.injectedAuthMethodModel.location).toBe(location); + expect(componentAsAny._window.nativeWindow.location.href).toBe(currentUrl); + + component.redirectToOrcid(); + + expect(setHrefSpy).toHaveBeenCalledWith(currentUrl); + + }); + +}); + +/** + * I represent the DOM elements and attach spies. + * + * @class Page + */ +class Page { + + public emailInput: HTMLInputElement; + public navigateSpy: jasmine.Spy; + public passwordInput: HTMLInputElement; + + constructor(private component: LogInOrcidComponent, private fixture: ComponentFixture) { + // use injector to get services + const injector = fixture.debugElement.injector; + const store = injector.get(Store); + + // add spies + this.navigateSpy = spyOn(store, 'dispatch'); + } + +} diff --git a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts b/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts new file mode 100644 index 0000000000..df234bcbb4 --- /dev/null +++ b/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts @@ -0,0 +1,110 @@ +import { Component, Inject, OnInit, } from '@angular/core'; + +import { Observable } from 'rxjs'; +import { select, Store } from '@ngrx/store'; + +import { renderAuthMethodFor } from '../log-in.methods-decorator'; +import { AuthMethodType } from '../../../../core/auth/models/auth.method-type'; +import { AuthMethod } from '../../../../core/auth/models/auth.method'; + +import { CoreState } from '../../../../core/core.reducers'; +import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors'; +import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service'; +import { isNotNull, isEmpty } from '../../../empty.util'; +import { AuthService } from '../../../../core/auth/auth.service'; +import { HardRedirectService } from '../../../../core/services/hard-redirect.service'; +import { take } from 'rxjs/operators'; +import { URLCombiner } from '../../../../core/url-combiner/url-combiner'; + +@Component({ + selector: 'ds-log-in-orcid', + templateUrl: './log-in-orcid.component.html', +}) +@renderAuthMethodFor(AuthMethodType.Orcid) +export class LogInOrcidComponent implements OnInit { + + /** + * The authentication method data. + * @type {AuthMethod} + */ + public authMethod: AuthMethod; + + /** + * True if the authentication is loading. + * @type {boolean} + */ + public loading: Observable; + + /** + * The orcid authentication location url. + * @type {string} + */ + public location: string; + + /** + * Whether user is authenticated. + * @type {Observable} + */ + public isAuthenticated: Observable; + + /** + * @constructor + * @param {AuthMethod} injectedAuthMethodModel + * @param {boolean} isStandalonePage + * @param {NativeWindowRef} _window + * @param {AuthService} authService + * @param {HardRedirectService} hardRedirectService + * @param {Store} store + */ + constructor( + @Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod, + @Inject('isStandalonePage') public isStandalonePage: boolean, + @Inject(NativeWindowService) protected _window: NativeWindowRef, + private authService: AuthService, + private hardRedirectService: HardRedirectService, + private store: Store + ) { + this.authMethod = injectedAuthMethodModel; + } + + ngOnInit(): void { + // set isAuthenticated + this.isAuthenticated = this.store.pipe(select(isAuthenticated)); + + // set loading + this.loading = this.store.pipe(select(isAuthenticationLoading)); + + // set location + this.location = decodeURIComponent(this.injectedAuthMethodModel.location); + + } + + redirectToOrcid() { + + this.authService.getRedirectUrl().pipe(take(1)).subscribe((redirectRoute) => { + if (!this.isStandalonePage) { + redirectRoute = this.hardRedirectService.getCurrentRoute(); + } else if (isEmpty(redirectRoute)) { + redirectRoute = '/'; + } + const correctRedirectUrl = new URLCombiner(this._window.nativeWindow.origin, redirectRoute).toString(); + + let orcidServerUrl = this.location; + const myRegexp = /\?redirectUrl=(.*)/g; + const match = myRegexp.exec(this.location); + const redirectUrlFromServer = (match && match[1]) ? match[1] : null; + + // Check whether the current page is different from the redirect url received from rest + if (isNotNull(redirectUrlFromServer) && redirectUrlFromServer !== correctRedirectUrl) { + // change the redirect url with the current page url + const newRedirectUrl = `?redirectUrl=${correctRedirectUrl}`; + orcidServerUrl = this.location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl); + } + + // redirect to orcid authentication url + this.hardRedirectService.redirect(orcidServerUrl); + }); + + } + +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 7b799bfaea..574d890ede 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -173,6 +173,7 @@ import { BitstreamRequestACopyPageComponent } from './bitstream-request-a-copy-p import { DsSelectComponent } from './ds-select/ds-select.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 { LogInOrcidComponent } from './log-in/methods/orcid/log-in-orcid.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -306,6 +307,7 @@ const COMPONENTS = [ LogInShibbolethComponent, LogInOidcComponent, + LogInOrcidComponent, LogInPasswordComponent, LogInContainerComponent, ItemVersionsComponent, @@ -378,6 +380,7 @@ const ENTRY_COMPONENTS = [ LogInPasswordComponent, LogInShibbolethComponent, LogInOidcComponent, + LogInOrcidComponent, BundleListElementComponent, ClaimedTaskActionsApproveComponent, ClaimedTaskActionsRejectComponent, diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index c3c68a6882..fe034ac34a 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2389,6 +2389,8 @@ "login.form.oidc": "Log in with OIDC", + "login.form.orcid": "Log in with ORCID", + "login.form.password": "Password", "login.form.shibboleth": "Log in with Shibboleth", From cc08a2829ec05e211d4604173c6fd4a1c37893c0 Mon Sep 17 00:00:00 2001 From: Pratik Rajkotiya Date: Wed, 13 Apr 2022 17:48:26 +0530 Subject: [PATCH 02/29] [CST-5668] ORCID Authorizations added. --- .../data/feature-authorization/feature-id.ts | 1 + .../item-pages/person/person.component.html | 1 + src/app/item-page/item-page-routing-paths.ts | 1 + src/app/item-page/item-page-routing.module.ts | 10 +- src/app/item-page/item-page.module.ts | 6 + .../orcid-auth/orcid-auth.component.html | 71 +++++++ .../orcid-auth/orcid-auth.component.scss | 0 .../orcid-auth/orcid-auth.component.ts | 98 +++++++++ .../orcid-page/orcid-page.component.html | 1 + .../orcid-page/orcid-page.component.scss | 0 .../orcid-page/orcid-page.component.ts | 9 + .../item-page/orcid-page/orcid-page.guard.ts | 31 +++ .../dso-page-orcid-button.component.html | 5 + .../dso-page-orcid-button.component.scss | 0 .../dso-page-orcid-button.component.spec.ts | 76 +++++++ .../dso-page-orcid-button.component.ts | 43 ++++ src/app/shared/shared.module.ts | 2 + src/assets/i18n/en.json5 | 191 ++++++++++++++++++ src/assets/images/orcid.logo.icon.svg | 21 ++ src/styles/_global-styles.scss | 11 + 20 files changed, 577 insertions(+), 1 deletion(-) create mode 100644 src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html create mode 100644 src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.scss create mode 100644 src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts create mode 100644 src/app/item-page/orcid-page/orcid-page.component.html create mode 100644 src/app/item-page/orcid-page/orcid-page.component.scss create mode 100644 src/app/item-page/orcid-page/orcid-page.component.ts create mode 100644 src/app/item-page/orcid-page/orcid-page.guard.ts create mode 100644 src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.html create mode 100644 src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.scss create mode 100644 src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.spec.ts create mode 100644 src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.ts create mode 100644 src/assets/images/orcid.logo.icon.svg diff --git a/src/app/core/data/feature-authorization/feature-id.ts b/src/app/core/data/feature-authorization/feature-id.ts index 029c75d9cb..1576c37a83 100644 --- a/src/app/core/data/feature-authorization/feature-id.ts +++ b/src/app/core/data/feature-authorization/feature-id.ts @@ -28,4 +28,5 @@ export enum FeatureID { CanCreateVersion = 'canCreateVersion', CanViewUsageStatistics = 'canViewUsageStatistics', CanSendFeedback = 'canSendFeedback', + CanSynchronizeWithORCID = 'canSynchronizeWithORCID' } diff --git a/src/app/entity-groups/research-entities/item-pages/person/person.component.html b/src/app/entity-groups/research-entities/item-pages/person/person.component.html index 5c2fd227fd..31ad9b2463 100644 --- a/src/app/entity-groups/research-entities/item-pages/person/person.component.html +++ b/src/app/entity-groups/research-entities/item-pages/person/person.component.html @@ -3,6 +3,7 @@ {{'person.page.titleprefix' | translate}}
+
diff --git a/src/app/item-page/item-page-routing-paths.ts b/src/app/item-page/item-page-routing-paths.ts index 74ad0aae07..9da2f91431 100644 --- a/src/app/item-page/item-page-routing-paths.ts +++ b/src/app/item-page/item-page-routing-paths.ts @@ -50,3 +50,4 @@ export const ITEM_EDIT_PATH = 'edit'; export const ITEM_EDIT_VERSIONHISTORY_PATH = 'versionhistory'; export const ITEM_VERSION_PATH = 'version'; export const UPLOAD_BITSTREAM_PATH = 'bitstreams/new'; +export const ORCID_PATH = 'orcid'; \ No newline at end of file diff --git a/src/app/item-page/item-page-routing.module.ts b/src/app/item-page/item-page-routing.module.ts index 7d7912bb42..011d7bd83d 100644 --- a/src/app/item-page/item-page-routing.module.ts +++ b/src/app/item-page/item-page-routing.module.ts @@ -7,7 +7,7 @@ import { VersionResolver } from './version-page/version.resolver'; import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service'; import { LinkService } from '../core/cache/builders/link.service'; import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component'; -import { ITEM_EDIT_PATH, UPLOAD_BITSTREAM_PATH } from './item-page-routing-paths'; +import { ITEM_EDIT_PATH, ORCID_PATH, UPLOAD_BITSTREAM_PATH } from './item-page-routing-paths'; import { ItemPageAdministratorGuard } from './item-page-administrator.guard'; import { MenuItemType } from '../shared/menu/initial-menus-state'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; @@ -16,6 +16,8 @@ import { ThemedFullItemPageComponent } from './full/themed-full-item-page.compon import { VersionPageComponent } from './version-page/version-page/version-page.component'; import { BitstreamRequestACopyPageComponent } from '../shared/bitstream-request-a-copy-page/bitstream-request-a-copy-page.component'; import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths'; +import { OrcidPageComponent } from './orcid-page/orcid-page.component'; +import { OrcidPageGuard } from './orcid-page/orcid-page.guard'; @NgModule({ imports: [ @@ -50,6 +52,11 @@ import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths'; { path: REQUEST_COPY_MODULE_PATH, component: BitstreamRequestACopyPageComponent, + }, + { + path: ORCID_PATH, + component: OrcidPageComponent, + canActivate: [OrcidPageGuard] } ], data: { @@ -88,6 +95,7 @@ import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths'; LinkService, ItemPageAdministratorGuard, VersionResolver, + OrcidPageGuard ] }) diff --git a/src/app/item-page/item-page.module.ts b/src/app/item-page/item-page.module.ts index 80cb1f61a2..f584164c97 100644 --- a/src/app/item-page/item-page.module.ts +++ b/src/app/item-page/item-page.module.ts @@ -34,6 +34,9 @@ import { MiradorViewerComponent } from './mirador-viewer/mirador-viewer.componen import { VersionPageComponent } from './version-page/version-page/version-page.component'; import { VersionedItemComponent } from './simple/item-types/versioned-item/versioned-item.component'; import { ThemedFileSectionComponent } from './simple/field-components/file-section/themed-file-section.component'; +import { OrcidAuthComponent } from './orcid-page/orcid-auth/orcid-auth.component'; +import { OrcidPageComponent } from './orcid-page/orcid-page.component'; +import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; const ENTRY_COMPONENTS = [ @@ -67,6 +70,8 @@ const DECLARATIONS = [ MediaViewerImageComponent, MiradorViewerComponent, VersionPageComponent, + OrcidPageComponent, + OrcidAuthComponent ]; @NgModule({ @@ -79,6 +84,7 @@ const DECLARATIONS = [ JournalEntitiesModule.withEntryComponents(), ResearchEntitiesModule.withEntryComponents(), NgxGalleryModule, + NgbAccordionModule ], declarations: [ ...DECLARATIONS, diff --git a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html new file mode 100644 index 0000000000..8e538b66aa --- /dev/null +++ b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html @@ -0,0 +1,71 @@ +
+ + + +
+ +
+
+
+
+ + +
+
+
{{ 'person.page.orcid.granted-authorizations'| translate }}
+
+
+
    +
  • {{getAuthorizationDescription(auth) | translate}}
  • +
+
+
+
+
+
{{ 'person.page.orcid.missing-authorizations'| translate }}
+
+
+
+ {{'person.page.orcid.no-missing-authorizations-message' | translate}} +
+
+ {{'person.page.orcid.missing-authorizations-message' | translate}} +
    +
  • {{getAuthorizationDescription(auth) | translate }}
  • +
+
+
+
+
+
+
+ {{ 'person.page.orcid.remove-orcid-message' | translate}} +
+
+
+ + +
+
+
+ + +
+
orcid-logo
+
{{ getOrcidNotLinkedMessage() | async }}
+
+
+
+ +
+
+
+
\ No newline at end of file diff --git a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.scss b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts new file mode 100644 index 0000000000..9651215dc6 --- /dev/null +++ b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts @@ -0,0 +1,98 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { RemoteData } from '../../../core/data/remote-data'; +import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service'; +import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service'; +import { Item } from '../../../core/shared/item.model'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; + +@Component({ + selector: 'ds-orcid-auth', + templateUrl: './orcid-auth.component.html', + styleUrls: ['./orcid-auth.component.scss'] +}) +export class OrcidAuthComponent implements OnInit { + + missingAuthorizations$ = new BehaviorSubject([]); + + unlinkProcessing = false; + + item: Item + + constructor( + private configurationService: ConfigurationDataService, + private researcherProfileService: ResearcherProfileService, + protected translateService: TranslateService, + private notificationsService: NotificationsService, + private itemService: ItemDataService, + private route: ActivatedRoute, + @Inject(NativeWindowService) private _window: NativeWindowRef, + ) { + this.itemService.findById(this.route.snapshot.paramMap.get('id'), true, true).pipe(getFirstCompletedRemoteData()).subscribe((data: RemoteData) => { + this.item = data.payload; + }); + } + + ngOnInit() { + const scopes = this.getOrcidAuthorizations(); + return this.configurationService.findByPropertyName('orcid.scope') + .pipe(getFirstSucceededRemoteDataPayload(), + map((configurationProperty) => configurationProperty.values), + map((allScopes) => allScopes.filter((scope) => !scopes.includes(scope)))) + .subscribe((missingScopes) => this.missingAuthorizations$.next(missingScopes)); + } + + getOrcidAuthorizations(): string[] { + return this.item.allMetadataValues('cris.orcid.scope'); + } + + isLinkedToOrcid(): boolean { + return this.researcherProfileService.isLinkedToOrcid(this.item); + } + + getOrcidNotLinkedMessage(): Observable { + const orcid = this.item.firstMetadataValue('person.identifier.orcid'); + if (orcid) { + return this.translateService.get('person.page.orcid.orcid-not-linked-message', { 'orcid': orcid }); + } else { + return this.translateService.get('person.page.orcid.no-orcid-message'); + } + } + + getAuthorizationDescription(scope: string) { + return 'person.page.orcid.scope.' + scope.substring(1).replace('/', '-'); + } + + onlyAdminCanDisconnectProfileFromOrcid(): Observable { + return this.researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid(); + } + + ownerCanDisconnectProfileFromOrcid(): Observable { + return this.researcherProfileService.ownerCanDisconnectProfileFromOrcid(); + } + + linkOrcid(): void { + this.researcherProfileService.getOrcidAuthorizeUrl(this.item).subscribe((authorizeUrl) => { + this._window.nativeWindow.location.href = authorizeUrl; + }); + } + + unlinkOrcid(): void { + this.unlinkProcessing = true; + this.researcherProfileService.unlinkOrcid(this.item).subscribe((remoteData) => { + this.unlinkProcessing = false; + if (remoteData.isSuccess) { + this.notificationsService.success(this.translateService.get('person.page.orcid.unlink.success')); + } else { + this.notificationsService.error(this.translateService.get('person.page.orcid.unlink.error')); + } + }); + } + +} diff --git a/src/app/item-page/orcid-page/orcid-page.component.html b/src/app/item-page/orcid-page/orcid-page.component.html new file mode 100644 index 0000000000..ba9f445ec2 --- /dev/null +++ b/src/app/item-page/orcid-page/orcid-page.component.html @@ -0,0 +1 @@ + diff --git a/src/app/item-page/orcid-page/orcid-page.component.scss b/src/app/item-page/orcid-page/orcid-page.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/item-page/orcid-page/orcid-page.component.ts b/src/app/item-page/orcid-page/orcid-page.component.ts new file mode 100644 index 0000000000..32b47e069f --- /dev/null +++ b/src/app/item-page/orcid-page/orcid-page.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ds-orcid-page', + templateUrl: './orcid-page.component.html', + styleUrls: ['./orcid-page.component.scss'] +}) +export class OrcidPageComponent { +} diff --git a/src/app/item-page/orcid-page/orcid-page.guard.ts b/src/app/item-page/orcid-page/orcid-page.guard.ts new file mode 100644 index 0000000000..97c528e9ae --- /dev/null +++ b/src/app/item-page/orcid-page/orcid-page.guard.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { Observable, of as observableOf } from 'rxjs'; +import { AuthService } from '../../core/auth/auth.service'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { Item } from '../../core/shared/item.model'; +import { ItemPageResolver } from '../item-page.resolver'; + +@Injectable({ + providedIn: 'root' +}) +/** + * Guard for preventing unauthorized access to certain {@link Item} pages requiring administrator rights + */ +export class OrcidPageGuard extends DsoPageSingleFeatureGuard { + constructor(protected resolver: ItemPageResolver, + protected authorizationService: AuthorizationDataService, + protected router: Router, + protected authService: AuthService) { + super(resolver, authorizationService, router, authService); + } + + /** + * Check administrator authorization rights + */ + getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(FeatureID.CanSynchronizeWithORCID); + } +} diff --git a/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.html b/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.html new file mode 100644 index 0000000000..7a3383fd1a --- /dev/null +++ b/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.html @@ -0,0 +1,5 @@ +ORCID + diff --git a/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.scss b/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.spec.ts b/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.spec.ts new file mode 100644 index 0000000000..9de3333b7f --- /dev/null +++ b/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.spec.ts @@ -0,0 +1,76 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { Item } from '../../../core/shared/item.model'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { of as observableOf } from 'rxjs'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; +import { By } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { DsoPageOrcidButtonComponent } from './dso-page-orcid-button.component'; + +describe('DsoPageOrcidButtonComponent', () => { + let component: DsoPageOrcidButtonComponent; + let fixture: ComponentFixture; + + let authorizationService: AuthorizationDataService; + let dso: DSpaceObject; + + beforeEach(waitForAsync(() => { + dso = Object.assign(new Item(), { + id: 'test-item', + _links: { + self: { href: 'test-item-selflink' } + } + }); + authorizationService = jasmine.createSpyObj('authorizationService', { + isAuthorized: observableOf(true) + }); + TestBed.configureTestingModule({ + declarations: [DsoPageOrcidButtonComponent], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NgbModule], + providers: [ + { provide: AuthorizationDataService, useValue: authorizationService } + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DsoPageOrcidButtonComponent); + component = fixture.componentInstance; + component.dso = dso; + component.pageRoute = 'test'; + fixture.detectChanges(); + }); + + it('should check the authorization of the current user', () => { + expect(authorizationService.isAuthorized).toHaveBeenCalledWith(FeatureID.CanEditOrcid, dso.self); + }); + + describe('when the user is authorized', () => { + beforeEach(() => { + (authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(true)); + component.ngOnInit(); + fixture.detectChanges(); + }); + + it('should render a link', () => { + const link = fixture.debugElement.query(By.css('a')); + expect(link).not.toBeNull(); + }); + }); + + describe('when the user is not authorized', () => { + beforeEach(() => { + (authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false)); + component.ngOnInit(); + fixture.detectChanges(); + }); + + it('should not render a link', () => { + const link = fixture.debugElement.query(By.css('a')); + expect(link).toBeNull(); + }); + }); +}); diff --git a/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.ts b/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.ts new file mode 100644 index 0000000000..9f244da7c9 --- /dev/null +++ b/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.ts @@ -0,0 +1,43 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { AuthorizationDataService } from 'src/app/core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from 'src/app/core/data/feature-authorization/feature-id'; +import { DSpaceObject } from 'src/app/core/shared/dspace-object.model'; + +@Component({ + selector: 'ds-dso-page-orcid-button', + templateUrl: './dso-page-orcid-button.component.html', + styleUrls: ['./dso-page-orcid-button.component.scss'] +}) +export class DsoPageOrcidButtonComponent implements OnInit { + /** + * The DSpaceObject to display a button to the edit page for + */ + @Input() dso: DSpaceObject; + + /** + * The prefix of the route to the edit page (before the object's UUID, e.g. "items") + */ + @Input() pageRoute: string; + + /** + * A message for the tooltip on the button + * Supports i18n keys + */ + @Input() tooltipMsg: string; + + /** + * Whether or not the current user is authorized to edit the DSpaceObject + */ + isAuthorized: BehaviorSubject = new BehaviorSubject(false); + + constructor(protected authorizationService: AuthorizationDataService) { } + + ngOnInit() { + this.authorizationService.isAuthorized(FeatureID.CanSynchronizeWithORCID, this.dso.self).pipe(take(1)).subscribe((isAuthorized: boolean) => { + this.isAuthorized.next(isAuthorized); + }); + } + +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 01649ee947..fcd80683b7 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -174,6 +174,7 @@ import { DsSelectComponent } from './ds-select/ds-select.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 { ClaimItemSelectorComponent } from './dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component'; +import { DsoPageOrcidButtonComponent } from './dso-page/dso-page-orcid-button/dso-page-orcid-button.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -414,6 +415,7 @@ const SHARED_ITEM_PAGE_COMPONENTS = [ GenericItemPageFieldComponent, MetadataRepresentationListComponent, RelatedItemsComponent, + DsoPageOrcidButtonComponent ]; diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index d0ff85ba51..8ef1135910 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -4208,4 +4208,195 @@ "researcherprofile.success.claim.body" : "Profile claimed with success", "researcherprofile.success.claim.title" : "Success", + + "person.page.orcid": "ORCID", + + "person.page.orcid.create": "Create an ORCID ID", + + "person.page.orcid.granted-authorizations": "Granted authorizations", + + "person.page.orcid.grant-authorizations" : "Grant authorizations", + + "person.page.orcid.link": "Connect to ORCID ID", + + "person.page.orcid.orcid-not-linked-message": "The ORCID iD of this profile ({{ orcid }}) has not yet been connected to an account on the ORCID registry or the connection is expired.", + + "person.page.orcid.unlink": "Disconnect from ORCID", + + "person.page.orcid.unlink.processing": "Processing...", + + "person.page.orcid.missing-authorizations": "Missing authorizations", + + "person.page.orcid.missing-authorizations-message": "The following authorizations are missing:", + + "person.page.orcid.no-missing-authorizations-message": "Great! This box is empty, so you have granted all access rights to use all functions offers by your institution.", + + "person.page.orcid.no-orcid-message": "No ORCID iD associated yet. By clicking on the button below it is possible to link this profile with an ORCID account.", + + "person.page.orcid.profile-preferences": "Profile preferences", + + "person.page.orcid.funding-preferences": "Funding preferences", + + "person.page.orcid.publications-preferences": "Publication preferences", + + "person.page.orcid.remove-orcid-message": "If you need to remove your ORCID, please contact the repository administrator", + + "person.page.orcid.save.preference.changes": "Update settings", + + "person.page.orcid.sync-profile.affiliation" : "Affiliation", + + "person.page.orcid.sync-profile.biographical" : "Biographical data", + + "person.page.orcid.sync-profile.education" : "Education", + + "person.page.orcid.sync-profile.identifiers" : "Identifiers", + + "person.page.orcid.sync-fundings.all" : "All fundings", + + "person.page.orcid.sync-fundings.mine" : "My fundings", + + "person.page.orcid.sync-fundings.my_selected" : "Selected fundings", + + "person.page.orcid.sync-fundings.disabled" : "Disabled", + + "person.page.orcid.sync-publications.all" : "All publications", + + "person.page.orcid.sync-publications.mine" : "My publications", + + "person.page.orcid.sync-publications.my_selected" : "Selected publications", + + "person.page.orcid.sync-publications.disabled" : "Disabled", + + "person.page.orcid.sync-queue.discard" : "Discard the change and do not synchronize with the ORCID registry", + + "person.page.orcid.sync-queue.discard.error": "The discarding of the ORCID queue record failed", + + "person.page.orcid.sync-queue.discard.success": "The ORCID queue record have been discarded successfully", + + "person.page.orcid.sync-queue.empty-message": "The ORCID queue registry is empty", + + "person.page.orcid.sync-queue.description" : "Description", + + "person.page.orcid.sync-queue.description.affiliation": "Affiliations", + + "person.page.orcid.sync-queue.description.country": "Country", + + "person.page.orcid.sync-queue.description.education": "Educations", + + "person.page.orcid.sync-queue.description.external_ids": "External ids", + + "person.page.orcid.sync-queue.description.other_names": "Other names", + + "person.page.orcid.sync-queue.description.qualification": "Qualifications", + + "person.page.orcid.sync-queue.description.researcher_urls": "Researcher urls", + + "person.page.orcid.sync-queue.description.keywords": "Keywords", + + "person.page.orcid.sync-queue.tooltip.insert": "Add a new entry in the ORCID registry", + + "person.page.orcid.sync-queue.tooltip.update": "Update this entry on the ORCID registry", + + "person.page.orcid.sync-queue.tooltip.delete": "Remove this entry from the ORCID registry", + + "person.page.orcid.sync-queue.tooltip.publication": "Publication", + + "person.page.orcid.sync-queue.tooltip.funding": "Funding", + + "person.page.orcid.sync-queue.tooltip.affiliation": "Affiliation", + + "person.page.orcid.sync-queue.tooltip.education": "Education", + + "person.page.orcid.sync-queue.tooltip.qualification": "Qualification", + + "person.page.orcid.sync-queue.tooltip.other_names": "Other name", + + "person.page.orcid.sync-queue.tooltip.country": "Country", + + "person.page.orcid.sync-queue.tooltip.keywords": "Keyword", + + "person.page.orcid.sync-queue.tooltip.external_ids": "External identifier", + + "person.page.orcid.sync-queue.tooltip.researcher_urls": "Researcher url", + + "person.page.orcid.sync-queue.send" : "Synchronize with ORCID registry", + + "person.page.orcid.sync-queue.send.unauthorized-error.title": "The submission to ORCID failed for missing authorizations.", + + "person.page.orcid.sync-queue.send.unauthorized-error.content": "Click here to grant again the required permissions. If the problem persists, contact the administrator", + + "person.page.orcid.sync-queue.send.bad-request-error": "The submission to ORCID failed because the resource sent to ORCID registry is not valid", + + "person.page.orcid.sync-queue.send.error": "The submission to ORCID failed", + + "person.page.orcid.sync-queue.send.conflict-error": "The submission to ORCID failed because the resource is already present on the ORCID registry", + + "person.page.orcid.sync-queue.send.not-found-warning": "The resource does not exists anymore on the ORCID registry.", + + "person.page.orcid.sync-queue.send.success": "The submission to ORCID was completed successfully", + + "person.page.orcid.sync-queue.send.validation-error": "The data that you want to synchronize with ORCID is not valid", + + "person.page.orcid.sync-queue.send.validation-error.amount-currency.required": "The amount's currency is required", + + "person.page.orcid.sync-queue.send.validation-error.external-id.required": "The resource to be sent requires at least one identifier", + + "person.page.orcid.sync-queue.send.validation-error.title.required": "The title is required", + + "person.page.orcid.sync-queue.send.validation-error.type.required": "The type is required", + + "person.page.orcid.sync-queue.send.validation-error.start-date.required": "The start date is required", + + "person.page.orcid.sync-queue.send.validation-error.funder.required": "The funder is required", + + "person.page.orcid.sync-queue.send.validation-error.organization.required": "The organization is required", + + "person.page.orcid.sync-queue.send.validation-error.organization.name-required": "The organization's name is required", + + "person.page.orcid.sync-queue.send.validation-error.organization.address-required": "The organization to be sent requires an address", + + "person.page.orcid.sync-queue.send.validation-error.organization.city-required": "The address of the organization to be sent requires a city", + + "person.page.orcid.sync-queue.send.validation-error.organization.country-required": "The address of the organization to be sent requires a country", + + "person.page.orcid.sync-queue.send.validation-error.disambiguated-organization.required": "An identifier to disambiguate organizations is required. Supported ids are GRID, Ringgold, Legal Entity identifiers (LEIs) and Crossref Funder Registry identifiers", + + "person.page.orcid.sync-queue.send.validation-error.disambiguated-organization.value-required": "The organization's identifiers requires a value", + + "person.page.orcid.sync-queue.send.validation-error.disambiguation-source.required": "The organization's identifiers requires a source", + + "person.page.orcid.sync-queue.send.validation-error.disambiguation-source.invalid": "The source of one of the organization identifiers is invalid. Supported sources are RINGGOLD, GRID, LEI and FUNDREF", + + "person.page.orcid.synchronization-mode": "Synchronization mode", + + "person.page.orcid.synchronization-mode.batch": "Batch", + + "person.page.orcid.synchronization-mode.label": "Synchronization mode", + + "person.page.orcid.synchronization-mode-message": "Enable 'Manual' Synchronization mode to disable batch synchronization, so you must send your data to ORCID Registry manually", + + "person.page.orcid.synchronization-settings-update.success": "The synchronization settings have been updated successfully", + + "person.page.orcid.synchronization-settings-update.error": "The update of the synchronization settings failed", + + "person.page.orcid.synchronization-mode.manual": "Manual", + + "person.page.orcid.scope.authenticate": "Get your ORCID iD", + + "person.page.orcid.scope.read-limited": "Read your information with visibility set to Trusted Parties", + + "person.page.orcid.scope.activities-update": "Add/update your research activities", + + "person.page.orcid.scope.person-update": "Add/update other information about you", + + "person.page.orcid.unlink.success": "The disconnection between the profile and the ORCID registry was successful", + + "person.page.orcid.unlink.error": "An error occurred while disconnecting between the profile and the ORCID registry. Try again", + + "person.orcid.sync.setting": "ORCID Synchronization settings", + + "person.orcid.registry.queue": "ORCID Registry Queue", + + "person.orcid.registry.auth": "ORCID Authorizations", + } diff --git a/src/assets/images/orcid.logo.icon.svg b/src/assets/images/orcid.logo.icon.svg new file mode 100644 index 0000000000..8aec5959e5 --- /dev/null +++ b/src/assets/images/orcid.logo.icon.svg @@ -0,0 +1,21 @@ + + + + Orcid logo + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/styles/_global-styles.scss b/src/styles/_global-styles.scss index cf251204e2..7d9550c0fd 100644 --- a/src/styles/_global-styles.scss +++ b/src/styles/_global-styles.scss @@ -97,4 +97,15 @@ ngb-modal-backdrop { } .researcher-profile-switch .switch.checked{ color: #fff; +} + +.custom-accordion .card-header button { + -webkit-box-shadow: none!important; + box-shadow: none!important; + width: 100%; +} +.custom-accordion .card:first-of-type { + border-bottom: var(--bs-card-border-width) solid var(--bs-card-border-color)!important; + border-bottom-left-radius: var(--bs-card-border-radius)!important; + border-bottom-right-radius: var(--bs-card-border-radius)!important; } \ No newline at end of file From dd4ff5e40c456da430b90d1c868fe5ce8adc81b6 Mon Sep 17 00:00:00 2001 From: Pratik Rajkotiya Date: Wed, 13 Apr 2022 17:54:29 +0530 Subject: [PATCH 03/29] [CST-5338] ORCID Settings added. --- src/app/item-page/item-page.module.ts | 4 +- .../orcid-page/orcid-page.component.html | 1 + .../orcid-sync/orcid-setting.component.html | 82 +++++++++++ .../orcid-sync/orcid-setting.component.scss | 0 .../orcid-sync/orcid-setting.component.ts | 139 ++++++++++++++++++ 5 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.html create mode 100644 src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.scss create mode 100644 src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts diff --git a/src/app/item-page/item-page.module.ts b/src/app/item-page/item-page.module.ts index f584164c97..2c4b57b249 100644 --- a/src/app/item-page/item-page.module.ts +++ b/src/app/item-page/item-page.module.ts @@ -37,6 +37,7 @@ import { ThemedFileSectionComponent } from './simple/field-components/file-secti import { OrcidAuthComponent } from './orcid-page/orcid-auth/orcid-auth.component'; import { OrcidPageComponent } from './orcid-page/orcid-page.component'; import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; +import { OrcidSettingComponent } from './orcid-page/orcid-sync/orcid-setting.component'; const ENTRY_COMPONENTS = [ @@ -71,7 +72,8 @@ const DECLARATIONS = [ MiradorViewerComponent, VersionPageComponent, OrcidPageComponent, - OrcidAuthComponent + OrcidAuthComponent, + OrcidSettingComponent ]; @NgModule({ diff --git a/src/app/item-page/orcid-page/orcid-page.component.html b/src/app/item-page/orcid-page/orcid-page.component.html index ba9f445ec2..4e62a8d51c 100644 --- a/src/app/item-page/orcid-page/orcid-page.component.html +++ b/src/app/item-page/orcid-page/orcid-page.component.html @@ -1 +1,2 @@ + diff --git a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.html b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.html new file mode 100644 index 0000000000..793e7570ed --- /dev/null +++ b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.html @@ -0,0 +1,82 @@ +
+ + + +
+
+
+ {{ 'person.page.orcid.synchronization-mode-message' | translate}} +
+
+
+
{{ 'person.page.orcid.synchronization-mode'| translate }}
+
+
+
+ + +
+
+
+
+
+
+
+
{{ 'person.page.orcid.publications-preferences'| translate }}
+
+
+
+
+ + +
+
+
+
+
+
+
{{ 'person.page.orcid.funding-preferences'| translate }}
+
+
+
+
+ + +
+
+
+
+
+
+
{{ 'person.page.orcid.profile-preferences'| translate }}
+
+
+
+
+ + +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.scss b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts new file mode 100644 index 0000000000..87385f0780 --- /dev/null +++ b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts @@ -0,0 +1,139 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { Operation } from 'fast-json-patch'; +import { switchMap } from 'rxjs/operators'; +import { AuthService } from '../../../core/auth/auth.service'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { RemoteData } from '../../../core/data/remote-data'; +import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service'; +import { Item } from '../../../core/shared/item.model'; +import { getFinishedRemoteData, getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; + +@Component({ + selector: 'ds-orcid-setting', + templateUrl: './orcid-setting.component.html', + styleUrls: ['./orcid-setting.component.scss'] +}) +export class OrcidSettingComponent implements OnInit { + + messagePrefix = 'person.page.orcid'; + + currentSyncMode: string; + + currentSyncPublications: string; + + currentSyncFundings: string; + + syncModes: { value: string, label: string }[]; + + syncPublicationOptions: { value: string, label: string }[]; + + syncFundingOptions: {value: string, label: string}[]; + + syncProfileOptions: { value: string, label: string, checked: boolean }[]; + + item: Item; + + constructor(private researcherProfileService: ResearcherProfileService, + protected translateService: TranslateService, + private notificationsService: NotificationsService, + public authService: AuthService, + private route: ActivatedRoute, + private itemService: ItemDataService + ) { + this.itemService.findById(this.route.snapshot.paramMap.get('id'), true, true).pipe(getFirstCompletedRemoteData()).subscribe((data: RemoteData) => { + this.item = data.payload; + }); + } + + ngOnInit() { + this.syncModes = [ + { + label: this.messagePrefix + '.synchronization-mode.batch', + value: 'BATCH' + }, + { + label: this.messagePrefix + '.synchronization-mode.manual', + value: 'MANUAL' + } + ]; + + this.syncPublicationOptions = ['DISABLED', 'ALL'] + .map((value) => { + return { + label: this.messagePrefix + '.sync-publications.' + value.toLowerCase(), + value: value, + }; + }); + + this.syncFundingOptions = ['DISABLED', 'ALL'] + .map((value) => { + return { + label: this.messagePrefix + '.sync-fundings.' + value.toLowerCase(), + value: value, + }; + }); + + const syncProfilePreferences = this.item.allMetadataValues('cris.orcid.sync-profile'); + + this.syncProfileOptions = ['AFFILIATION', 'EDUCATION', 'BIOGRAPHICAL', 'IDENTIFIERS'] + .map((value) => { + return { + label: this.messagePrefix + '.sync-profile.' + value.toLowerCase(), + value: value, + checked: syncProfilePreferences.includes(value) + }; + }); + + this.currentSyncMode = this.getCurrentPreference('cris.orcid.sync-mode', ['BATCH, MANUAL'], 'MANUAL'); + this.currentSyncPublications = this.getCurrentPreference('cris.orcid.sync-publications', ['DISABLED', 'ALL'], 'DISABLED'); + this.currentSyncFundings = this.getCurrentPreference('cris.orcid.sync-fundings', ['DISABLED', 'ALL'], 'DISABLED'); + } + + onSubmit(form: FormGroup) { + const operations: Operation[] = []; + this.fillOperationsFor(operations, '/orcid/mode', form.value.syncMode); + this.fillOperationsFor(operations, '/orcid/publications', form.value.syncPublications); + this.fillOperationsFor(operations, '/orcid/fundings', form.value.syncFundings); + + const syncProfileValue = this.syncProfileOptions + .map((syncProfileOption => syncProfileOption.value)) + .filter((value) => form.value['syncProfile_' + value]) + .join(','); + + this.fillOperationsFor(operations, '/orcid/profile', syncProfileValue); + + if (operations.length === 0) { + return; + } + + this.researcherProfileService.findById(this.item.firstMetadata('cris.owner').authority).pipe( + switchMap((profile) => this.researcherProfileService.patch(profile, operations)), + getFinishedRemoteData() + ).subscribe((remoteData) => { + if (remoteData.isSuccess) { + this.notificationsService.success(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.success')); + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.error')); + } + }); + } + + fillOperationsFor(operations: Operation[], path: string, currentValue: string) { + operations.push({ + path: path, + op: 'replace', + value: currentValue + }); + } + + getCurrentPreference(metadataField: string, allowedValues: string[], defaultValue: string): string { + const currentPreference = this.item.firstMetadataValue(metadataField); + return (currentPreference && allowedValues.includes(currentPreference)) ? currentPreference : defaultValue; + } + + +} From da1fb5506b4edec2fac0c04eff624ada4e86de0f Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Fri, 6 May 2022 16:50:22 +0200 Subject: [PATCH 04/29] [CST-5668] Fixed orcid page --- .../profile/researcher-profile.service.ts | 6 ++-- .../orcid-auth/orcid-auth.component.ts | 6 ++-- .../orcid-page/orcid-page.component.html | 2 +- .../orcid-page/orcid-page.component.ts | 30 ++++++++++++++++++- .../orcid-sync/orcid-setting.component.ts | 12 ++++---- 5 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/app/core/profile/researcher-profile.service.ts b/src/app/core/profile/researcher-profile.service.ts index 100fa459a1..b7035a921f 100644 --- a/src/app/core/profile/researcher-profile.service.ts +++ b/src/app/core/profile/researcher-profile.service.ts @@ -162,7 +162,7 @@ export class ResearcherProfileService { * @returns the check result */ isLinkedToOrcid(item: Item): boolean { - return item.hasMetadata('cris.orcid.authenticated'); + return item.hasMetadata('dspace.orcid.authenticated'); } /** @@ -214,7 +214,7 @@ export class ResearcherProfileService { op:'remove' }]; - return this.findById(item.firstMetadata('cris.owner').authority).pipe( + return this.findById(item.firstMetadata('dspace.object.owner').authority).pipe( switchMap((profile) => this.patch(profile, operations)), getFinishedRemoteData() ); @@ -227,7 +227,7 @@ export class ResearcherProfileService { 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); + const redirectUri = environment.rest.baseUrl + '/api/eperson/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(' '); })); diff --git a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts index 9651215dc6..8594cba9f5 100644 --- a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts +++ b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts @@ -33,7 +33,7 @@ export class OrcidAuthComponent implements OnInit { private itemService: ItemDataService, private route: ActivatedRoute, @Inject(NativeWindowService) private _window: NativeWindowRef, - ) { + ) { this.itemService.findById(this.route.snapshot.paramMap.get('id'), true, true).pipe(getFirstCompletedRemoteData()).subscribe((data: RemoteData) => { this.item = data.payload; }); @@ -49,10 +49,10 @@ export class OrcidAuthComponent implements OnInit { } getOrcidAuthorizations(): string[] { - return this.item.allMetadataValues('cris.orcid.scope'); + return this.item.allMetadataValues('dspace.orcid.scope'); } - isLinkedToOrcid(): boolean { + isLinkedToOrcid(): boolean { return this.researcherProfileService.isLinkedToOrcid(this.item); } diff --git a/src/app/item-page/orcid-page/orcid-page.component.html b/src/app/item-page/orcid-page/orcid-page.component.html index 4e62a8d51c..e0cbfe95e2 100644 --- a/src/app/item-page/orcid-page/orcid-page.component.html +++ b/src/app/item-page/orcid-page/orcid-page.component.html @@ -1,2 +1,2 @@ - + diff --git a/src/app/item-page/orcid-page/orcid-page.component.ts b/src/app/item-page/orcid-page/orcid-page.component.ts index 32b47e069f..eed5ebc276 100644 --- a/src/app/item-page/orcid-page/orcid-page.component.ts +++ b/src/app/item-page/orcid-page/orcid-page.component.ts @@ -1,4 +1,14 @@ -import { Component } from '@angular/core'; +import {Component, Inject} from '@angular/core'; +import {ConfigurationDataService} from '../../core/data/configuration-data.service'; +import {ResearcherProfileService} from '../../core/profile/researcher-profile.service'; +import {TranslateService} from '@ngx-translate/core'; +import {NotificationsService} from '../../shared/notifications/notifications.service'; +import {ItemDataService} from '../../core/data/item-data.service'; +import {ActivatedRoute} from '@angular/router'; +import {NativeWindowRef, NativeWindowService} from '../../core/services/window.service'; +import {getFirstCompletedRemoteData} from '../../core/shared/operators'; +import {RemoteData} from '../../core/data/remote-data'; +import {Item} from '../../core/shared/item.model'; @Component({ selector: 'ds-orcid-page', @@ -6,4 +16,22 @@ import { Component } from '@angular/core'; styleUrls: ['./orcid-page.component.scss'] }) export class OrcidPageComponent { + + item: Item; + + constructor( + private itemService: ItemDataService, + private researcherProfileService: ResearcherProfileService, + private route: ActivatedRoute, + @Inject(NativeWindowService) private _window: NativeWindowRef, + ) { + this.itemService.findById(this.route.snapshot.paramMap.get('id'), true, true).pipe(getFirstCompletedRemoteData()).subscribe((data: RemoteData) => { + this.item = data.payload; + }); + } + + isLinkedToOrcid(): boolean { + return this.researcherProfileService.isLinkedToOrcid(this.item); + } + } diff --git a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts index 87385f0780..45ec48e788 100644 --- a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts +++ b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts @@ -77,9 +77,9 @@ export class OrcidSettingComponent implements OnInit { }; }); - const syncProfilePreferences = this.item.allMetadataValues('cris.orcid.sync-profile'); + const syncProfilePreferences = this.item.allMetadataValues('dspace.orcid.sync-profile'); - this.syncProfileOptions = ['AFFILIATION', 'EDUCATION', 'BIOGRAPHICAL', 'IDENTIFIERS'] + this.syncProfileOptions = ['BIOGRAPHICAL', 'IDENTIFIERS'] .map((value) => { return { label: this.messagePrefix + '.sync-profile.' + value.toLowerCase(), @@ -88,9 +88,9 @@ export class OrcidSettingComponent implements OnInit { }; }); - this.currentSyncMode = this.getCurrentPreference('cris.orcid.sync-mode', ['BATCH, MANUAL'], 'MANUAL'); - this.currentSyncPublications = this.getCurrentPreference('cris.orcid.sync-publications', ['DISABLED', 'ALL'], 'DISABLED'); - this.currentSyncFundings = this.getCurrentPreference('cris.orcid.sync-fundings', ['DISABLED', 'ALL'], 'DISABLED'); + this.currentSyncMode = this.getCurrentPreference('dspace.orcid.sync-mode', ['BATCH, MANUAL'], 'MANUAL'); + this.currentSyncPublications = this.getCurrentPreference('dspace.orcid.sync-publications', ['DISABLED', 'ALL'], 'DISABLED'); + this.currentSyncFundings = this.getCurrentPreference('dspace.orcid.sync-fundings', ['DISABLED', 'ALL'], 'DISABLED'); } onSubmit(form: FormGroup) { @@ -110,7 +110,7 @@ export class OrcidSettingComponent implements OnInit { return; } - this.researcherProfileService.findById(this.item.firstMetadata('cris.owner').authority).pipe( + this.researcherProfileService.findById(this.item.firstMetadata('dspace.object.owner').authority).pipe( switchMap((profile) => this.researcherProfileService.patch(profile, operations)), getFinishedRemoteData() ).subscribe((remoteData) => { From 06091e39ca8f970e7166a6266ecf95ee0c39517f Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Fri, 20 May 2022 17:16:28 +0200 Subject: [PATCH 05/29] [CST-5668] Improved orcid page adding back button and fixing synchronization mode setting --- .../item-pages/person/person.component.html | 2 +- src/app/item-page/orcid-page/orcid-page.component.html | 8 ++++++++ src/app/item-page/orcid-page/orcid-page.component.ts | 5 +++++ .../orcid-page/orcid-sync/orcid-setting.component.ts | 2 +- src/assets/i18n/en.json5 | 1 + 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/app/entity-groups/research-entities/item-pages/person/person.component.html b/src/app/entity-groups/research-entities/item-pages/person/person.component.html index d8325d8077..bcff32c553 100644 --- a/src/app/entity-groups/research-entities/item-pages/person/person.component.html +++ b/src/app/entity-groups/research-entities/item-pages/person/person.component.html @@ -3,7 +3,7 @@ {{'person.page.titleprefix' | translate}}
- +
diff --git a/src/app/item-page/orcid-page/orcid-page.component.html b/src/app/item-page/orcid-page/orcid-page.component.html index e0cbfe95e2..296d5468c5 100644 --- a/src/app/item-page/orcid-page/orcid-page.component.html +++ b/src/app/item-page/orcid-page/orcid-page.component.html @@ -1,2 +1,10 @@ + + diff --git a/src/app/item-page/orcid-page/orcid-page.component.ts b/src/app/item-page/orcid-page/orcid-page.component.ts index eed5ebc276..506b5c7e49 100644 --- a/src/app/item-page/orcid-page/orcid-page.component.ts +++ b/src/app/item-page/orcid-page/orcid-page.component.ts @@ -9,6 +9,7 @@ import {NativeWindowRef, NativeWindowService} from '../../core/services/window.s import {getFirstCompletedRemoteData} from '../../core/shared/operators'; import {RemoteData} from '../../core/data/remote-data'; import {Item} from '../../core/shared/item.model'; +import {getItemPageRoute} from '../item-page-routing-paths'; @Component({ selector: 'ds-orcid-page', @@ -34,4 +35,8 @@ export class OrcidPageComponent { return this.researcherProfileService.isLinkedToOrcid(this.item); } + getItemPage(): string { + return getItemPageRoute(this.item); + } + } diff --git a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts index 45ec48e788..00a6018892 100644 --- a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts +++ b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts @@ -88,7 +88,7 @@ export class OrcidSettingComponent implements OnInit { }; }); - this.currentSyncMode = this.getCurrentPreference('dspace.orcid.sync-mode', ['BATCH, MANUAL'], 'MANUAL'); + this.currentSyncMode = this.getCurrentPreference('dspace.orcid.sync-mode', ['BATCH', 'MANUAL'], 'MANUAL'); this.currentSyncPublications = this.getCurrentPreference('dspace.orcid.sync-publications', ['DISABLED', 'ALL'], 'DISABLED'); this.currentSyncFundings = this.getCurrentPreference('dspace.orcid.sync-fundings', ['DISABLED', 'ALL'], 'DISABLED'); } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 0f240899cd..79714d78a6 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2067,6 +2067,7 @@ "item.edit.withdraw.success": "The item was withdrawn successfully", + "item.orcid.return": "Back", "item.listelement.badge": "Item", From 857a3c56b749e93b596f9ab9125e9ad506b5dd9f Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Fri, 20 May 2022 17:31:54 +0200 Subject: [PATCH 06/29] [CST-5668] Fixed lint warnings --- src/app/item-page/item-page-routing-paths.ts | 2 +- src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts | 2 +- src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/item-page/item-page-routing-paths.ts b/src/app/item-page/item-page-routing-paths.ts index 9da2f91431..90a4a54b1e 100644 --- a/src/app/item-page/item-page-routing-paths.ts +++ b/src/app/item-page/item-page-routing-paths.ts @@ -50,4 +50,4 @@ export const ITEM_EDIT_PATH = 'edit'; export const ITEM_EDIT_VERSIONHISTORY_PATH = 'versionhistory'; export const ITEM_VERSION_PATH = 'version'; export const UPLOAD_BITSTREAM_PATH = 'bitstreams/new'; -export const ORCID_PATH = 'orcid'; \ No newline at end of file +export const ORCID_PATH = 'orcid'; diff --git a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts index 8594cba9f5..0f368ada2f 100644 --- a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts +++ b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts @@ -23,7 +23,7 @@ export class OrcidAuthComponent implements OnInit { unlinkProcessing = false; - item: Item + item: Item; constructor( private configurationService: ConfigurationDataService, diff --git a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts b/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts index 0bc9e18520..2181f3db20 100644 --- a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts +++ b/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts @@ -14,7 +14,7 @@ import { AuthService } from '../../../../core/auth/auth.service'; import { HardRedirectService } from '../../../../core/services/hard-redirect.service'; import { take } from 'rxjs/operators'; import { URLCombiner } from '../../../../core/url-combiner/url-combiner'; -import {CoreState} from "../../../../core/core-state.model"; +import {CoreState} from '../../../../core/core-state.model'; @Component({ selector: 'ds-log-in-orcid', From 7320d1cc66dd85b4771798c9af0a3b63a38c16cf Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Tue, 7 Jun 2022 14:59:04 +0200 Subject: [PATCH 07/29] [CST-5668] Removed unused imports --- src/app/item-page/orcid-page/orcid-page.component.ts | 3 --- src/app/item-page/simple/item-page.component.ts | 11 +++-------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/app/item-page/orcid-page/orcid-page.component.ts b/src/app/item-page/orcid-page/orcid-page.component.ts index 506b5c7e49..3c464596b1 100644 --- a/src/app/item-page/orcid-page/orcid-page.component.ts +++ b/src/app/item-page/orcid-page/orcid-page.component.ts @@ -1,8 +1,5 @@ import {Component, Inject} from '@angular/core'; -import {ConfigurationDataService} from '../../core/data/configuration-data.service'; import {ResearcherProfileService} from '../../core/profile/researcher-profile.service'; -import {TranslateService} from '@ngx-translate/core'; -import {NotificationsService} from '../../shared/notifications/notifications.service'; import {ItemDataService} from '../../core/data/item-data.service'; import {ActivatedRoute} from '@angular/router'; import {NativeWindowRef, NativeWindowService} from '../../core/services/window.service'; diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index b3660eb9d7..e75ea9a843 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -1,26 +1,21 @@ -import { map, mergeMap, take, tap } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { Observable } from 'rxjs'; import { ItemDataService } from '../../core/data/item-data.service'; import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; import { fadeInOut } from '../../shared/animations/fade'; -import { getAllSucceededRemoteDataPayload, getFirstSucceededRemoteData } from '../../core/shared/operators'; +import { getAllSucceededRemoteDataPayload } from '../../core/shared/operators'; import { ViewMode } from '../../core/shared/view-mode.model'; import { AuthService } from '../../core/auth/auth.service'; import { getItemPageRoute } from '../item-page-routing-paths'; import { redirectOn4xx } from '../../core/shared/authorized.operators'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; -import { TranslateService } from '@ngx-translate/core'; -import { ResearcherProfileService } from '../../core/profile/researcher-profile.service'; -import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model'; -import { isNotUndefined } from '../../shared/empty.util'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; /** From 43f4ff7cdeea6d797e5e98d48e3976a25b38e601 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Tue, 7 Jun 2022 15:15:51 +0200 Subject: [PATCH 08/29] [CST-5668] Fixed DsoPageOrcidButtonComponent test --- .../dso-page-orcid-button.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.spec.ts b/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.spec.ts index 9de3333b7f..c70ec4b808 100644 --- a/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.spec.ts +++ b/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.spec.ts @@ -45,7 +45,7 @@ describe('DsoPageOrcidButtonComponent', () => { }); it('should check the authorization of the current user', () => { - expect(authorizationService.isAuthorized).toHaveBeenCalledWith(FeatureID.CanEditOrcid, dso.self); + expect(authorizationService.isAuthorized).toHaveBeenCalledWith(FeatureID.CanSynchronizeWithORCID, dso.self); }); describe('when the user is authorized', () => { From a9fcdce9605d3f4f7ce5c55795ca570adcc9b495 Mon Sep 17 00:00:00 2001 From: nikunj59 Date: Tue, 7 Jun 2022 15:34:47 +0530 Subject: [PATCH 09/29] CST-5309 changes for error page component --- src/app/app-routing-paths.ts | 2 ++ src/app/app-routing.module.ts | 3 +++ src/app/page-error/page-error.component.html | 10 +++++++ src/app/page-error/page-error.component.scss | 0 src/app/page-error/page-error.component.ts | 27 +++++++++++++++++++ .../page-error/themed-page-error.component.ts | 26 ++++++++++++++++++ src/app/root.module.ts | 6 ++++- src/assets/i18n/en.json5 | 10 +++++++ 8 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 src/app/page-error/page-error.component.html create mode 100644 src/app/page-error/page-error.component.scss create mode 100644 src/app/page-error/page-error.component.ts create mode 100644 src/app/page-error/themed-page-error.component.ts diff --git a/src/app/app-routing-paths.ts b/src/app/app-routing-paths.ts index 6524edef77..866957f70d 100644 --- a/src/app/app-routing-paths.ts +++ b/src/app/app-routing-paths.ts @@ -107,6 +107,8 @@ export function getPageInternalServerErrorRoute() { return `/${INTERNAL_SERVER_ERROR}`; } +export const ERROR_PAGE = 'error' + export const INFO_MODULE_PATH = 'info'; export function getInfoModulePath() { return `/${INFO_MODULE_PATH}`; diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index f0869d9fb6..1815e0ec60 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -8,6 +8,7 @@ import { ACCESS_CONTROL_MODULE_PATH, ADMIN_MODULE_PATH, BITSTREAM_MODULE_PATH, + ERROR_PAGE, FORBIDDEN_PATH, FORGOT_PASSWORD_PATH, INFO_MODULE_PATH, @@ -31,11 +32,13 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component'; import { ServerCheckGuard } from './core/server-check/server-check.guard'; import { MenuResolver } from './menu.resolver'; +import { ThemedPageErrorComponent } from './page-error/themed-page-error.component'; @NgModule({ imports: [ RouterModule.forRoot([ { path: INTERNAL_SERVER_ERROR, component: ThemedPageInternalServerErrorComponent }, + { path: ERROR_PAGE , component: ThemedPageErrorComponent }, { path: '', canActivate: [AuthBlockingGuard], diff --git a/src/app/page-error/page-error.component.html b/src/app/page-error/page-error.component.html new file mode 100644 index 0000000000..d0571e420d --- /dev/null +++ b/src/app/page-error/page-error.component.html @@ -0,0 +1,10 @@ +
+

{{status}}

+

{{"error-page.description." + status | translate}}

+
+

{{"error-page." + code | translate}}

+
+

+ {{ status + ".link.home-page" | translate}} +

+
diff --git a/src/app/page-error/page-error.component.scss b/src/app/page-error/page-error.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/page-error/page-error.component.ts b/src/app/page-error/page-error.component.ts new file mode 100644 index 0000000000..18f0b34c47 --- /dev/null +++ b/src/app/page-error/page-error.component.ts @@ -0,0 +1,27 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +/** + * This component representing the `PageError` DSpace page. + */ +@Component({ + selector: 'ds-page-error', + styleUrls: ['./page-error.component.scss'], + templateUrl: './page-error.component.html', + changeDetection: ChangeDetectionStrategy.Default +}) +export class PageErrorComponent { + status: number; + code: string; + /** + * Initialize instance variables + * + * @param {ActivatedRoute} activatedRoute + */ + constructor(private activatedRoute: ActivatedRoute) { + this.activatedRoute.queryParams.subscribe((params) => { + this.status = params['status']; + this.code = params['code']; + }); + } +} diff --git a/src/app/page-error/themed-page-error.component.ts b/src/app/page-error/themed-page-error.component.ts new file mode 100644 index 0000000000..34d29fb2a9 --- /dev/null +++ b/src/app/page-error/themed-page-error.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { ThemedComponent } from '../shared/theme-support/themed.component'; +import { PageErrorComponent } from './page-error.component'; + +/** + * Themed wrapper for PageErrorComponent + */ +@Component({ + selector: 'ds-themed-search-page', + styleUrls: [], + templateUrl: '../shared/theme-support/themed.component.html', +}) +export class ThemedPageErrorComponent extends ThemedComponent { + + protected getComponentName(): string { + return 'PageErrorComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../themes/${themeName}/app/page-error/page-error.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`src/app/page-error/page-error.component`); + } +} diff --git a/src/app/root.module.ts b/src/app/root.module.ts index e5a8aad949..8577f0d728 100644 --- a/src/app/root.module.ts +++ b/src/app/root.module.ts @@ -40,6 +40,8 @@ import { import { PageInternalServerErrorComponent } from './page-internal-server-error/page-internal-server-error.component'; +import { ThemedPageErrorComponent } from './page-error/themed-page-error.component'; +import { PageErrorComponent } from './page-error/page-error.component'; const IMPORTS = [ CommonModule, @@ -74,7 +76,9 @@ const DECLARATIONS = [ ThemedForbiddenComponent, IdleModalComponent, ThemedPageInternalServerErrorComponent, - PageInternalServerErrorComponent + PageInternalServerErrorComponent, + ThemedPageErrorComponent, + PageErrorComponent ]; const EXPORTS = [ diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index c81ab66516..7ea671a0bc 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -27,6 +27,16 @@ "404.page-not-found": "page not found", + "error-page.description.401": "unauthorized", + + "error-page.description.403": "forbidden", + + "error-page.description.500": "Service Unavailable", + + "error-page.description.404": "page not found", + + "error-page.orcid.generic-error": "An error occurred during login via ORCID. Make sure you have shared your ORCID account email address with DSpace. If the error persists, contact the administrator", + "access-status.embargo.listelement.badge": "Embargo", "access-status.metadata.only.listelement.badge": "Metadata only", From 1e9e4d5b12c4136adafe68b08dc7b066c1342acd Mon Sep 17 00:00:00 2001 From: nikunj59 Date: Tue, 7 Jun 2022 17:42:36 +0530 Subject: [PATCH 10/29] CST-5309 added test cases --- src/app/page-error/page-error.component.html | 2 +- .../page-error/page-error.component.spec.ts | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/app/page-error/page-error.component.spec.ts diff --git a/src/app/page-error/page-error.component.html b/src/app/page-error/page-error.component.html index d0571e420d..9a5f02600a 100644 --- a/src/app/page-error/page-error.component.html +++ b/src/app/page-error/page-error.component.html @@ -1,5 +1,5 @@
-

{{status}}

+

{{status}}

{{"error-page.description." + status | translate}}


{{"error-page." + code | translate}}

diff --git a/src/app/page-error/page-error.component.spec.ts b/src/app/page-error/page-error.component.spec.ts new file mode 100644 index 0000000000..0f876f3196 --- /dev/null +++ b/src/app/page-error/page-error.component.spec.ts @@ -0,0 +1,48 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { PageErrorComponent } from './page-error.component'; +import { ActivatedRoute } from '@angular/router'; +import { ActivatedRouteStub } from '../shared/testing/active-router.stub'; +import { of as observableOf } from 'rxjs'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { By } from '@angular/platform-browser'; +import { TranslateLoaderMock } from '../shared/testing/translate-loader.mock'; + +describe('PageErrorComponent', () => { + let component: PageErrorComponent; + let fixture: ComponentFixture; + const activatedRouteStub = Object.assign(new ActivatedRouteStub(), { + queryParams: observableOf({ + status: 401, + code: 'orcid.generic-error' + }) + }); + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ PageErrorComponent ], + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) + ], + providers: [ + { provide: ActivatedRoute, useValue: activatedRouteStub }, + ] + }).compileComponents(); + + fixture = TestBed.createComponent(PageErrorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should show error for 401 unauthorized', () => { + const statusElement = fixture.debugElement.query(By.css('[data-test="status"]')).nativeElement; + expect(statusElement.innerHTML).toEqual('401'); + }); +}); From 7ea208e314d2510e1c96506f0b066c5cb10c3e6c Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Tue, 7 Jun 2022 17:47:20 +0200 Subject: [PATCH 11/29] [CST-5668] Fixed lint warnings --- src/app/app-routing-paths.ts | 2 +- src/app/page-error/page-error.component.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/app-routing-paths.ts b/src/app/app-routing-paths.ts index 866957f70d..b230bdc1ac 100644 --- a/src/app/app-routing-paths.ts +++ b/src/app/app-routing-paths.ts @@ -107,7 +107,7 @@ export function getPageInternalServerErrorRoute() { return `/${INTERNAL_SERVER_ERROR}`; } -export const ERROR_PAGE = 'error' +export const ERROR_PAGE = 'error'; export const INFO_MODULE_PATH = 'info'; export function getInfoModulePath() { diff --git a/src/app/page-error/page-error.component.ts b/src/app/page-error/page-error.component.ts index 18f0b34c47..dea1b68407 100644 --- a/src/app/page-error/page-error.component.ts +++ b/src/app/page-error/page-error.component.ts @@ -20,8 +20,8 @@ export class PageErrorComponent { */ constructor(private activatedRoute: ActivatedRoute) { this.activatedRoute.queryParams.subscribe((params) => { - this.status = params['status']; - this.code = params['code']; + this.status = params.status; + this.code = params.code; }); } } From c3ececdde714ab880b1918d41adb718c74d1fc8a Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 9 Jun 2022 19:22:23 +0200 Subject: [PATCH 12/29] [CST-5668] Retrieve item from route data --- .../orcid-auth/orcid-auth.component.html | 140 +++++++++--------- .../orcid-auth/orcid-auth.component.ts | 12 +- .../orcid-page/orcid-page.component.html | 4 +- .../orcid-page/orcid-page.component.ts | 55 +++++-- .../orcid-sync/orcid-setting.component.ts | 40 +++-- 5 files changed, 146 insertions(+), 105 deletions(-) diff --git a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html index 8e538b66aa..12b13c433c 100644 --- a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html +++ b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html @@ -1,71 +1,77 @@
- - - -
- -
-
-
-
- - -
-
-
{{ 'person.page.orcid.granted-authorizations'| translate }}
-
-
-
    -
  • {{getAuthorizationDescription(auth) | translate}}
  • -
+ + + +
+ +
+
+
+
+ + +
+
+
{{ 'person.page.orcid.granted-authorizations'| translate }}
+
+
+
    +
  • {{getAuthorizationDescription(auth) | translate}}
  • +
+
+
+
+
+
{{ 'person.page.orcid.missing-authorizations'| translate }}
+
+
+
+ {{'person.page.orcid.no-missing-authorizations-message' | translate}}
+
+ {{'person.page.orcid.missing-authorizations-message' | translate}} +
    +
  • {{getAuthorizationDescription(auth) | translate }}
  • +
+
-
-
{{ 'person.page.orcid.missing-authorizations'| translate }}
-
-
-
- {{'person.page.orcid.no-missing-authorizations-message' | translate}} -
-
- {{'person.page.orcid.missing-authorizations-message' | translate}} -
    -
  • {{getAuthorizationDescription(auth) | translate }}
  • -
-
-
-
-
-
-
- {{ 'person.page.orcid.remove-orcid-message' | translate}} -
-
-
- - -
-
- - - -
-
orcid-logo
-
{{ getOrcidNotLinkedMessage() | async }}
-
-
-
- -
-
-
-
\ No newline at end of file +
+
+
+ {{ 'person.page.orcid.remove-orcid-message' | translate}} +
+
+
+ + +
+
+ + + +
+
orcid-logo
+
{{ getOrcidNotLinkedMessage() | async }}
+
+
+
+ +
+
+
+
diff --git a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts index 0f368ada2f..aa234648f0 100644 --- a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts +++ b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, OnInit } from '@angular/core'; +import { Component, Inject, Input, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, Observable } from 'rxjs'; @@ -19,12 +19,15 @@ import { NotificationsService } from '../../../shared/notifications/notification }) export class OrcidAuthComponent implements OnInit { + /** + * The item for which showing the orcid settings + */ + @Input() item: Item; + missingAuthorizations$ = new BehaviorSubject([]); unlinkProcessing = false; - item: Item; - constructor( private configurationService: ConfigurationDataService, private researcherProfileService: ResearcherProfileService, @@ -34,9 +37,6 @@ export class OrcidAuthComponent implements OnInit { private route: ActivatedRoute, @Inject(NativeWindowService) private _window: NativeWindowRef, ) { - this.itemService.findById(this.route.snapshot.paramMap.get('id'), true, true).pipe(getFirstCompletedRemoteData()).subscribe((data: RemoteData) => { - this.item = data.payload; - }); } ngOnInit() { diff --git a/src/app/item-page/orcid-page/orcid-page.component.html b/src/app/item-page/orcid-page/orcid-page.component.html index 296d5468c5..c105ba6127 100644 --- a/src/app/item-page/orcid-page/orcid-page.component.html +++ b/src/app/item-page/orcid-page/orcid-page.component.html @@ -6,5 +6,5 @@
- - + + diff --git a/src/app/item-page/orcid-page/orcid-page.component.ts b/src/app/item-page/orcid-page/orcid-page.component.ts index 3c464596b1..122c199f61 100644 --- a/src/app/item-page/orcid-page/orcid-page.component.ts +++ b/src/app/item-page/orcid-page/orcid-page.component.ts @@ -1,37 +1,64 @@ -import {Component, Inject} from '@angular/core'; -import {ResearcherProfileService} from '../../core/profile/researcher-profile.service'; -import {ItemDataService} from '../../core/data/item-data.service'; -import {ActivatedRoute} from '@angular/router'; -import {NativeWindowRef, NativeWindowService} from '../../core/services/window.service'; -import {getFirstCompletedRemoteData} from '../../core/shared/operators'; -import {RemoteData} from '../../core/data/remote-data'; -import {Item} from '../../core/shared/item.model'; -import {getItemPageRoute} from '../item-page-routing-paths'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { map } from 'rxjs/operators'; + +import { ResearcherProfileService } from '../../core/profile/researcher-profile.service'; +import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; +import { RemoteData } from '../../core/data/remote-data'; +import { Item } from '../../core/shared/item.model'; +import { getItemPageRoute } from '../item-page-routing-paths'; +import { AuthService } from '../../core/auth/auth.service'; +import { redirectOn4xx } from '../../core/shared/authorized.operators'; + +/** + * A component that represents the orcid settings page + */ @Component({ selector: 'ds-orcid-page', templateUrl: './orcid-page.component.html', styleUrls: ['./orcid-page.component.scss'] }) -export class OrcidPageComponent { +export class OrcidPageComponent implements OnInit { + /** + * The item for which showing the orcid settings + */ item: Item; constructor( - private itemService: ItemDataService, + private authService: AuthService, private researcherProfileService: ResearcherProfileService, private route: ActivatedRoute, - @Inject(NativeWindowService) private _window: NativeWindowRef, + private router: Router ) { - this.itemService.findById(this.route.snapshot.paramMap.get('id'), true, true).pipe(getFirstCompletedRemoteData()).subscribe((data: RemoteData) => { - this.item = data.payload; + } + + /** + * Retrieve the item for which showing the orcid settings + */ + ngOnInit(): void { + this.route.data.pipe( + map((data) => data.dso as RemoteData), + redirectOn4xx(this.router, this.authService), + getFirstSucceededRemoteDataPayload() + ).subscribe((item) => { + this.item = item; }); } + /** + * Check if the current item is linked to an ORCID profile. + * + * @returns the check result + */ isLinkedToOrcid(): boolean { return this.researcherProfileService.isLinkedToOrcid(this.item); } + /** + * Get the route to an item's page + */ getItemPage(): string { return getItemPageRoute(this.item); } diff --git a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts index 00a6018892..2429ac2e43 100644 --- a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts +++ b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts @@ -1,16 +1,18 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { FormGroup } from '@angular/forms'; -import { ActivatedRoute } from '@angular/router'; + import { TranslateService } from '@ngx-translate/core'; import { Operation } from 'fast-json-patch'; +import { of } from 'rxjs'; import { switchMap } from 'rxjs/operators'; + import { AuthService } from '../../../core/auth/auth.service'; -import { ItemDataService } from '../../../core/data/item-data.service'; import { RemoteData } from '../../../core/data/remote-data'; import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service'; import { Item } from '../../../core/shared/item.model'; -import { getFinishedRemoteData, getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model'; @Component({ selector: 'ds-orcid-setting', @@ -19,6 +21,11 @@ import { NotificationsService } from '../../../shared/notifications/notification }) export class OrcidSettingComponent implements OnInit { + /** + * The item for which showing the orcid settings + */ + @Input() item: Item; + messagePrefix = 'person.page.orcid'; currentSyncMode: string; @@ -35,18 +42,12 @@ export class OrcidSettingComponent implements OnInit { syncProfileOptions: { value: string, label: string, checked: boolean }[]; - item: Item; constructor(private researcherProfileService: ResearcherProfileService, protected translateService: TranslateService, private notificationsService: NotificationsService, - public authService: AuthService, - private route: ActivatedRoute, - private itemService: ItemDataService + public authService: AuthService ) { - this.itemService.findById(this.route.snapshot.paramMap.get('id'), true, true).pipe(getFirstCompletedRemoteData()).subscribe((data: RemoteData) => { - this.item = data.payload; - }); } ngOnInit() { @@ -110,10 +111,18 @@ export class OrcidSettingComponent implements OnInit { return; } - this.researcherProfileService.findById(this.item.firstMetadata('dspace.object.owner').authority).pipe( - switchMap((profile) => this.researcherProfileService.patch(profile, operations)), - getFinishedRemoteData() - ).subscribe((remoteData) => { + this.researcherProfileService.findByRelatedItem(this.item).pipe( + getFirstCompletedRemoteData(), + switchMap((profileRD: RemoteData) => { + if (profileRD.hasSucceeded) { + return this.researcherProfileService.updateByOrcidOperations(profileRD.payload, operations).pipe( + getFirstCompletedRemoteData() + ); + } else { + return of(profileRD); + } + }), + ).subscribe((remoteData: RemoteData) => { if (remoteData.isSuccess) { this.notificationsService.success(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.success')); } else { @@ -135,5 +144,4 @@ export class OrcidSettingComponent implements OnInit { return (currentPreference && allowedValues.includes(currentPreference)) ? currentPreference : defaultValue; } - } From 17cc3078df6e6acce4447f474bae460c452f1901 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 9 Jun 2022 19:23:19 +0200 Subject: [PATCH 13/29] [CST-5668] Fix after merge --- .../researcher-profile.service.spec.ts | 14 +++++++- .../profile/researcher-profile.service.ts | 33 ++++++++++++++++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/app/core/profile/researcher-profile.service.spec.ts b/src/app/core/profile/researcher-profile.service.spec.ts index eb5ff776fe..0dada6e5e3 100644 --- a/src/app/core/profile/researcher-profile.service.spec.ts +++ b/src/app/core/profile/researcher-profile.service.spec.ts @@ -26,6 +26,8 @@ import { ReplaceOperation } from 'fast-json-patch'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { PostRequest } from '../data/request.models'; import { followLink } from '../../shared/utils/follow-link-config.model'; +import { ConfigurationProperty } from '../shared/configuration-property.model'; +import { ConfigurationDataService } from '../data/configuration-data.service'; describe('ResearcherProfileService', () => { let scheduler: TestScheduler; @@ -36,6 +38,7 @@ describe('ResearcherProfileService', () => { let objectCache: ObjectCacheService; let halService: HALEndpointService; let responseCacheEntry: RequestEntry; + let configurationDataService: ConfigurationDataService; const researcherProfileId = 'beef9946-rt56-479e-8f11-b90cbe9f7241'; const itemId = 'beef9946-rt56-479e-8f11-b90cbe9f7241'; @@ -136,6 +139,14 @@ describe('ResearcherProfileService', () => { const itemService = jasmine.createSpyObj('ItemService', { findByHref: jasmine.createSpy('findByHref') }); + configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'test', + values: [ + 'org.dspace.ctask.general.ProfileFormats = test' + ] + })) + }); service = new ResearcherProfileService( requestService, @@ -146,7 +157,8 @@ describe('ResearcherProfileService', () => { http, routerStub, comparator, - itemService + itemService, + configurationDataService ); serviceAsAny = service; diff --git a/src/app/core/profile/researcher-profile.service.ts b/src/app/core/profile/researcher-profile.service.ts index a3ed6d22af..702c168539 100644 --- a/src/app/core/profile/researcher-profile.service.ts +++ b/src/app/core/profile/researcher-profile.service.ts @@ -4,7 +4,7 @@ import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; import { Store } from '@ngrx/store'; -import { RemoveOperation, ReplaceOperation } from 'fast-json-patch'; +import { Operation, RemoveOperation, ReplaceOperation } from 'fast-json-patch'; import { combineLatest, Observable } from 'rxjs'; import { find, map, switchMap } from 'rxjs/operators'; @@ -32,10 +32,11 @@ 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'; +import { hasValue, isEmpty } from '../../shared/empty.util'; import { CoreState } from '../core-state.model'; import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { Item } from '../shared/item.model'; +import { createFailedRemoteDataObject$ } from '../../shared/remote-data.utils'; /** * A private DataService implementation to delegate specific methods to. @@ -64,9 +65,9 @@ class ResearcherProfileServiceImpl extends DataService { @dataService(RESEARCHER_PROFILE) export class ResearcherProfileService { - dataService: ResearcherProfileServiceImpl; + protected dataService: ResearcherProfileServiceImpl; - responseMsToLive: number = 10 * 1000; + protected responseMsToLive: number = 10 * 1000; constructor( protected requestService: RequestService, @@ -121,6 +122,20 @@ export class ResearcherProfileService { ); } + /** + * Find a researcher profile by its own related item + * + * @param item + */ + public findByRelatedItem(item: Item): Observable> { + const profileId = item.firstMetadata('dspace.object.owner')?.authority; + if (isEmpty(profileId)) { + return createFailedRemoteDataObject$(); + } else { + return this.findById(profileId); + } + } + /** * Find the item id related to the given researcher profile. * @@ -253,6 +268,16 @@ export class ResearcherProfileService { return this.rdbService.buildFromRequestUUID(requestId, followLink('item')); } + /** + * Update researcher profile by patch orcid operation + * + * @param researcherProfile + * @param operations + */ + public updateByOrcidOperations(researcherProfile: ResearcherProfile, operations: Operation[]): Observable> { + return this.dataService.patch(researcherProfile, operations); + } + private getOrcidDisconnectionAllowedUsersConfiguration(): Observable { return this.configurationService.findByPropertyName('orcid.disconnection.allowed-users').pipe( getFirstSucceededRemoteDataPayload() From 3cb5e0c22662656eae4a1e1460c97cf7414bcce9 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 10 Jun 2022 11:32:48 +0200 Subject: [PATCH 14/29] [CST-5668] Fix button style and tooltip messages --- .../dso-page-edit-button.component.scss | 4 +--- .../dso-page-orcid-button.component.html | 3 ++- .../dso-page-orcid-button.component.ts | 14 +++++--------- .../person-page-claim-button.component.html | 8 +++++++- src/assets/i18n/en.json5 | 6 ++++++ src/styles/_global-styles.scss | 5 +++++ 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/app/shared/dso-page/dso-page-edit-button/dso-page-edit-button.component.scss b/src/app/shared/dso-page/dso-page-edit-button/dso-page-edit-button.component.scss index e8b7d689a3..8b13789179 100644 --- a/src/app/shared/dso-page/dso-page-edit-button/dso-page-edit-button.component.scss +++ b/src/app/shared/dso-page/dso-page-edit-button/dso-page-edit-button.component.scss @@ -1,3 +1 @@ -.btn-dark { - background-color: var(--ds-admin-sidebar-bg); -} + diff --git a/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.html b/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.html index 7a3383fd1a..03a07beb8a 100644 --- a/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.html +++ b/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.html @@ -1,5 +1,6 @@ ORCID + role="button" >{{'item.page.orcid.title' | translate}} diff --git a/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.ts b/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.ts index 9f244da7c9..c345d8cbdc 100644 --- a/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.ts +++ b/src/app/shared/dso-page/dso-page-orcid-button/dso-page-orcid-button.component.ts @@ -1,9 +1,11 @@ import { Component, Input, OnInit } from '@angular/core'; + import { BehaviorSubject } from 'rxjs'; import { take } from 'rxjs/operators'; -import { AuthorizationDataService } from 'src/app/core/data/feature-authorization/authorization-data.service'; -import { FeatureID } from 'src/app/core/data/feature-authorization/feature-id'; -import { DSpaceObject } from 'src/app/core/shared/dspace-object.model'; + +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; @Component({ selector: 'ds-dso-page-orcid-button', @@ -21,12 +23,6 @@ export class DsoPageOrcidButtonComponent implements OnInit { */ @Input() pageRoute: string; - /** - * A message for the tooltip on the button - * Supports i18n keys - */ - @Input() tooltipMsg: string; - /** * Whether or not the current user is authorized to edit the DSpaceObject */ diff --git a/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.html b/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.html index 12d5d2b47d..c4bba286bf 100644 --- a/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.html +++ b/src/app/shared/dso-page/person-page-claim-button/person-page-claim-button.component.html @@ -1 +1,7 @@ - + diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 0449e9fda7..c87c76dc9c 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2186,6 +2186,10 @@ "item.page.link.simple": "Simple item page", + "item.page.orcid.title": "ORCID", + + "item.page.orcid.tooltip": "Open ORCID setting page", + "item.page.person.search.title": "Articles by this author", "item.page.related-items.view-more": "Show {{ amount }} more", @@ -2220,6 +2224,8 @@ "item.page.claim.button": "Claim", + "item.page.claim.tooltip": "Claim this item as profile", + "item.preview.dc.identifier.uri": "Identifier:", "item.preview.dc.contributor.author": "Authors:", diff --git a/src/styles/_global-styles.scss b/src/styles/_global-styles.scss index 1bd2ec7b0a..89d1d76e9a 100644 --- a/src/styles/_global-styles.scss +++ b/src/styles/_global-styles.scss @@ -133,3 +133,8 @@ ds-dynamic-form-control-container.d-none { */ visibility: collapse; } + +/* Used for dso administrative functionality */ +.btn-dark { + background-color: var(--ds-admin-sidebar-bg); +} From b372a27513961613a9435e651aaf6e9de38a88f8 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 10 Jun 2022 12:30:15 +0200 Subject: [PATCH 15/29] [CST-5668] Add test for orcid-page.component --- .../orcid-page/orcid-page.component.html | 6 +- .../orcid-page/orcid-page.component.spec.ts | 113 ++++++++++++++++++ 2 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 src/app/item-page/orcid-page/orcid-page.component.spec.ts diff --git a/src/app/item-page/orcid-page/orcid-page.component.html b/src/app/item-page/orcid-page/orcid-page.component.html index c105ba6127..91c49596b6 100644 --- a/src/app/item-page/orcid-page/orcid-page.component.html +++ b/src/app/item-page/orcid-page/orcid-page.component.html @@ -1,7 +1,9 @@ -
+ diff --git a/src/app/item-page/orcid-page/orcid-page.component.spec.ts b/src/app/item-page/orcid-page/orcid-page.component.spec.ts new file mode 100644 index 0000000000..1d61f18c3e --- /dev/null +++ b/src/app/item-page/orcid-page/orcid-page.component.spec.ts @@ -0,0 +1,113 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; + +import { of as observableOf } from 'rxjs'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; + +import { AuthService } from '../../core/auth/auth.service'; +import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; +import { ResearcherProfileService } from '../../core/profile/researcher-profile.service'; +import { OrcidPageComponent } from './orcid-page.component'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { Item } from '../../core/shared/item.model'; +import { createPaginatedList } from '../../shared/testing/utils.test'; +import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; + +fdescribe('OrcidPageComponent test suite', () => { + let comp: OrcidPageComponent; + let fixture: ComponentFixture; + + let authService: jasmine.SpyObj; + let routeStub: jasmine.SpyObj; + let routeData: any; + let researcherProfileService: jasmine.SpyObj; + + const mockItem: Item = Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'test item' + } + ] + } + }); + const mockItemLinkedToOrcid: Item = Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: { + 'dc.title': [ + { + value: 'test item' + } + ], + 'dspace.orcid.authenticated': [ + { + value: 'true' + } + ] + } + }); + + beforeEach(waitForAsync(() => { + authService = jasmine.createSpyObj('authService', { + isAuthenticated: jasmine.createSpy('isAuthenticated'), + navigateByUrl: jasmine.createSpy('navigateByUrl') + }); + + routeData = { + dso: createSuccessfulRemoteDataObject(mockItem), + }; + + routeStub = Object.assign(new ActivatedRouteStub(), { + data: observableOf(routeData) + }); + + researcherProfileService = jasmine.createSpyObj('researcherProfileService', { + isLinkedToOrcid: jasmine.createSpy('isLinkedToOrcid') + }); + + void TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + RouterTestingModule.withRoutes([]) + ], + declarations: [OrcidPageComponent], + providers: [ + { provide: ActivatedRoute, useValue: routeStub }, + { provide: ResearcherProfileService, useValue: researcherProfileService }, + { provide: AuthService, useValue: authService }, + ], + + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(waitForAsync(() => { + fixture = TestBed.createComponent(OrcidPageComponent); + comp = fixture.componentInstance; + authService.isAuthenticated.and.returnValue(observableOf(true)); + fixture.detectChanges(); + })); + + it('should create', () => { + const btn = fixture.debugElement.queryAll(By.css('[data-test="back-button"]')); + expect(comp).toBeTruthy(); + expect(btn.length).toBe(1); + }); + + it('should call isLinkedToOrcid', () => { + comp.isLinkedToOrcid(); + + expect(researcherProfileService.isLinkedToOrcid).toHaveBeenCalledWith(comp.item); + }); + +}); From 03369b8e10cfec86294a96d00e7279f7cf300393 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 10 Jun 2022 19:03:31 +0200 Subject: [PATCH 16/29] [CST-5668] Address feedback and add test for orcid-auth.component --- .../researcher-profile.service.spec.ts | 250 ++++++++++++- .../profile/researcher-profile.service.ts | 64 ++-- .../orcid-auth/orcid-auth.component.html | 96 ++--- .../orcid-auth/orcid-auth.component.spec.ts | 336 ++++++++++++++++++ .../orcid-auth/orcid-auth.component.ts | 176 +++++++-- .../orcid-page/orcid-page.component.html | 6 +- .../orcid-page/orcid-page.component.spec.ts | 26 +- .../orcid-page/orcid-page.component.ts | 26 +- 8 files changed, 863 insertions(+), 117 deletions(-) create mode 100644 src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.spec.ts diff --git a/src/app/core/profile/researcher-profile.service.spec.ts b/src/app/core/profile/researcher-profile.service.spec.ts index 0dada6e5e3..9a6b0477d5 100644 --- a/src/app/core/profile/researcher-profile.service.spec.ts +++ b/src/app/core/profile/researcher-profile.service.spec.ts @@ -12,6 +12,7 @@ import { RequestService } from '../data/request.service'; import { PageInfo } from '../shared/page-info.model'; import { buildPaginatedList } from '../data/paginated-list.model'; import { + createFailedRemoteDataObject$, createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ @@ -22,12 +23,14 @@ import { ResearcherProfileService } from './researcher-profile.service'; import { RouterMock } from '../../shared/mocks/router.mock'; import { ResearcherProfile } from './model/researcher-profile.model'; import { Item } from '../shared/item.model'; -import { ReplaceOperation } from 'fast-json-patch'; +import { RemoveOperation, ReplaceOperation } from 'fast-json-patch'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { PostRequest } from '../data/request.models'; import { followLink } from '../../shared/utils/follow-link-config.model'; import { ConfigurationProperty } from '../shared/configuration-property.model'; import { ConfigurationDataService } from '../data/configuration-data.service'; +import { createPaginatedList } from '../../shared/testing/utils.test'; +import { environment } from '../../../environments/environment'; describe('ResearcherProfileService', () => { let scheduler: TestScheduler; @@ -89,6 +92,106 @@ describe('ResearcherProfileService', () => { }, } }); + + const mockItemUnlinkedToOrcid: Item = Object.assign(new Item(), { + id: 'mockItemUnlinkedToOrcid', + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: { + 'dc.title': [{ + value: 'test person' + }], + 'dspace.entity.type': [{ + 'value': 'Person' + }] + } + }); + + const mockItemLinkedToOrcid: Item = Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: { + 'dc.title': [{ + value: 'test person' + }], + 'dspace.entity.type': [{ + 'value': 'Person' + }], + 'dspace.object.owner': [{ + 'value': 'test person', + 'language': null, + 'authority': 'researcher-profile-id', + 'confidence': 600, + 'place': 0 + }], + 'dspace.orcid.authenticated': [{ + 'value': '2022-06-10T15:15:12.952872', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }], + 'dspace.orcid.scope': [{ + 'value': '/authenticate', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }, { + 'value': '/read-limited', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 1 + }, { + 'value': '/activities/update', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 2 + }, { + 'value': '/person/update', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 3 + }], + 'person.identifier.orcid': [{ + 'value': 'orcid-id', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }] + } + }); + + const disconnectionAllowAdmin = { + uuid: 'orcid.disconnection.allowed-users', + name: 'orcid.disconnection.allowed-users', + values: ['only_admin'] + } as ConfigurationProperty; + + const disconnectionAllowAdminOwner = { + uuid: 'orcid.disconnection.allowed-users', + name: 'orcid.disconnection.allowed-users', + values: ['admin_and_owner'] + } as ConfigurationProperty; + + const authorizeUrl = { + uuid: 'orcid.authorize-url', + name: 'orcid.authorize-url', + values: ['orcid.authorize-url'] + } as ConfigurationProperty; + const appClientId = { + uuid: 'orcid.application-client-id', + name: 'orcid.application-client-id', + values: ['orcid.application-client-id'] + } as ConfigurationProperty; + const orcidScope = { + uuid: 'orcid.scope', + name: 'orcid.scope', + values: ['/authenticate', '/read-limited'] + } as ConfigurationProperty; + const endpointURL = `https://rest.api/rest/api/profiles`; const endpointURLWithEmbed = 'https://rest.api/rest/api/profiles?embed=item'; const sourceUri = `https://rest.api/rest/api/external-source/profile`; @@ -140,12 +243,7 @@ describe('ResearcherProfileService', () => { findByHref: jasmine.createSpy('findByHref') }); configurationDataService = jasmine.createSpyObj('configurationDataService', { - findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { - name: 'test', - values: [ - 'org.dspace.ctask.general.ProfileFormats = test' - ] - })) + findByPropertyName: jasmine.createSpy('findByPropertyName') }); service = new ResearcherProfileService( @@ -283,7 +381,7 @@ describe('ResearcherProfileService', () => { }); describe('createFromExternalSource', () => { - let patchSpy; + beforeEach(() => { spyOn((service as any).dataService, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched)); spyOn((service as any), 'findById').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched)); @@ -305,4 +403,140 @@ describe('ResearcherProfileService', () => { }); }); + describe('isLinkedToOrcid', () => { + it('should return true when item has metadata', () => { + const result = service.isLinkedToOrcid(mockItemLinkedToOrcid); + expect(result).toBeTrue(); + }); + + it('should return true when item has no metadata', () => { + const result = service.isLinkedToOrcid(mockItemUnlinkedToOrcid); + expect(result).toBeFalse(); + }); + }); + + describe('onlyAdminCanDisconnectProfileFromOrcid', () => { + it('should return true when property is only_admin', () => { + spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createSuccessfulRemoteDataObject$(disconnectionAllowAdmin)); + const result = service.onlyAdminCanDisconnectProfileFromOrcid(); + const expected = cold('(a|)', { + a: true + }); + expect(result).toBeObservable(expected); + }); + + it('should return false on faild', () => { + spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createFailedRemoteDataObject$()); + const result = service.onlyAdminCanDisconnectProfileFromOrcid(); + const expected = cold('(a|)', { + a: false + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('ownerCanDisconnectProfileFromOrcid', () => { + it('should return true when property is admin_and_owner', () => { + spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createSuccessfulRemoteDataObject$(disconnectionAllowAdminOwner)); + const result = service.ownerCanDisconnectProfileFromOrcid(); + const expected = cold('(a|)', { + a: true + }); + expect(result).toBeObservable(expected); + }); + + it('should return false on faild', () => { + spyOn((service as any), 'getOrcidDisconnectionAllowedUsersConfiguration').and.returnValue(createFailedRemoteDataObject$()); + const result = service.ownerCanDisconnectProfileFromOrcid(); + const expected = cold('(a|)', { + a: false + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('unlinkOrcid', () => { + beforeEach(() => { + scheduler = getTestScheduler(); + spyOn((service as any).dataService, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched)); + spyOn((service as any), 'findById').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfile)); + }); + + it('should call patch method properly', () => { + const operations: RemoveOperation[] = [{ + path: '/orcid', + op: 'remove' + }]; + + scheduler.schedule(() => service.unlinkOrcid(mockItemLinkedToOrcid).subscribe()); + scheduler.flush(); + + expect((service as any).dataService.patch).toHaveBeenCalledWith(researcherProfile, operations); + }); + }); + + describe('getOrcidAuthorizeUrl', () => { + beforeEach(() => { + (service as any).configurationService.findByPropertyName.and.returnValues( + createSuccessfulRemoteDataObject$(authorizeUrl), + createSuccessfulRemoteDataObject$(appClientId), + createSuccessfulRemoteDataObject$(orcidScope) + ); + }); + + it('should build the url properly', () => { + const result = service.getOrcidAuthorizeUrl(mockItemUnlinkedToOrcid); + const redirectUri = environment.rest.baseUrl + '/api/eperson/orcid/' + mockItemUnlinkedToOrcid.id + '/?url=undefined'; + const url = 'orcid.authorize-url?client_id=orcid.application-client-id&redirect_uri=' + redirectUri + '&response_type=code&scope=/authenticate /read-limited'; + + const expected = cold('(a|)', { + a: url + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('updateByOrcidOperations', () => { + beforeEach(() => { + scheduler = getTestScheduler(); + spyOn((service as any).dataService, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched)); + }); + + it('should call patch method properly', () => { + scheduler.schedule(() => service.updateByOrcidOperations(researcherProfile, []).subscribe()); + scheduler.flush(); + + expect((service as any).dataService.patch).toHaveBeenCalledWith(researcherProfile, []); + }); + }); + + describe('getOrcidAuthorizationScopesByItem', () => { + it('should return list of scopes saved in the item', () => { + const orcidScopes = [ + '/authenticate', + '/read-limited', + '/activities/update', + '/person/update' + ]; + const result = service.getOrcidAuthorizationScopesByItem(mockItemLinkedToOrcid); + expect(result).toEqual(orcidScopes); + }); + }); + + describe('getOrcidAuthorizationScopes', () => { + it('should return list of scopes by configuration', () => { + (service as any).configurationService.findByPropertyName.and.returnValue( + createSuccessfulRemoteDataObject$(orcidScope) + ); + const orcidScopes = [ + '/authenticate', + '/read-limited' + ]; + const expected = cold('(a|)', { + a: orcidScopes + }); + const result = service.getOrcidAuthorizationScopes(); + expect(result).toBeObservable(expected); + }); + }); }); diff --git a/src/app/core/profile/researcher-profile.service.ts b/src/app/core/profile/researcher-profile.service.ts index 702c168539..f944d53b46 100644 --- a/src/app/core/profile/researcher-profile.service.ts +++ b/src/app/core/profile/researcher-profile.service.ts @@ -32,7 +32,7 @@ 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, isEmpty } from '../../shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { CoreState } from '../core-state.model'; import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { Item } from '../shared/item.model'; @@ -171,7 +171,7 @@ export class ResearcherProfileService { * @param item the item to check * @returns the check result */ - isLinkedToOrcid(item: Item): boolean { + public isLinkedToOrcid(item: Item): boolean { return item.hasMetadata('dspace.orcid.authenticated'); } @@ -180,9 +180,11 @@ export class ResearcherProfileService { * * @returns the check result */ - onlyAdminCanDisconnectProfileFromOrcid(): Observable { + public onlyAdminCanDisconnectProfileFromOrcid(): Observable { return this.getOrcidDisconnectionAllowedUsersConfiguration().pipe( - map((property) => property.values.map( (value) => value.toLowerCase()).includes('only_admin')) + map((propertyRD: RemoteData) => { + return propertyRD.hasSucceeded && propertyRD.payload.values.map((value) => value.toLowerCase()).includes('only_admin'); + }) ); } @@ -191,25 +193,10 @@ export class ResearcherProfileService { * * @returns the check result */ - ownerCanDisconnectProfileFromOrcid(): Observable { + public ownerCanDisconnectProfileFromOrcid(): Observable { 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 { - return this.getOrcidDisconnectionAllowedUsersConfiguration().pipe( - map((property) => { - const values = property.values.map( (value) => value.toLowerCase()); - return values.includes('only_admin') || values.includes('admin_and_owner'); + map((propertyRD: RemoteData) => { + return propertyRD.hasSucceeded && propertyRD.payload.values.map( (value) => value.toLowerCase()).includes('admin_and_owner'); }) ); } @@ -217,8 +204,7 @@ export class ResearcherProfileService { /** * If the given item represents a profile unlink it from ORCID. */ - unlinkOrcid(item: Item): Observable> { - + public unlinkOrcid(item: Item): Observable> { const operations: RemoveOperation[] = [{ path:'/orcid', op:'remove' @@ -231,7 +217,12 @@ export class ResearcherProfileService { ); } - getOrcidAuthorizeUrl(profile: Item): Observable { + /** + * Build and return the url to authenticate with orcid + * + * @param profile + */ + public getOrcidAuthorizeUrl(profile: Item): Observable { return combineLatest([ this.configurationService.findByPropertyName('orcid.authorize-url').pipe(getFirstSucceededRemoteDataPayload()), this.configurationService.findByPropertyName('orcid.application-client-id').pipe(getFirstSucceededRemoteDataPayload()), @@ -278,9 +269,28 @@ export class ResearcherProfileService { return this.dataService.patch(researcherProfile, operations); } - private getOrcidDisconnectionAllowedUsersConfiguration(): Observable { + /** + * Return all orcid authorization scopes saved in the given item + * + * @param item + */ + public getOrcidAuthorizationScopesByItem(item: Item): string[] { + return isNotEmpty(item) ? item.allMetadataValues('dspace.orcid.scope') : []; + } + + /** + * Return all orcid authorization scopes available by configuration + */ + public getOrcidAuthorizationScopes(): Observable { + return this.configurationService.findByPropertyName('orcid.scope').pipe( + getFirstCompletedRemoteData(), + map((propertyRD: RemoteData) => propertyRD.hasSucceeded ? propertyRD.payload.values : []) + ); + } + + private getOrcidDisconnectionAllowedUsersConfiguration(): Observable> { return this.configurationService.findByPropertyName('orcid.disconnection.allowed-users').pipe( - getFirstSucceededRemoteDataPayload() + getFirstCompletedRemoteData() ); } diff --git a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html index 12b13c433c..b15b23d1e0 100644 --- a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html +++ b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html @@ -3,75 +3,89 @@
- +
+
- -
-
-
{{ 'person.page.orcid.granted-authorizations'| translate }}
-
-
-
    -
  • {{getAuthorizationDescription(auth) | translate}}
  • -
-
-
-
-
-
{{ 'person.page.orcid.missing-authorizations'| translate }}
-
-
-
- {{'person.page.orcid.no-missing-authorizations-message' | translate}} -
-
- {{'person.page.orcid.missing-authorizations-message' | translate}} + +
+
+
+
+
{{ 'person.page.orcid.granted-authorizations'| translate }}
+
+
    -
  • {{getAuthorizationDescription(auth) | translate }}
  • +
  • + {{getAuthorizationDescription(auth) | translate}} +
+
+
+
{{ 'person.page.orcid.missing-authorizations'| translate }}
+
+
+ + {{'person.page.orcid.no-missing-authorizations-message' | translate}} + + + {{'person.page.orcid.missing-authorizations-message' | translate}} +
    +
  • + {{getAuthorizationDescription(auth) | translate }} +
  • +
+
+
+
+
+
-
+ {{ 'person.page.orcid.remove-orcid-message' | translate}} -
-
-
- -
- +
+
- + +
orcid-logo
-
{{ getOrcidNotLinkedMessage() | async }}
+
+ {{ getOrcidNotLinkedMessage() | async }} +
-
+
- -
+
+
+ diff --git a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.spec.ts b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.spec.ts new file mode 100644 index 0000000000..a2ec1cf9b1 --- /dev/null +++ b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.spec.ts @@ -0,0 +1,336 @@ +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; + +import { getTestScheduler } from 'jasmine-marbles'; +import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; +import { of } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; + +import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { Item } from '../../../core/shared/item.model'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; +import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; +import { OrcidAuthComponent } from './orcid-auth.component'; +import { NativeWindowService } from '../../../core/services/window.service'; +import { NativeWindowMockFactory } from '../../../shared/mocks/mock-native-window-ref'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model'; + +describe('OrcidAuthComponent test suite', () => { + let comp: OrcidAuthComponent; + let fixture: ComponentFixture; + let scheduler: TestScheduler; + let researcherProfileService: jasmine.SpyObj; + let nativeWindowRef; + let notificationsService; + + const orcidScopes = [ + '/authenticate', + '/read-limited', + '/activities/update', + '/person/update' + ]; + + const partialOrcidScopes = [ + '/authenticate', + '/read-limited', + ]; + + const mockItemUnlinkedToOrcid: Item = Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: { + 'dc.title': [{ + value: 'test person' + }], + 'dspace.entity.type': [{ + 'value': 'Person' + }] + } + }); + + const mockItemLinkedToOrcid: Item = Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: { + 'dc.title': [{ + value: 'test person' + }], + 'dspace.entity.type': [{ + 'value': 'Person' + }], + 'dspace.object.owner': [{ + 'value': 'test person', + 'language': null, + 'authority': 'deced3e7-68e2-495d-bf98-7c44fc33b8ff', + 'confidence': 600, + 'place': 0 + }], + 'dspace.orcid.authenticated': [{ + 'value': '2022-06-10T15:15:12.952872', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }], + 'dspace.orcid.scope': [{ + 'value': '/authenticate', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }, { + 'value': '/read-limited', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 1 + }, { + 'value': '/activities/update', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 2 + }, { + 'value': '/person/update', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 3 + }], + 'person.identifier.orcid': [{ + 'value': 'orcid-id', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }] + } + }); + + beforeEach(waitForAsync(() => { + researcherProfileService = jasmine.createSpyObj('researcherProfileService', { + getOrcidAuthorizationScopes: jasmine.createSpy('getOrcidAuthorizationScopes'), + getOrcidAuthorizationScopesByItem: jasmine.createSpy('getOrcidAuthorizationScopesByItem'), + getOrcidAuthorizeUrl: jasmine.createSpy('getOrcidAuthorizeUrl'), + isLinkedToOrcid: jasmine.createSpy('isLinkedToOrcid'), + onlyAdminCanDisconnectProfileFromOrcid: jasmine.createSpy('onlyAdminCanDisconnectProfileFromOrcid'), + ownerCanDisconnectProfileFromOrcid: jasmine.createSpy('ownerCanDisconnectProfileFromOrcid'), + unlinkOrcid: jasmine.createSpy('unlinkOrcid') + }); + + void TestBed.configureTestingModule({ + imports: [ + NgbAccordionModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + RouterTestingModule.withRoutes([]) + ], + declarations: [OrcidAuthComponent], + providers: [ + { provide: NativeWindowService, useFactory: NativeWindowMockFactory }, + { provide: NotificationsService, useClass: NotificationsServiceStub }, + { provide: ResearcherProfileService, useValue: researcherProfileService } + ], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(OrcidAuthComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(waitForAsync(() => { + scheduler = getTestScheduler(); + fixture = TestBed.createComponent(OrcidAuthComponent); + comp = fixture.componentInstance; + researcherProfileService.getOrcidAuthorizationScopes.and.returnValue(of(orcidScopes)); + })); + + describe('when orcid profile is not linked', () => { + beforeEach(waitForAsync(() => { + comp.item = mockItemUnlinkedToOrcid; + researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([]); + researcherProfileService.isLinkedToOrcid.and.returnValue(false); + researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false)); + researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true)); + researcherProfileService.getOrcidAuthorizeUrl.and.returnValue(of('oarcidUrl')); + fixture.detectChanges(); + })); + + it('should create', fakeAsync(() => { + const orcidLinked = fixture.debugElement.query(By.css('[data-test="orcidLinked"]')); + const orcidNotLinked = fixture.debugElement.query(By.css('[data-test="orcidNotLinked"]')); + expect(orcidLinked).toBeFalsy(); + expect(orcidNotLinked).toBeTruthy(); + })); + + it('should change location on link', () => { + nativeWindowRef = (comp as any)._window; + scheduler.schedule(() => comp.linkOrcid()); + scheduler.flush(); + + expect(nativeWindowRef.nativeWindow.location.href).toBe('oarcidUrl'); + }); + + }); + + describe('when orcid profile is linked', () => { + beforeEach(waitForAsync(() => { + comp.item = mockItemLinkedToOrcid; + researcherProfileService.isLinkedToOrcid.and.returnValue(true); + })); + + describe('', () => { + + beforeEach(waitForAsync(() => { + comp.item = mockItemLinkedToOrcid; + notificationsService = (comp as any).notificationsService; + researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]); + researcherProfileService.isLinkedToOrcid.and.returnValue(true); + researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false)); + researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true)); + })); + + describe('and unlink is successfully', () => { + beforeEach(waitForAsync(() => { + comp.item = mockItemLinkedToOrcid; + researcherProfileService.unlinkOrcid.and.returnValue(createSuccessfulRemoteDataObject$(new ResearcherProfile())); + spyOn(comp.unlink, 'emit'); + fixture.detectChanges(); + })); + + it('should show success notification', () => { + scheduler.schedule(() => comp.unlinkOrcid()); + scheduler.flush(); + + expect(notificationsService.success).toHaveBeenCalled(); + expect(comp.unlink.emit).toHaveBeenCalled(); + }); + }); + + describe('and unlink is failed', () => { + beforeEach(waitForAsync(() => { + comp.item = mockItemLinkedToOrcid; + researcherProfileService.unlinkOrcid.and.returnValue(createFailedRemoteDataObject$()); + fixture.detectChanges(); + })); + + it('should show success notification', () => { + scheduler.schedule(() => comp.unlinkOrcid()); + scheduler.flush(); + + expect(notificationsService.error).toHaveBeenCalled(); + }); + }); + }); + + describe('and has orcid authorization scopes', () => { + + beforeEach(waitForAsync(() => { + comp.item = mockItemLinkedToOrcid; + researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]); + researcherProfileService.isLinkedToOrcid.and.returnValue(true); + researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false)); + researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true)); + fixture.detectChanges(); + })); + + it('should create', fakeAsync(() => { + const orcidLinked = fixture.debugElement.query(By.css('[data-test="orcidLinked"]')); + const orcidNotLinked = fixture.debugElement.query(By.css('[data-test="orcidNotLinked"]')); + expect(orcidLinked).toBeTruthy(); + expect(orcidNotLinked).toBeFalsy(); + })); + + it('should display orcid authorizations', fakeAsync(() => { + const orcidAuthorizations = fixture.debugElement.query(By.css('[data-test="hasOrcidAuthorizations"]')); + const noMissingOrcidAuthorizations = fixture.debugElement.query(By.css('[data-test="noMissingOrcidAuthorizations"]')); + const orcidAuthorizationsList = fixture.debugElement.queryAll(By.css('[data-test="orcidAuthorization"]')); + + expect(orcidAuthorizations).toBeTruthy(); + expect(noMissingOrcidAuthorizations).toBeTruthy(); + expect(orcidAuthorizationsList.length).toBe(4); + })); + }); + + describe('and has missing orcid authorization scopes', () => { + + beforeEach(waitForAsync(() => { + comp.item = mockItemLinkedToOrcid; + researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...partialOrcidScopes]); + researcherProfileService.isLinkedToOrcid.and.returnValue(true); + researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false)); + researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true)); + fixture.detectChanges(); + })); + + it('should create', fakeAsync(() => { + const orcidLinked = fixture.debugElement.query(By.css('[data-test="orcidLinked"]')); + const orcidNotLinked = fixture.debugElement.query(By.css('[data-test="orcidNotLinked"]')); + expect(orcidLinked).toBeTruthy(); + expect(orcidNotLinked).toBeFalsy(); + })); + + it('should display orcid authorizations', fakeAsync(() => { + const orcidAuthorizations = fixture.debugElement.query(By.css('[data-test="hasOrcidAuthorizations"]')); + const missingOrcidAuthorizations = fixture.debugElement.query(By.css('[data-test="missingOrcidAuthorizations"]')); + const orcidAuthorizationsList = fixture.debugElement.queryAll(By.css('[data-test="orcidAuthorization"]')); + const missingOrcidAuthorizationsList = fixture.debugElement.queryAll(By.css('[data-test="missingOrcidAuthorization"]')); + + expect(orcidAuthorizations).toBeTruthy(); + expect(missingOrcidAuthorizations).toBeTruthy(); + expect(orcidAuthorizationsList.length).toBe(2); + expect(missingOrcidAuthorizationsList.length).toBe(2); + })); + }); + + describe('and only admin can unlink scopes', () => { + + beforeEach(waitForAsync(() => { + comp.item = mockItemLinkedToOrcid; + researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]); + researcherProfileService.isLinkedToOrcid.and.returnValue(true); + researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(true)); + researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(false)); + fixture.detectChanges(); + })); + + it('should display warning panel', fakeAsync(() => { + const unlinkOnlyAdmin = fixture.debugElement.query(By.css('[data-test="unlinkOnlyAdmin"]')); + const unlinkOwner = fixture.debugElement.query(By.css('[data-test="unlinkOwner"]')); + expect(unlinkOnlyAdmin).toBeTruthy(); + expect(unlinkOwner).toBeFalsy(); + })); + + }); + + describe('and owner can unlink scopes', () => { + + beforeEach(waitForAsync(() => { + comp.item = mockItemLinkedToOrcid; + researcherProfileService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]); + researcherProfileService.isLinkedToOrcid.and.returnValue(true); + researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(true)); + researcherProfileService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true)); + fixture.detectChanges(); + })); + + it('should display warning panel', fakeAsync(() => { + const unlinkOnlyAdmin = fixture.debugElement.query(By.css('[data-test="unlinkOnlyAdmin"]')); + const unlinkOwner = fixture.debugElement.query(By.css('[data-test="unlinkOwner"]')); + expect(unlinkOnlyAdmin).toBeFalsy(); + expect(unlinkOwner).toBeTruthy(); + })); + + }); + + }); + + +}); diff --git a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts index aa234648f0..ed8b1fd3d9 100644 --- a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts +++ b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts @@ -1,59 +1,126 @@ -import { Component, Inject, Input, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { Component, EventEmitter, Inject, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; + import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; -import { ItemDataService } from '../../../core/data/item-data.service'; -import { RemoteData } from '../../../core/data/remote-data'; + import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service'; import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service'; import { Item } from '../../../core/shared/item.model'; -import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { RemoteData } from '../../../core/data/remote-data'; +import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model'; @Component({ selector: 'ds-orcid-auth', templateUrl: './orcid-auth.component.html', styleUrls: ['./orcid-auth.component.scss'] }) -export class OrcidAuthComponent implements OnInit { +export class OrcidAuthComponent implements OnInit, OnChanges { /** * The item for which showing the orcid settings */ @Input() item: Item; - missingAuthorizations$ = new BehaviorSubject([]); + /** + * The list of exposed orcid authorization scopes for the orcid profile + */ + profileAuthorizationScopes: BehaviorSubject = new BehaviorSubject([]); - unlinkProcessing = false; + /** + * The list of all orcid authorization scopes missing in the orcid profile + */ + missingAuthorizationScopes: BehaviorSubject = new BehaviorSubject([]); + + /** + * The list of all orcid authorization scopes available + */ + orcidAuthorizationScopes: BehaviorSubject = new BehaviorSubject([]); + + /** + * A boolean representing if unlink operation is processing + */ + unlinkProcessing: BehaviorSubject = new BehaviorSubject(false); + + /** + * A boolean representing if orcid profile is linked + */ + private isOrcidLinked$: BehaviorSubject = new BehaviorSubject(false); + + /** + * A boolean representing if only admin can disconnect orcid profile + */ + private onlyAdminCanDisconnectProfileFromOrcid$: BehaviorSubject = new BehaviorSubject(false); + + /** + * A boolean representing if owner can disconnect orcid profile + */ + private ownerCanDisconnectProfileFromOrcid$: BehaviorSubject = new BehaviorSubject(false); + + /** + * An event emitted when orcid profile is unliked successfully + */ + @Output() unlink: EventEmitter = new EventEmitter(); constructor( - private configurationService: ConfigurationDataService, private researcherProfileService: ResearcherProfileService, - protected translateService: TranslateService, + private translateService: TranslateService, private notificationsService: NotificationsService, - private itemService: ItemDataService, - private route: ActivatedRoute, @Inject(NativeWindowService) private _window: NativeWindowRef, - ) { + ) { } ngOnInit() { - const scopes = this.getOrcidAuthorizations(); - return this.configurationService.findByPropertyName('orcid.scope') - .pipe(getFirstSucceededRemoteDataPayload(), - map((configurationProperty) => configurationProperty.values), - map((allScopes) => allScopes.filter((scope) => !scopes.includes(scope)))) - .subscribe((missingScopes) => this.missingAuthorizations$.next(missingScopes)); + this.researcherProfileService.getOrcidAuthorizationScopes().subscribe((scopes: string[]) => { + this.orcidAuthorizationScopes.next(scopes); + this.initOrcidAuthSettings(); + }); } - getOrcidAuthorizations(): string[] { - return this.item.allMetadataValues('dspace.orcid.scope'); + ngOnChanges(changes: SimpleChanges): void { + if (!changes.item.isFirstChange() && changes.item.currentValue !== changes.item.previousValue) { + this.initOrcidAuthSettings(); + } } - isLinkedToOrcid(): boolean { - return this.researcherProfileService.isLinkedToOrcid(this.item); + /** + * Check if the list of exposed orcid authorization scopes for the orcid profile has values + */ + hasOrcidAuthorizations(): Observable { + return this.profileAuthorizationScopes.asObservable().pipe( + map((scopes: string[]) => scopes.length > 0) + ); + } + + /** + * Return the list of exposed orcid authorization scopes for the orcid profile + */ + getOrcidAuthorizations(): Observable { + return this.profileAuthorizationScopes.asObservable(); + } + + /** + * Check if the list of exposed orcid authorization scopes for the orcid profile has values + */ + hasMissingOrcidAuthorizations(): Observable { + return this.missingAuthorizationScopes.asObservable().pipe( + map((scopes: string[]) => scopes.length > 0) + ); + } + + /** + * Return the list of exposed orcid authorization scopes for the orcid profile + */ + getMissingOrcidAuthorizations(): Observable { + return this.profileAuthorizationScopes.asObservable(); + } + + /** + * Return a boolean representing if orcid profile is linked + */ + isLinkedToOrcid(): Observable { + return this.isOrcidLinked$.asObservable(); } getOrcidNotLinkedMessage(): Observable { @@ -65,34 +132,85 @@ export class OrcidAuthComponent implements OnInit { } } + /** + * Get label for a given orcid authorization scope + * + * @param scope + */ getAuthorizationDescription(scope: string) { return 'person.page.orcid.scope.' + scope.substring(1).replace('/', '-'); } + /** + * Return a boolean representing if only admin can disconnect orcid profile + */ onlyAdminCanDisconnectProfileFromOrcid(): Observable { - return this.researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid(); + return this.onlyAdminCanDisconnectProfileFromOrcid$.asObservable(); } + /** + * Return a boolean representing if owner can disconnect orcid profile + */ ownerCanDisconnectProfileFromOrcid(): Observable { - return this.researcherProfileService.ownerCanDisconnectProfileFromOrcid(); + return this.ownerCanDisconnectProfileFromOrcid$.asObservable(); } + /** + * Link existing person profile with orcid + */ linkOrcid(): void { this.researcherProfileService.getOrcidAuthorizeUrl(this.item).subscribe((authorizeUrl) => { this._window.nativeWindow.location.href = authorizeUrl; }); } + /** + * Unlink existing person profile from orcid + */ unlinkOrcid(): void { - this.unlinkProcessing = true; - this.researcherProfileService.unlinkOrcid(this.item).subscribe((remoteData) => { - this.unlinkProcessing = false; + this.unlinkProcessing.next(true); + this.researcherProfileService.unlinkOrcid(this.item).subscribe((remoteData: RemoteData) => { + this.unlinkProcessing.next(false); if (remoteData.isSuccess) { this.notificationsService.success(this.translateService.get('person.page.orcid.unlink.success')); + this.unlink.emit(); } else { this.notificationsService.error(this.translateService.get('person.page.orcid.unlink.error')); } }); } + /** + * initialize all Orcid authentication settings + * @private + */ + private initOrcidAuthSettings(): void { + + this.setOrcidAuthorizationsFromItem(); + + this.setMissingOrcidAuthorizations(); + + this.researcherProfileService.onlyAdminCanDisconnectProfileFromOrcid().subscribe((result) => { + this.onlyAdminCanDisconnectProfileFromOrcid$.next(result); + }); + + this.researcherProfileService.ownerCanDisconnectProfileFromOrcid().subscribe((result) => { + this.ownerCanDisconnectProfileFromOrcid$.next(result); + }); + + this.isOrcidLinked$.next(this.researcherProfileService.isLinkedToOrcid(this.item)); + } + + private setMissingOrcidAuthorizations(): void { + const profileScopes = this.researcherProfileService.getOrcidAuthorizationScopesByItem(this.item); + const orcidScopes = this.orcidAuthorizationScopes.value; + const missingScopes = orcidScopes.filter((scope) => !profileScopes.includes(scope)); + + this.missingAuthorizationScopes.next(missingScopes); + } + + private setOrcidAuthorizationsFromItem(): void { + this.profileAuthorizationScopes.next(this.researcherProfileService.getOrcidAuthorizationScopesByItem(this.item)); + } + } diff --git a/src/app/item-page/orcid-page/orcid-page.component.html b/src/app/item-page/orcid-page/orcid-page.component.html index 91c49596b6..1921db3e92 100644 --- a/src/app/item-page/orcid-page/orcid-page.component.html +++ b/src/app/item-page/orcid-page/orcid-page.component.html @@ -1,4 +1,4 @@ -
+
- - + + diff --git a/src/app/item-page/orcid-page/orcid-page.component.spec.ts b/src/app/item-page/orcid-page/orcid-page.component.spec.ts index 1d61f18c3e..9210d56865 100644 --- a/src/app/item-page/orcid-page/orcid-page.component.spec.ts +++ b/src/app/item-page/orcid-page/orcid-page.component.spec.ts @@ -1,11 +1,13 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { By } from '@angular/platform-browser'; import { of as observableOf } from 'rxjs'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TestScheduler } from 'rxjs/testing'; +import { getTestScheduler } from 'jasmine-marbles'; import { AuthService } from '../../core/auth/auth.service'; import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; @@ -15,14 +17,16 @@ import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } f import { Item } from '../../core/shared/item.model'; import { createPaginatedList } from '../../shared/testing/utils.test'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; +import { ItemDataService } from '../../core/data/item-data.service'; -fdescribe('OrcidPageComponent test suite', () => { +describe('OrcidPageComponent test suite', () => { let comp: OrcidPageComponent; let fixture: ComponentFixture; - + let scheduler: TestScheduler; let authService: jasmine.SpyObj; let routeStub: jasmine.SpyObj; let routeData: any; + let itemDataService: jasmine.SpyObj; let researcherProfileService: jasmine.SpyObj; const mockItem: Item = Object.assign(new Item(), { @@ -70,6 +74,10 @@ fdescribe('OrcidPageComponent test suite', () => { isLinkedToOrcid: jasmine.createSpy('isLinkedToOrcid') }); + itemDataService = jasmine.createSpyObj('ItemDataService', { + findById: jasmine.createSpy('findById') + }); + void TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot({ @@ -85,6 +93,7 @@ fdescribe('OrcidPageComponent test suite', () => { { provide: ActivatedRoute, useValue: routeStub }, { provide: ResearcherProfileService, useValue: researcherProfileService }, { provide: AuthService, useValue: authService }, + { provide: ItemDataService, useValue: itemDataService }, ], schemas: [NO_ERRORS_SCHEMA] @@ -92,6 +101,7 @@ fdescribe('OrcidPageComponent test suite', () => { })); beforeEach(waitForAsync(() => { + scheduler = getTestScheduler(); fixture = TestBed.createComponent(OrcidPageComponent); comp = fixture.componentInstance; authService.isAuthenticated.and.returnValue(observableOf(true)); @@ -107,7 +117,15 @@ fdescribe('OrcidPageComponent test suite', () => { it('should call isLinkedToOrcid', () => { comp.isLinkedToOrcid(); - expect(researcherProfileService.isLinkedToOrcid).toHaveBeenCalledWith(comp.item); + expect(researcherProfileService.isLinkedToOrcid).toHaveBeenCalledWith(comp.item.value); }); + it('should update item', fakeAsync(() => { + itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid)); + scheduler.schedule(() => comp.updateItem()); + scheduler.flush(); + + expect(comp.item.value).toEqual(mockItemLinkedToOrcid); + })); + }); diff --git a/src/app/item-page/orcid-page/orcid-page.component.ts b/src/app/item-page/orcid-page/orcid-page.component.ts index 122c199f61..be4e0e7945 100644 --- a/src/app/item-page/orcid-page/orcid-page.component.ts +++ b/src/app/item-page/orcid-page/orcid-page.component.ts @@ -1,15 +1,17 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; +import { BehaviorSubject } from 'rxjs'; import { map } from 'rxjs/operators'; import { ResearcherProfileService } from '../../core/profile/researcher-profile.service'; -import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; import { getItemPageRoute } from '../item-page-routing-paths'; import { AuthService } from '../../core/auth/auth.service'; import { redirectOn4xx } from '../../core/shared/authorized.operators'; +import { ItemDataService } from '../../core/data/item-data.service'; /** * A component that represents the orcid settings page @@ -24,10 +26,11 @@ export class OrcidPageComponent implements OnInit { /** * The item for which showing the orcid settings */ - item: Item; + item: BehaviorSubject = new BehaviorSubject(null); constructor( private authService: AuthService, + private itemService: ItemDataService, private researcherProfileService: ResearcherProfileService, private route: ActivatedRoute, private router: Router @@ -43,7 +46,7 @@ export class OrcidPageComponent implements OnInit { redirectOn4xx(this.router, this.authService), getFirstSucceededRemoteDataPayload() ).subscribe((item) => { - this.item = item; + this.item.next(item); }); } @@ -53,14 +56,27 @@ export class OrcidPageComponent implements OnInit { * @returns the check result */ isLinkedToOrcid(): boolean { - return this.researcherProfileService.isLinkedToOrcid(this.item); + return this.researcherProfileService.isLinkedToOrcid(this.item.value); } /** * Get the route to an item's page */ getItemPage(): string { - return getItemPageRoute(this.item); + return getItemPageRoute(this.item.value); + } + + /** + * Retrieve the updated profile item + */ + updateItem(): void { + this.itemService.findById(this.item.value.id, false).pipe( + getFirstCompletedRemoteData() + ).subscribe((itemRD: RemoteData) => { + if (itemRD.hasSucceeded) { + this.item.next(itemRD.payload); + } + }); } } From 10f4f80f0de7e33afc3c80824f52849193ba7593 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 10 Jun 2022 19:27:40 +0200 Subject: [PATCH 17/29] [CST-5668] Create and abstract component for authentication with external providers --- .../log-in-external-provider.component.ts | 110 ++++++++++++++++++ .../methods/oidc/log-in-oidc.component.ts | 99 +--------------- .../methods/orcid/log-in-orcid.component.ts | 99 +--------------- .../shibboleth/log-in-shibboleth.component.ts | 102 +--------------- 4 files changed, 125 insertions(+), 285 deletions(-) create mode 100644 src/app/shared/log-in/methods/log-in-external-provider.component.ts diff --git a/src/app/shared/log-in/methods/log-in-external-provider.component.ts b/src/app/shared/log-in/methods/log-in-external-provider.component.ts new file mode 100644 index 0000000000..037fc40e90 --- /dev/null +++ b/src/app/shared/log-in/methods/log-in-external-provider.component.ts @@ -0,0 +1,110 @@ +import { Component, Inject, OnInit, } from '@angular/core'; + +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { select, Store } from '@ngrx/store'; + +import { AuthMethod } from '../../../core/auth/models/auth.method'; + +import { isAuthenticated, isAuthenticationLoading } from '../../../core/auth/selectors'; +import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service'; +import { isEmpty, isNotNull } from '../../empty.util'; +import { AuthService } from '../../../core/auth/auth.service'; +import { HardRedirectService } from '../../../core/services/hard-redirect.service'; +import { URLCombiner } from '../../../core/url-combiner/url-combiner'; +import { CoreState } from '../../../core/core-state.model'; + +@Component({ + selector: 'ds-log-in-external-provider', + template: '' + +}) +export abstract class LogInExternalProviderComponent implements OnInit { + + /** + * The authentication method data. + * @type {AuthMethod} + */ + public authMethod: AuthMethod; + + /** + * True if the authentication is loading. + * @type {boolean} + */ + public loading: Observable; + + /** + * The shibboleth authentication location url. + * @type {string} + */ + public location: string; + + /** + * Whether user is authenticated. + * @type {Observable} + */ + public isAuthenticated: Observable; + + /** + * @constructor + * @param {AuthMethod} injectedAuthMethodModel + * @param {boolean} isStandalonePage + * @param {NativeWindowRef} _window + * @param {AuthService} authService + * @param {HardRedirectService} hardRedirectService + * @param {Store} store + */ + constructor( + @Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod, + @Inject('isStandalonePage') public isStandalonePage: boolean, + @Inject(NativeWindowService) protected _window: NativeWindowRef, + private authService: AuthService, + private hardRedirectService: HardRedirectService, + private store: Store + ) { + this.authMethod = injectedAuthMethodModel; + } + + ngOnInit(): void { + // set isAuthenticated + this.isAuthenticated = this.store.pipe(select(isAuthenticated)); + + // set loading + this.loading = this.store.pipe(select(isAuthenticationLoading)); + + // set location + this.location = decodeURIComponent(this.injectedAuthMethodModel.location); + + } + + /** + * Redirect to the external provider url for login + */ + redirectToExternalProvider() { + this.authService.getRedirectUrl().pipe(take(1)).subscribe((redirectRoute) => { + if (!this.isStandalonePage) { + redirectRoute = this.hardRedirectService.getCurrentRoute(); + } else if (isEmpty(redirectRoute)) { + redirectRoute = '/'; + } + const correctRedirectUrl = new URLCombiner(this._window.nativeWindow.origin, redirectRoute).toString(); + + let externalServerUrl = this.location; + const myRegexp = /\?redirectUrl=(.*)/g; + const match = myRegexp.exec(this.location); + const redirectUrlFromServer = (match && match[1]) ? match[1] : null; + + // Check whether the current page is different from the redirect url received from rest + if (isNotNull(redirectUrlFromServer) && redirectUrlFromServer !== correctRedirectUrl) { + // change the redirect url with the current page url + const newRedirectUrl = `?redirectUrl=${correctRedirectUrl}`; + externalServerUrl = this.location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl); + } + + // redirect to shibboleth authentication url + this.hardRedirectService.redirect(externalServerUrl); + }); + + } + +} diff --git a/src/app/shared/log-in/methods/oidc/log-in-oidc.component.ts b/src/app/shared/log-in/methods/oidc/log-in-oidc.component.ts index 38cedf91ec..882996b207 100644 --- a/src/app/shared/log-in/methods/oidc/log-in-oidc.component.ts +++ b/src/app/shared/log-in/methods/oidc/log-in-oidc.component.ts @@ -1,110 +1,21 @@ -import { Component, Inject, OnInit, } from '@angular/core'; - -import { Observable } from 'rxjs'; -import { select, Store } from '@ngrx/store'; +import { Component, } from '@angular/core'; import { renderAuthMethodFor } from '../log-in.methods-decorator'; import { AuthMethodType } from '../../../../core/auth/models/auth.method-type'; -import { AuthMethod } from '../../../../core/auth/models/auth.method'; - -import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors'; -import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service'; -import { isNotNull, isEmpty } from '../../../empty.util'; -import { AuthService } from '../../../../core/auth/auth.service'; -import { HardRedirectService } from '../../../../core/services/hard-redirect.service'; -import { take } from 'rxjs/operators'; -import { URLCombiner } from '../../../../core/url-combiner/url-combiner'; -import { CoreState } from '../../../../core/core-state.model'; +import { LogInExternalProviderComponent } from '../log-in-external-provider.component'; @Component({ selector: 'ds-log-in-oidc', templateUrl: './log-in-oidc.component.html', }) @renderAuthMethodFor(AuthMethodType.Oidc) -export class LogInOidcComponent implements OnInit { +export class LogInOidcComponent extends LogInExternalProviderComponent { /** - * The authentication method data. - * @type {AuthMethod} + * Redirect to orcid authentication url */ - public authMethod: AuthMethod; - - /** - * True if the authentication is loading. - * @type {boolean} - */ - public loading: Observable; - - /** - * The oidc authentication location url. - * @type {string} - */ - public location: string; - - /** - * Whether user is authenticated. - * @type {Observable} - */ - public isAuthenticated: Observable; - - /** - * @constructor - * @param {AuthMethod} injectedAuthMethodModel - * @param {boolean} isStandalonePage - * @param {NativeWindowRef} _window - * @param {AuthService} authService - * @param {HardRedirectService} hardRedirectService - * @param {Store} store - */ - constructor( - @Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod, - @Inject('isStandalonePage') public isStandalonePage: boolean, - @Inject(NativeWindowService) protected _window: NativeWindowRef, - private authService: AuthService, - private hardRedirectService: HardRedirectService, - private store: Store - ) { - this.authMethod = injectedAuthMethodModel; - } - - ngOnInit(): void { - // set isAuthenticated - this.isAuthenticated = this.store.pipe(select(isAuthenticated)); - - // set loading - this.loading = this.store.pipe(select(isAuthenticationLoading)); - - // set location - this.location = decodeURIComponent(this.injectedAuthMethodModel.location); - - } - redirectToOidc() { - - this.authService.getRedirectUrl().pipe(take(1)).subscribe((redirectRoute) => { - if (!this.isStandalonePage) { - redirectRoute = this.hardRedirectService.getCurrentRoute(); - } else if (isEmpty(redirectRoute)) { - redirectRoute = '/'; - } - const correctRedirectUrl = new URLCombiner(this._window.nativeWindow.origin, redirectRoute).toString(); - - let oidcServerUrl = this.location; - const myRegexp = /\?redirectUrl=(.*)/g; - const match = myRegexp.exec(this.location); - const redirectUrlFromServer = (match && match[1]) ? match[1] : null; - - // Check whether the current page is different from the redirect url received from rest - if (isNotNull(redirectUrlFromServer) && redirectUrlFromServer !== correctRedirectUrl) { - // change the redirect url with the current page url - const newRedirectUrl = `?redirectUrl=${correctRedirectUrl}`; - oidcServerUrl = this.location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl); - } - - // redirect to oidc authentication url - this.hardRedirectService.redirect(oidcServerUrl); - }); - + this.redirectToExternalProvider(); } } diff --git a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts b/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts index 2181f3db20..e0b1da3db5 100644 --- a/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts +++ b/src/app/shared/log-in/methods/orcid/log-in-orcid.component.ts @@ -1,110 +1,21 @@ -import { Component, Inject, OnInit, } from '@angular/core'; - -import { Observable } from 'rxjs'; -import { select, Store } from '@ngrx/store'; +import { Component, } from '@angular/core'; import { renderAuthMethodFor } from '../log-in.methods-decorator'; import { AuthMethodType } from '../../../../core/auth/models/auth.method-type'; -import { AuthMethod } from '../../../../core/auth/models/auth.method'; - -import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors'; -import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service'; -import { isNotNull, isEmpty } from '../../../empty.util'; -import { AuthService } from '../../../../core/auth/auth.service'; -import { HardRedirectService } from '../../../../core/services/hard-redirect.service'; -import { take } from 'rxjs/operators'; -import { URLCombiner } from '../../../../core/url-combiner/url-combiner'; -import {CoreState} from '../../../../core/core-state.model'; +import { LogInExternalProviderComponent } from '../log-in-external-provider.component'; @Component({ selector: 'ds-log-in-orcid', templateUrl: './log-in-orcid.component.html', }) @renderAuthMethodFor(AuthMethodType.Orcid) -export class LogInOrcidComponent implements OnInit { +export class LogInOrcidComponent extends LogInExternalProviderComponent { /** - * The authentication method data. - * @type {AuthMethod} + * Redirect to orcid authentication url */ - public authMethod: AuthMethod; - - /** - * True if the authentication is loading. - * @type {boolean} - */ - public loading: Observable; - - /** - * The orcid authentication location url. - * @type {string} - */ - public location: string; - - /** - * Whether user is authenticated. - * @type {Observable} - */ - public isAuthenticated: Observable; - - /** - * @constructor - * @param {AuthMethod} injectedAuthMethodModel - * @param {boolean} isStandalonePage - * @param {NativeWindowRef} _window - * @param {AuthService} authService - * @param {HardRedirectService} hardRedirectService - * @param {Store} store - */ - constructor( - @Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod, - @Inject('isStandalonePage') public isStandalonePage: boolean, - @Inject(NativeWindowService) protected _window: NativeWindowRef, - private authService: AuthService, - private hardRedirectService: HardRedirectService, - private store: Store - ) { - this.authMethod = injectedAuthMethodModel; - } - - ngOnInit(): void { - // set isAuthenticated - this.isAuthenticated = this.store.pipe(select(isAuthenticated)); - - // set loading - this.loading = this.store.pipe(select(isAuthenticationLoading)); - - // set location - this.location = decodeURIComponent(this.injectedAuthMethodModel.location); - - } - redirectToOrcid() { - - this.authService.getRedirectUrl().pipe(take(1)).subscribe((redirectRoute) => { - if (!this.isStandalonePage) { - redirectRoute = this.hardRedirectService.getCurrentRoute(); - } else if (isEmpty(redirectRoute)) { - redirectRoute = '/'; - } - const correctRedirectUrl = new URLCombiner(this._window.nativeWindow.origin, redirectRoute).toString(); - - let orcidServerUrl = this.location; - const myRegexp = /\?redirectUrl=(.*)/g; - const match = myRegexp.exec(this.location); - const redirectUrlFromServer = (match && match[1]) ? match[1] : null; - - // Check whether the current page is different from the redirect url received from rest - if (isNotNull(redirectUrlFromServer) && redirectUrlFromServer !== correctRedirectUrl) { - // change the redirect url with the current page url - const newRedirectUrl = `?redirectUrl=${correctRedirectUrl}`; - orcidServerUrl = this.location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl); - } - - // redirect to orcid authentication url - this.hardRedirectService.redirect(orcidServerUrl); - }); - + this.redirectToExternalProvider(); } } diff --git a/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.ts b/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.ts index d218a7ca4e..dcfb3ccfc3 100644 --- a/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.ts +++ b/src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.ts @@ -1,21 +1,8 @@ -import { Component, Inject, OnInit, } from '@angular/core'; - -import { Observable } from 'rxjs'; -import { select, Store } from '@ngrx/store'; +import { Component, } from '@angular/core'; import { renderAuthMethodFor } from '../log-in.methods-decorator'; import { AuthMethodType } from '../../../../core/auth/models/auth.method-type'; -import { AuthMethod } from '../../../../core/auth/models/auth.method'; - -import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/selectors'; -import { RouteService } from '../../../../core/services/route.service'; -import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service'; -import { isNotNull, isEmpty } from '../../../empty.util'; -import { AuthService } from '../../../../core/auth/auth.service'; -import { HardRedirectService } from '../../../../core/services/hard-redirect.service'; -import { take } from 'rxjs/operators'; -import { URLCombiner } from '../../../../core/url-combiner/url-combiner'; -import { CoreState } from '../../../../core/core-state.model'; +import { LogInExternalProviderComponent } from '../log-in-external-provider.component'; @Component({ selector: 'ds-log-in-shibboleth', @@ -24,92 +11,13 @@ import { CoreState } from '../../../../core/core-state.model'; }) @renderAuthMethodFor(AuthMethodType.Shibboleth) -export class LogInShibbolethComponent implements OnInit { +export class LogInShibbolethComponent extends LogInExternalProviderComponent { /** - * The authentication method data. - * @type {AuthMethod} + * Redirect to shibboleth authentication url */ - public authMethod: AuthMethod; - - /** - * True if the authentication is loading. - * @type {boolean} - */ - public loading: Observable; - - /** - * The shibboleth authentication location url. - * @type {string} - */ - public location: string; - - /** - * Whether user is authenticated. - * @type {Observable} - */ - public isAuthenticated: Observable; - - /** - * @constructor - * @param {AuthMethod} injectedAuthMethodModel - * @param {boolean} isStandalonePage - * @param {NativeWindowRef} _window - * @param {RouteService} route - * @param {AuthService} authService - * @param {HardRedirectService} hardRedirectService - * @param {Store} store - */ - constructor( - @Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod, - @Inject('isStandalonePage') public isStandalonePage: boolean, - @Inject(NativeWindowService) protected _window: NativeWindowRef, - private route: RouteService, - private authService: AuthService, - private hardRedirectService: HardRedirectService, - private store: Store - ) { - this.authMethod = injectedAuthMethodModel; - } - - ngOnInit(): void { - // set isAuthenticated - this.isAuthenticated = this.store.pipe(select(isAuthenticated)); - - // set loading - this.loading = this.store.pipe(select(isAuthenticationLoading)); - - // set location - this.location = decodeURIComponent(this.injectedAuthMethodModel.location); - - } - redirectToShibboleth() { - - this.authService.getRedirectUrl().pipe(take(1)).subscribe((redirectRoute) => { - if (!this.isStandalonePage) { - redirectRoute = this.hardRedirectService.getCurrentRoute(); - } else if (isEmpty(redirectRoute)) { - redirectRoute = '/'; - } - const correctRedirectUrl = new URLCombiner(this._window.nativeWindow.origin, redirectRoute).toString(); - - let shibbolethServerUrl = this.location; - const myRegexp = /\?redirectUrl=(.*)/g; - const match = myRegexp.exec(this.location); - const redirectUrlFromServer = (match && match[1]) ? match[1] : null; - - // Check whether the current page is different from the redirect url received from rest - if (isNotNull(redirectUrlFromServer) && redirectUrlFromServer !== correctRedirectUrl) { - // change the redirect url with the current page url - const newRedirectUrl = `?redirectUrl=${correctRedirectUrl}`; - shibbolethServerUrl = this.location.replace(/\?redirectUrl=(.*)/g, newRedirectUrl); - } - - // redirect to shibboleth authentication url - this.hardRedirectService.redirect(shibbolethServerUrl); - }); - + this.redirectToExternalProvider(); } } From 0b0fae45fa3417094f56ede7a4286abeeba6d726 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 13 Jun 2022 09:27:47 +0200 Subject: [PATCH 18/29] [CST-5668] Fix layout and add typedoc --- .../orcid-sync/orcid-setting.component.html | 144 ++++++++++-------- .../orcid-sync/orcid-setting.component.ts | 73 +++++++-- 2 files changed, 139 insertions(+), 78 deletions(-) diff --git a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.html b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.html index 793e7570ed..563ddba699 100644 --- a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.html +++ b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.html @@ -4,71 +4,85 @@
-
+ {{ 'person.page.orcid.synchronization-mode-message' | translate}} -
-
-
-
{{ 'person.page.orcid.synchronization-mode'| translate }}
-
-
-
- - -
-
-
-
-
-
-
-
{{ 'person.page.orcid.publications-preferences'| translate }}
-
-
-
-
- - -
-
-
-
-
-
-
{{ 'person.page.orcid.funding-preferences'| translate }}
-
-
-
-
- - -
-
-
-
-
-
-
{{ 'person.page.orcid.profile-preferences'| translate }}
-
-
-
-
- - -
-
-
-
-
-
-
+ +
-
+
+
+
+
+
{{ 'person.page.orcid.publications-preferences'| translate }}
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
{{ 'person.page.orcid.funding-preferences'| translate }}
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
{{ 'person.page.orcid.profile-preferences'| translate }}
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+ @@ -78,5 +92,5 @@
- -
\ No newline at end of file + +
diff --git a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts index 2429ac2e43..74b96f8859 100644 --- a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts +++ b/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts @@ -26,30 +26,57 @@ export class OrcidSettingComponent implements OnInit { */ @Input() item: Item; + /** + * The prefix used for i18n keys + */ messagePrefix = 'person.page.orcid'; + /** + * The current synchronization mode + */ currentSyncMode: string; + /** + * The current synchronization mode for publications + */ currentSyncPublications: string; - currentSyncFundings: string; + /** + * The current synchronization mode for funding + */ + currentSyncFunding: string; + /** + * The synchronization options + */ syncModes: { value: string, label: string }[]; + /** + * The synchronization options for publications + */ syncPublicationOptions: { value: string, label: string }[]; - syncFundingOptions: {value: string, label: string}[]; + /** + * The synchronization options for funding + */ + syncFundingOptions: { value: string, label: string }[]; + /** + * The profile synchronization options + */ syncProfileOptions: { value: string, label: string, checked: boolean }[]; constructor(private researcherProfileService: ResearcherProfileService, - protected translateService: TranslateService, - private notificationsService: NotificationsService, - public authService: AuthService + protected translateService: TranslateService, + private notificationsService: NotificationsService, + public authService: AuthService ) { } + /** + * Init orcid settings form + */ ngOnInit() { this.syncModes = [ { @@ -91,10 +118,15 @@ export class OrcidSettingComponent implements OnInit { this.currentSyncMode = this.getCurrentPreference('dspace.orcid.sync-mode', ['BATCH', 'MANUAL'], 'MANUAL'); this.currentSyncPublications = this.getCurrentPreference('dspace.orcid.sync-publications', ['DISABLED', 'ALL'], 'DISABLED'); - this.currentSyncFundings = this.getCurrentPreference('dspace.orcid.sync-fundings', ['DISABLED', 'ALL'], 'DISABLED'); + this.currentSyncFunding = this.getCurrentPreference('dspace.orcid.sync-fundings', ['DISABLED', 'ALL'], 'DISABLED'); } - onSubmit(form: FormGroup) { + /** + * Generate path operations to save orcid synchronization preferences + * + * @param form The form group + */ + onSubmit(form: FormGroup): void { const operations: Operation[] = []; this.fillOperationsFor(operations, '/orcid/mode', form.value.syncMode); this.fillOperationsFor(operations, '/orcid/publications', form.value.syncPublications); @@ -131,7 +163,27 @@ export class OrcidSettingComponent implements OnInit { }); } - fillOperationsFor(operations: Operation[], path: string, currentValue: string) { + /** + * Retrieve setting saved in the item's metadata + * + * @param metadataField The metadata name that contains setting + * @param allowedValues The allowed values + * @param defaultValue The default value + * @private + */ + private getCurrentPreference(metadataField: string, allowedValues: string[], defaultValue: string): string { + const currentPreference = this.item.firstMetadataValue(metadataField); + return (currentPreference && allowedValues.includes(currentPreference)) ? currentPreference : defaultValue; + } + + /** + * Generate a replace patch operation + * + * @param operations + * @param path + * @param currentValue + */ + private fillOperationsFor(operations: Operation[], path: string, currentValue: string): void { operations.push({ path: path, op: 'replace', @@ -139,9 +191,4 @@ export class OrcidSettingComponent implements OnInit { }); } - getCurrentPreference(metadataField: string, allowedValues: string[], defaultValue: string): string { - const currentPreference = this.item.firstMetadataValue(metadataField); - return (currentPreference && allowedValues.includes(currentPreference)) ? currentPreference : defaultValue; - } - } From 98f1baea2fb894dbdacbaf3450363c68d09153b0 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 13 Jun 2022 09:32:24 +0200 Subject: [PATCH 19/29] [CST-5668] Rename component --- src/app/item-page/item-page.module.ts | 24 +++++++++++++------ .../orcid-page/orcid-page.component.html | 2 +- .../orcid-sync-settings.component.html} | 0 .../orcid-sync-settings.component.scss} | 0 .../orcid-sync-settings.component.ts} | 8 +++---- 5 files changed, 22 insertions(+), 12 deletions(-) rename src/app/item-page/orcid-page/{orcid-sync/orcid-setting.component.html => orcid-sync-settings/orcid-sync-settings.component.html} (100%) rename src/app/item-page/orcid-page/{orcid-sync/orcid-setting.component.scss => orcid-sync-settings/orcid-sync-settings.component.scss} (100%) rename src/app/item-page/orcid-page/{orcid-sync/orcid-setting.component.ts => orcid-sync-settings/orcid-sync-settings.component.ts} (96%) diff --git a/src/app/item-page/item-page.module.ts b/src/app/item-page/item-page.module.ts index 2c4b57b249..d1be5a9cd8 100644 --- a/src/app/item-page/item-page.module.ts +++ b/src/app/item-page/item-page.module.ts @@ -6,11 +6,19 @@ import { SharedModule } from '../shared/shared.module'; import { ItemPageComponent } from './simple/item-page.component'; import { ItemPageRoutingModule } from './item-page-routing.module'; import { MetadataUriValuesComponent } from './field-components/metadata-uri-values/metadata-uri-values.component'; -import { ItemPageAuthorFieldComponent } from './simple/field-components/specific-field/author/item-page-author-field.component'; -import { ItemPageDateFieldComponent } from './simple/field-components/specific-field/date/item-page-date-field.component'; -import { ItemPageAbstractFieldComponent } from './simple/field-components/specific-field/abstract/item-page-abstract-field.component'; +import { + ItemPageAuthorFieldComponent +} from './simple/field-components/specific-field/author/item-page-author-field.component'; +import { + ItemPageDateFieldComponent +} from './simple/field-components/specific-field/date/item-page-date-field.component'; +import { + ItemPageAbstractFieldComponent +} from './simple/field-components/specific-field/abstract/item-page-abstract-field.component'; import { ItemPageUriFieldComponent } from './simple/field-components/specific-field/uri/item-page-uri-field.component'; -import { ItemPageTitleFieldComponent } from './simple/field-components/specific-field/title/item-page-title-field.component'; +import { + ItemPageTitleFieldComponent +} from './simple/field-components/specific-field/title/item-page-title-field.component'; import { ItemPageFieldComponent } from './simple/field-components/specific-field/item-page-field.component'; import { CollectionsComponent } from './field-components/collections/collections.component'; import { FullItemPageComponent } from './full/full-item-page.component'; @@ -20,7 +28,9 @@ import { ItemComponent } from './simple/item-types/shared/item.component'; import { EditItemPageModule } from './edit-item-page/edit-item-page.module'; import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component'; import { StatisticsModule } from '../statistics/statistics.module'; -import { AbstractIncrementalListComponent } from './simple/abstract-incremental-list/abstract-incremental-list.component'; +import { + AbstractIncrementalListComponent +} from './simple/abstract-incremental-list/abstract-incremental-list.component'; import { UntypedItemComponent } from './simple/item-types/untyped-item/untyped-item.component'; import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module'; import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module'; @@ -37,7 +47,7 @@ import { ThemedFileSectionComponent } from './simple/field-components/file-secti import { OrcidAuthComponent } from './orcid-page/orcid-auth/orcid-auth.component'; import { OrcidPageComponent } from './orcid-page/orcid-page.component'; import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; -import { OrcidSettingComponent } from './orcid-page/orcid-sync/orcid-setting.component'; +import { OrcidSyncSettingsComponent } from './orcid-page/orcid-sync-settings/orcid-sync-settings.component'; const ENTRY_COMPONENTS = [ @@ -73,7 +83,7 @@ const DECLARATIONS = [ VersionPageComponent, OrcidPageComponent, OrcidAuthComponent, - OrcidSettingComponent + OrcidSyncSettingsComponent ]; @NgModule({ diff --git a/src/app/item-page/orcid-page/orcid-page.component.html b/src/app/item-page/orcid-page/orcid-page.component.html index 1921db3e92..6b49c4d224 100644 --- a/src/app/item-page/orcid-page/orcid-page.component.html +++ b/src/app/item-page/orcid-page/orcid-page.component.html @@ -9,4 +9,4 @@
- + diff --git a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.html b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.html similarity index 100% rename from src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.html rename to src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.html diff --git a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.scss b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.scss similarity index 100% rename from src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.scss rename to src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.scss diff --git a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts similarity index 96% rename from src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts rename to src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts index 74b96f8859..111c7a08df 100644 --- a/src/app/item-page/orcid-page/orcid-sync/orcid-setting.component.ts +++ b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts @@ -15,11 +15,11 @@ import { NotificationsService } from '../../../shared/notifications/notification import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model'; @Component({ - selector: 'ds-orcid-setting', - templateUrl: './orcid-setting.component.html', - styleUrls: ['./orcid-setting.component.scss'] + selector: 'ds-orcid-sync-setting', + templateUrl: './orcid-sync-settings.component.html', + styleUrls: ['./orcid-sync-settings.component.scss'] }) -export class OrcidSettingComponent implements OnInit { +export class OrcidSyncSettingsComponent implements OnInit { /** * The item for which showing the orcid settings From a915659cc962fa553ab1fd718743954a69a16b51 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 13 Jun 2022 10:39:44 +0200 Subject: [PATCH 20/29] [CST-5668] Add test --- .../orcid-sync-settings.component.html | 8 +- .../orcid-sync-settings.component.spec.ts | 257 ++++++++++++++++++ .../orcid-sync-settings.component.ts | 5 +- 3 files changed, 262 insertions(+), 8 deletions(-) create mode 100644 src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts diff --git a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.html b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.html index 563ddba699..6487e78476 100644 --- a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.html +++ b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.html @@ -9,7 +9,7 @@
-
+
{{ 'person.page.orcid.synchronization-mode'| translate }}
@@ -28,7 +28,7 @@
-
+
{{ 'person.page.orcid.publications-preferences'| translate }}
@@ -46,7 +46,7 @@
-
+
{{ 'person.page.orcid.funding-preferences'| translate }}
@@ -63,7 +63,7 @@
-
+
{{ 'person.page.orcid.profile-preferences'| translate }}
diff --git a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts new file mode 100644 index 0000000000..ccc00178cb --- /dev/null +++ b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts @@ -0,0 +1,257 @@ +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; + +import { getTestScheduler } from 'jasmine-marbles'; +import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; +import { TestScheduler } from 'rxjs/testing'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { Operation } from 'fast-json-patch'; + +import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service'; +import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { Item } from '../../../core/shared/item.model'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; +import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { OrcidSyncSettingsComponent } from './orcid-sync-settings.component'; +import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model'; + +describe('OrcidAuthComponent test suite', () => { + let comp: OrcidSyncSettingsComponent; + let fixture: ComponentFixture; + let scheduler: TestScheduler; + let researcherProfileService: jasmine.SpyObj; + let notificationsService; + let formGroup: FormGroup; + + const mockResearcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), { + id: 'test-id', + visible: true, + type: 'profile', + _links: { + item: { + href: 'https://rest.api/rest/api/profiles/test-id/item' + }, + self: { + href: 'https://rest.api/rest/api/profiles/test-id' + }, + } + }); + + const mockItemLinkedToOrcid: Item = Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: { + 'dc.title': [{ + value: 'test person' + }], + 'dspace.entity.type': [{ + 'value': 'Person' + }], + 'dspace.object.owner': [{ + 'value': 'test person', + 'language': null, + 'authority': 'deced3e7-68e2-495d-bf98-7c44fc33b8ff', + 'confidence': 600, + 'place': 0 + }], + 'dspace.orcid.authenticated': [{ + 'value': '2022-06-10T15:15:12.952872', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }], + 'dspace.orcid.scope': [{ + 'value': '/authenticate', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }, { + 'value': '/read-limited', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 1 + }, { + 'value': '/activities/update', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 2 + }, { + 'value': '/person/update', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 3 + }], + 'dspace.orcid.sync-mode': [{ + 'value': 'MANUAL', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }], + 'dspace.orcid.sync-profile': [{ + 'value': 'BIOGRAPHICAL', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }, { + 'value': 'IDENTIFIERS', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 1 + }], + 'dspace.orcid.sync-publications': [{ + 'value': 'ALL', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }], + 'person.identifier.orcid': [{ + 'value': 'orcid-id', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0 + }] + } + }); + + beforeEach(waitForAsync(() => { + researcherProfileService = jasmine.createSpyObj('researcherProfileService', { + findByRelatedItem: jasmine.createSpy('findByRelatedItem'), + updateByOrcidOperations: jasmine.createSpy('updateByOrcidOperations') + }); + + void TestBed.configureTestingModule({ + imports: [ + FormsModule, + NgbAccordionModule, + ReactiveFormsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + RouterTestingModule.withRoutes([]) + ], + declarations: [OrcidSyncSettingsComponent], + providers: [ + { provide: NotificationsService, useClass: NotificationsServiceStub }, + { provide: ResearcherProfileService, useValue: researcherProfileService } + ], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(OrcidSyncSettingsComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(waitForAsync(() => { + scheduler = getTestScheduler(); + fixture = TestBed.createComponent(OrcidSyncSettingsComponent); + comp = fixture.componentInstance; + comp.item = mockItemLinkedToOrcid; + fixture.detectChanges(); + })); + + it('should create cards properly', () => { + const modes = fixture.debugElement.query(By.css('[data-test="sync-mode"]')); + const publication = fixture.debugElement.query(By.css('[data-test="sync-mode-publication"]')); + const funding = fixture.debugElement.query(By.css('[data-test="sync-mode-funding"]')); + const preferences = fixture.debugElement.query(By.css('[data-test="profile-preferences"]')); + expect(modes).toBeTruthy(); + expect(publication).toBeTruthy(); + expect(funding).toBeTruthy(); + expect(preferences).toBeTruthy(); + }); + + it('should init sync modes properly', () => { + expect(comp.currentSyncMode).toBe('MANUAL'); + expect(comp.currentSyncPublications).toBe('ALL'); + expect(comp.currentSyncFunding).toBe('DISABLED'); + }); + + describe('form submit', () => { + beforeEach(() => { + scheduler = getTestScheduler(); + notificationsService = (comp as any).notificationsService; + formGroup = new FormGroup({ + syncMode: new FormControl('MANUAL'), + syncFundings: new FormControl('ALL'), + syncPublications: new FormControl('ALL'), + syncProfile_BIOGRAPHICAL: new FormControl(true), + syncProfile_IDENTIFIERS: new FormControl(true), + }); + }); + + it('should call updateByOrcidOperations properly', () => { + researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); + researcherProfileService.updateByOrcidOperations.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); + const expectedOps: Operation[] = [ + { + path: '/orcid/mode', + op: 'replace', + value: 'MANUAL' + }, { + path: '/orcid/publications', + op: 'replace', + value: 'ALL' + }, { + path: '/orcid/fundings', + op: 'replace', + value: 'ALL' + }, { + path: '/orcid/profile', + op: 'replace', + value: 'BIOGRAPHICAL,IDENTIFIERS' + } + ]; + + scheduler.schedule(() => comp.onSubmit(formGroup)); + scheduler.flush(); + + expect(researcherProfileService.updateByOrcidOperations).toHaveBeenCalledWith(mockResearcherProfile, expectedOps); + }); + + it('should show notification on success', () => { + researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); + researcherProfileService.updateByOrcidOperations.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); + + scheduler.schedule(() => comp.onSubmit(formGroup)); + scheduler.flush(); + + expect(notificationsService.success).toHaveBeenCalled(); + }); + + it('should show notification on error', () => { + researcherProfileService.findByRelatedItem.and.returnValue(createFailedRemoteDataObject$()); + + scheduler.schedule(() => comp.onSubmit(formGroup)); + scheduler.flush(); + + expect(notificationsService.error).toHaveBeenCalled(); + }); + + it('should show notification on error', () => { + researcherProfileService.findByRelatedItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile)); + researcherProfileService.updateByOrcidOperations.and.returnValue(createFailedRemoteDataObject$()); + + scheduler.schedule(() => comp.onSubmit(formGroup)); + scheduler.flush(); + + expect(notificationsService.error).toHaveBeenCalled(); + }); + }); + +}); diff --git a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts index 111c7a08df..5b5e13a1aa 100644 --- a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts +++ b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts @@ -6,7 +6,6 @@ import { Operation } from 'fast-json-patch'; import { of } from 'rxjs'; import { switchMap } from 'rxjs/operators'; -import { AuthService } from '../../../core/auth/auth.service'; import { RemoteData } from '../../../core/data/remote-data'; import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service'; import { Item } from '../../../core/shared/item.model'; @@ -68,10 +67,8 @@ export class OrcidSyncSettingsComponent implements OnInit { constructor(private researcherProfileService: ResearcherProfileService, - protected translateService: TranslateService, private notificationsService: NotificationsService, - public authService: AuthService - ) { + private translateService: TranslateService) { } /** From 9b6aa9f324d1631381c6102445581d7eee0d07dc Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 13 Jun 2022 14:55:17 +0200 Subject: [PATCH 21/29] [CST-5668] Improve synchronization description --- .../orcid-sync-settings.component.html | 8 +++++--- src/assets/i18n/en.json5 | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.html b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.html index 6487e78476..75038d5973 100644 --- a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.html +++ b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.html @@ -4,15 +4,17 @@
- - {{ 'person.page.orcid.synchronization-mode-message' | translate}} -
{{ 'person.page.orcid.synchronization-mode'| translate }}
+
+ + {{ 'person.page.orcid.synchronization-mode-message' | translate}} + +
-
+
{{ 'person.page.orcid.funding-preferences'| translate }}
+
+ + {{ 'person.page.orcid.synchronization-mode-funding-message' | translate}} + +
-
+
{{ 'person.page.orcid.profile-preferences'| translate }}
+
+ + {{ 'person.page.orcid.synchronization-mode-profile-message' | translate}} + +
Date: Tue, 14 Jun 2022 10:05:29 +0200 Subject: [PATCH 23/29] [CST-5668] Fix issue with notification css that override min-width for all alert boxes --- .../notifications/notification/notification.component.html | 2 +- .../notifications/notification/notification.component.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/shared/notifications/notification/notification.component.html b/src/app/shared/notifications/notification/notification.component.html index e8b3d37b5f..befa84cfc0 100644 --- a/src/app/shared/notifications/notification/notification.component.html +++ b/src/app/shared/notifications/notification/notification.component.html @@ -1,4 +1,4 @@ -