diff --git a/src/app/item-page/item-page-routing-paths.ts b/src/app/item-page/item-page-routing-paths.ts
index 74ad0aae07..90a4a54b1e 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';
diff --git a/src/app/item-page/item-page-routing.module.ts b/src/app/item-page/item-page-routing.module.ts
index 59dafd4d99..add2c3d768 100644
--- a/src/app/item-page/item-page-routing.module.ts
+++ b/src/app/item-page/item-page-routing.module.ts
@@ -7,15 +7,19 @@ 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 { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
import { MenuItemType } from '../shared/menu/menu-item-type.model';
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 {
+ 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 +54,11 @@ import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths';
{
path: REQUEST_COPY_MODULE_PATH,
component: BitstreamRequestACopyPageComponent,
+ },
+ {
+ path: ORCID_PATH,
+ component: OrcidPageComponent,
+ canActivate: [AuthenticatedGuard, OrcidPageGuard]
}
],
data: {
@@ -88,6 +97,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..cbb9f3299e 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';
@@ -34,6 +44,11 @@ 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';
+import { OrcidSyncSettingsComponent } from './orcid-page/orcid-sync-settings/orcid-sync-settings.component';
+import { OrcidQueueComponent } from './orcid-page/orcid-queue/orcid-queue.component';
const ENTRY_COMPONENTS = [
@@ -67,6 +82,10 @@ const DECLARATIONS = [
MediaViewerImageComponent,
MiradorViewerComponent,
VersionPageComponent,
+ OrcidPageComponent,
+ OrcidAuthComponent,
+ OrcidSyncSettingsComponent,
+ OrcidQueueComponent
];
@NgModule({
@@ -79,6 +98,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..e57ce33008
--- /dev/null
+++ b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html
@@ -0,0 +1,84 @@
+
+
{{'person.orcid.registry.auth' | translate}}
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+ {{getAuthorizationDescription(auth) | 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}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getOrcidNotLinkedMessage() | async }}
+
+
+
+
+
+
+
+
+
+
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.spec.ts b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.spec.ts
new file mode 100644
index 0000000000..e96e5996fb
--- /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 { OrcidAuthService } from '../../../core/orcid/orcid-auth.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 orcidAuthService: 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(() => {
+ orcidAuthService = 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'),
+ unlinkOrcidByItem: jasmine.createSpy('unlinkOrcidByItem')
+ });
+
+ 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: OrcidAuthService, useValue: orcidAuthService }
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ }).overrideComponent(OrcidAuthComponent, {
+ set: { changeDetection: ChangeDetectionStrategy.Default }
+ }).compileComponents();
+ }));
+
+ beforeEach(waitForAsync(() => {
+ scheduler = getTestScheduler();
+ fixture = TestBed.createComponent(OrcidAuthComponent);
+ comp = fixture.componentInstance;
+ orcidAuthService.getOrcidAuthorizationScopes.and.returnValue(of(orcidScopes));
+ }));
+
+ describe('when orcid profile is not linked', () => {
+ beforeEach(waitForAsync(() => {
+ comp.item = mockItemUnlinkedToOrcid;
+ orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([]);
+ orcidAuthService.isLinkedToOrcid.and.returnValue(false);
+ orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
+ orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
+ orcidAuthService.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;
+ orcidAuthService.isLinkedToOrcid.and.returnValue(true);
+ }));
+
+ describe('', () => {
+
+ beforeEach(waitForAsync(() => {
+ comp.item = mockItemLinkedToOrcid;
+ notificationsService = (comp as any).notificationsService;
+ orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
+ orcidAuthService.isLinkedToOrcid.and.returnValue(true);
+ orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
+ orcidAuthService.ownerCanDisconnectProfileFromOrcid.and.returnValue(of(true));
+ }));
+
+ describe('and unlink is successfully', () => {
+ beforeEach(waitForAsync(() => {
+ comp.item = mockItemLinkedToOrcid;
+ orcidAuthService.unlinkOrcidByItem.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;
+ orcidAuthService.unlinkOrcidByItem.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;
+ orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
+ orcidAuthService.isLinkedToOrcid.and.returnValue(true);
+ orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
+ orcidAuthService.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;
+ orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...partialOrcidScopes]);
+ orcidAuthService.isLinkedToOrcid.and.returnValue(true);
+ orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(false));
+ orcidAuthService.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;
+ orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
+ orcidAuthService.isLinkedToOrcid.and.returnValue(true);
+ orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(true));
+ orcidAuthService.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;
+ orcidAuthService.getOrcidAuthorizationScopesByItem.and.returnValue([...orcidScopes]);
+ orcidAuthService.isLinkedToOrcid.and.returnValue(true);
+ orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid.and.returnValue(of(true));
+ orcidAuthService.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
new file mode 100644
index 0000000000..ea970e7d31
--- /dev/null
+++ b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts
@@ -0,0 +1,218 @@
+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 { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service';
+import { Item } from '../../../core/shared/item.model';
+import { NotificationsService } from '../../../shared/notifications/notifications.service';
+import { RemoteData } from '../../../core/data/remote-data';
+import { ResearcherProfile } from '../../../core/profile/model/researcher-profile.model';
+import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
+import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
+
+@Component({
+ selector: 'ds-orcid-auth',
+ templateUrl: './orcid-auth.component.html',
+ styleUrls: ['./orcid-auth.component.scss']
+})
+export class OrcidAuthComponent implements OnInit, OnChanges {
+
+ /**
+ * The item for which showing the orcid settings
+ */
+ @Input() item: Item;
+
+ /**
+ * The list of exposed orcid authorization scopes for the orcid profile
+ */
+ profileAuthorizationScopes: BehaviorSubject = new BehaviorSubject([]);
+
+ /**
+ * 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 orcidAuthService: OrcidAuthService,
+ private translateService: TranslateService,
+ private notificationsService: NotificationsService,
+ @Inject(NativeWindowService) private _window: NativeWindowRef,
+ ) {
+ }
+
+ ngOnInit() {
+ this.orcidAuthService.getOrcidAuthorizationScopes().subscribe((scopes: string[]) => {
+ this.orcidAuthorizationScopes.next(scopes);
+ this.initOrcidAuthSettings();
+ });
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (!changes.item.isFirstChange() && changes.item.currentValue !== changes.item.previousValue) {
+ this.initOrcidAuthSettings();
+ }
+ }
+
+ /**
+ * 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 {
+ 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');
+ }
+ }
+
+ /**
+ * 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.onlyAdminCanDisconnectProfileFromOrcid$.asObservable();
+ }
+
+ /**
+ * Return a boolean representing if owner can disconnect orcid profile
+ */
+ ownerCanDisconnectProfileFromOrcid(): Observable {
+ return this.ownerCanDisconnectProfileFromOrcid$.asObservable();
+ }
+
+ /**
+ * Link existing person profile with orcid
+ */
+ linkOrcid(): void {
+ this.orcidAuthService.getOrcidAuthorizeUrl(this.item).subscribe((authorizeUrl) => {
+ this._window.nativeWindow.location.href = authorizeUrl;
+ });
+ }
+
+ /**
+ * Unlink existing person profile from orcid
+ */
+ unlinkOrcid(): void {
+ this.unlinkProcessing.next(true);
+ this.orcidAuthService.unlinkOrcidByItem(this.item).pipe(
+ getFirstCompletedRemoteData()
+ ).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.orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid().subscribe((result) => {
+ this.onlyAdminCanDisconnectProfileFromOrcid$.next(result);
+ });
+
+ this.orcidAuthService.ownerCanDisconnectProfileFromOrcid().subscribe((result) => {
+ this.ownerCanDisconnectProfileFromOrcid$.next(result);
+ });
+
+ this.isOrcidLinked$.next(this.orcidAuthService.isLinkedToOrcid(this.item));
+ }
+
+ private setMissingOrcidAuthorizations(): void {
+ const profileScopes = this.orcidAuthService.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.orcidAuthService.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
new file mode 100644
index 0000000000..33c3125d67
--- /dev/null
+++ b/src/app/item-page/orcid-page/orcid-page.component.html
@@ -0,0 +1,19 @@
+
+
+
+
+ {{'person.page.orcid.link.error.message' | translate}}
+
+
+
+
+
+
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.spec.ts b/src/app/item-page/orcid-page/orcid-page.component.spec.ts
new file mode 100644
index 0000000000..1ed237943e
--- /dev/null
+++ b/src/app/item-page/orcid-page/orcid-page.component.spec.ts
@@ -0,0 +1,220 @@
+import { NO_ERRORS_SCHEMA, PLATFORM_ID } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+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';
+import { OrcidPageComponent } from './orcid-page.component';
+import {
+ createFailedRemoteDataObject$,
+ 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';
+import { ItemDataService } from '../../core/data/item-data.service';
+import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model';
+import { OrcidAuthService } from '../../core/orcid/orcid-auth.service';
+
+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 orcidAuthService: jasmine.SpyObj;
+
+ 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 mockItem: Item = Object.assign(new Item(), {
+ id: 'test-id',
+ bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
+ metadata: {
+ 'dc.title': [
+ {
+ language: 'en_US',
+ value: 'test item'
+ }
+ ]
+ }
+ });
+ const mockItemLinkedToOrcid: Item = Object.assign(new Item(), {
+ id: 'test-id',
+ 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 = new ActivatedRouteStub({}, routeData);
+
+ orcidAuthService = jasmine.createSpyObj('OrcidAuthService', {
+ isLinkedToOrcid: jasmine.createSpy('isLinkedToOrcid'),
+ linkOrcidByItem: jasmine.createSpy('linkOrcidByItem'),
+ });
+
+ itemDataService = jasmine.createSpyObj('ItemDataService', {
+ findById: jasmine.createSpy('findById')
+ });
+
+ void TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot({
+ loader: {
+ provide: TranslateLoader,
+ useClass: TranslateLoaderMock
+ }
+ }),
+ RouterTestingModule.withRoutes([])
+ ],
+ declarations: [OrcidPageComponent],
+ providers: [
+ { provide: ActivatedRoute, useValue: routeStub },
+ { provide: OrcidAuthService, useValue: orcidAuthService },
+ { provide: AuthService, useValue: authService },
+ { provide: ItemDataService, useValue: itemDataService },
+ { provide: PLATFORM_ID, useValue: 'browser' },
+ ],
+
+ schemas: [NO_ERRORS_SCHEMA]
+ }).compileComponents();
+ }));
+
+ beforeEach(waitForAsync(() => {
+ scheduler = getTestScheduler();
+ fixture = TestBed.createComponent(OrcidPageComponent);
+ comp = fixture.componentInstance;
+ authService.isAuthenticated.and.returnValue(observableOf(true));
+ }));
+
+ describe('whn has no query param', () => {
+ beforeEach(waitForAsync(() => {
+ fixture.detectChanges();
+ }));
+
+ it('should create', () => {
+ const btn = fixture.debugElement.queryAll(By.css('[data-test="back-button"]'));
+ const auth = fixture.debugElement.query(By.css('[data-test="orcid-auth"]'));
+ const settings = fixture.debugElement.query(By.css('[data-test="orcid-sync-setting"]'));
+ expect(comp).toBeTruthy();
+ expect(btn.length).toBe(1);
+ expect(auth).toBeTruthy();
+ expect(settings).toBeTruthy();
+ expect(comp.itemId).toBe('test-id');
+ });
+
+ it('should call isLinkedToOrcid', () => {
+ comp.isLinkedToOrcid();
+
+ expect(orcidAuthService.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);
+ }));
+ });
+
+ describe('when query param contains orcid code', () => {
+ beforeEach(waitForAsync(() => {
+ spyOn(comp, 'updateItem').and.callThrough();
+ routeStub.testParams = {
+ code: 'orcid-code'
+ };
+ }));
+
+ describe('and linking to orcid profile is successfully', () => {
+ beforeEach(waitForAsync(() => {
+ orcidAuthService.linkOrcidByItem.and.returnValue(createSuccessfulRemoteDataObject$(mockResearcherProfile));
+ itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid));
+ fixture.detectChanges();
+ }));
+
+ it('should call linkOrcidByItem', () => {
+ expect(orcidAuthService.linkOrcidByItem).toHaveBeenCalledWith(mockItem, 'orcid-code');
+ expect(comp.updateItem).toHaveBeenCalled();
+ });
+
+ it('should create', () => {
+ const btn = fixture.debugElement.queryAll(By.css('[data-test="back-button"]'));
+ const auth = fixture.debugElement.query(By.css('[data-test="orcid-auth"]'));
+ const settings = fixture.debugElement.query(By.css('[data-test="orcid-sync-setting"]'));
+ expect(comp).toBeTruthy();
+ expect(btn.length).toBe(1);
+ expect(auth).toBeTruthy();
+ expect(settings).toBeTruthy();
+ expect(comp.itemId).toBe('test-id');
+ });
+
+ });
+
+ describe('and linking to orcid profile is failed', () => {
+ beforeEach(waitForAsync(() => {
+ orcidAuthService.linkOrcidByItem.and.returnValue(createFailedRemoteDataObject$());
+ itemDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockItemLinkedToOrcid));
+ fixture.detectChanges();
+ }));
+
+ it('should call linkOrcidByItem', () => {
+ expect(orcidAuthService.linkOrcidByItem).toHaveBeenCalledWith(mockItem, 'orcid-code');
+ expect(comp.updateItem).not.toHaveBeenCalled();
+ });
+
+ it('should create', () => {
+ const btn = fixture.debugElement.queryAll(By.css('[data-test="back-button"]'));
+ const auth = fixture.debugElement.query(By.css('[data-test="orcid-auth"]'));
+ const settings = fixture.debugElement.query(By.css('[data-test="orcid-sync-setting"]'));
+ const error = fixture.debugElement.query(By.css('[data-test="error-box"]'));
+ expect(comp).toBeTruthy();
+ expect(btn.length).toBe(1);
+ expect(error).toBeTruthy();
+ expect(auth).toBeFalsy();
+ expect(settings).toBeFalsy();
+ });
+
+ });
+ });
+});
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..f3dbb569d9
--- /dev/null
+++ b/src/app/item-page/orcid-page/orcid-page.component.ts
@@ -0,0 +1,153 @@
+import { Component, Inject, OnInit, PLATFORM_ID } from '@angular/core';
+import { ActivatedRoute, ParamMap, Router } from '@angular/router';
+import { isPlatformBrowser } from '@angular/common';
+
+import { BehaviorSubject, combineLatest } from 'rxjs';
+import { map, take } from 'rxjs/operators';
+
+import { OrcidAuthService } from '../../core/orcid/orcid-auth.service';
+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';
+import { isNotEmpty } from '../../shared/empty.util';
+import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model';
+
+/**
+ * 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 implements OnInit {
+
+ /**
+ * A boolean representing if the connection operation with orcid profile is in progress
+ */
+ connectionStatus: BehaviorSubject = new BehaviorSubject(false);
+
+ /**
+ * The item for which showing the orcid settings
+ */
+ item: BehaviorSubject- = new BehaviorSubject
- (null);
+
+ /**
+ * The item id for which showing the orcid settings
+ */
+ itemId: string;
+
+ /**
+ * A boolean representing if the connection operation with orcid profile is in progress
+ */
+ processingConnection: BehaviorSubject = new BehaviorSubject(true);
+
+ constructor(
+ @Inject(PLATFORM_ID) private platformId: any,
+ private authService: AuthService,
+ private itemService: ItemDataService,
+ private orcidAuthService: OrcidAuthService,
+ private route: ActivatedRoute,
+ private router: Router
+ ) {
+ }
+
+ /**
+ * Retrieve the item for which showing the orcid settings
+ */
+ ngOnInit(): void {
+ if (isPlatformBrowser(this.platformId)) {
+ const codeParam$ = this.route.queryParamMap.pipe(
+ take(1),
+ map((paramMap: ParamMap) => paramMap.get('code')),
+ );
+
+ const item$ = this.route.data.pipe(
+ map((data) => data.dso as RemoteData
- ),
+ redirectOn4xx(this.router, this.authService),
+ getFirstSucceededRemoteDataPayload()
+ );
+
+ combineLatest([codeParam$, item$]).subscribe(([codeParam, item]) => {
+ this.itemId = item.id;
+ /**
+ * Check if code is present in the query param. If so it means this page is loaded after attempting to
+ * link the person to the ORCID profile, otherwise the person is already linked to ORCID profile
+ */
+ if (isNotEmpty(codeParam)) {
+ this.linkProfileToOrcid(item, codeParam);
+ } else {
+ this.item.next(item);
+ this.processingConnection.next(false);
+ this.connectionStatus.next(true);
+ }
+ });
+ }
+ }
+
+ /**
+ * Check if the current item is linked to an ORCID profile.
+ *
+ * @returns the check result
+ */
+ isLinkedToOrcid(): boolean {
+ return this.orcidAuthService.isLinkedToOrcid(this.item.value);
+ }
+
+ /**
+ * Get the route to an item's page
+ */
+ getItemPage(): string {
+ return getItemPageRoute(this.item.value);
+ }
+
+ /**
+ * Retrieve the updated profile item
+ */
+ updateItem(): void {
+ this.clearRouteParams();
+ this.itemService.findById(this.itemId, false).pipe(
+ getFirstCompletedRemoteData()
+ ).subscribe((itemRD: RemoteData
- ) => {
+ if (itemRD.hasSucceeded) {
+ this.item.next(itemRD.payload);
+ }
+ });
+ }
+
+ /**
+ * Link person item to ORCID profile by using the code received after redirect from ORCID.
+ *
+ * @param person The person item to link to ORCID profile
+ * @param code The auth-code received from ORCID
+ */
+ private linkProfileToOrcid(person: Item, code: string) {
+ this.orcidAuthService.linkOrcidByItem(person, code).pipe(
+ getFirstCompletedRemoteData()
+ ).subscribe((profileRD: RemoteData) => {
+ this.processingConnection.next(false);
+ if (profileRD.hasSucceeded) {
+ this.connectionStatus.next(true);
+ this.updateItem();
+ } else {
+ this.item.next(person);
+ this.connectionStatus.next(false);
+ this.clearRouteParams();
+ }
+ });
+ }
+
+ /**
+ * Update route removing the code from query params
+ * @private
+ */
+ private clearRouteParams(): void {
+ // update route removing the code from query params
+ const redirectUrl = this.router.url.split('?')[0];
+ this.router.navigate([redirectUrl]);
+ }
+}
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/item-page/orcid-page/orcid-queue/orcid-queue.component.html b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html
new file mode 100644
index 0000000000..9358bcf835
--- /dev/null
+++ b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html
@@ -0,0 +1,51 @@
+
+
+
+
{{ 'person.orcid.registry.queue' | translate }}
+
+
+ {{ 'person.page.orcid.sync-queue.empty-message' | translate}}
+
+
0"
+ [paginationOptions]="paginationOptions"
+ [collectionSize]="(getList() | async)?.payload?.totalElements"
+ [retainScrollPosition]="false" [hideGear]="true" (paginationChange)="updateList()">
+
+
+
+
+
+ {{'person.page.orcid.sync-queue.table.header.type' | translate}} |
+ {{'person.page.orcid.sync-queue.table.header.description' | translate}} |
+ {{'person.page.orcid.sync-queue.table.header.action' | translate}} |
+
+
+
+
+
+
+ |
+
+ {{ entry.description }}
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
diff --git a/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.scss b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.spec.ts b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.spec.ts
new file mode 100644
index 0000000000..9107ac34ff
--- /dev/null
+++ b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.spec.ts
@@ -0,0 +1,151 @@
+import { OrcidQueueComponent } from './orcid-queue.component';
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
+import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
+import { RouterTestingModule } from '@angular/router/testing';
+import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
+import { OrcidQueueService } from '../../../core/orcid/orcid-queue.service';
+import { PaginationService } from '../../../core/pagination/pagination.service';
+import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
+import { NotificationsService } from '../../../shared/notifications/notifications.service';
+import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
+import { OrcidHistoryDataService } from '../../../core/orcid/orcid-history-data.service';
+import { OrcidQueue } from '../../../core/orcid/model/orcid-queue.model';
+import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
+import { createPaginatedList } from '../../../shared/testing/utils.test';
+import { PaginatedList } from '../../../core/data/paginated-list.model';
+import { By } from '@angular/platform-browser';
+import { Item } from '../../../core/shared/item.model';
+import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
+
+describe('OrcidQueueComponent test suite', () => {
+ let component: OrcidQueueComponent;
+ let fixture: ComponentFixture;
+ let debugElement: DebugElement;
+ let orcidQueueService: OrcidQueueService;
+ let orcidAuthService: jasmine.SpyObj;
+
+ const testProfileItemId = 'test-owner-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
+ }],
+ 'person.identifier.orcid': [{
+ 'value': 'orcid-id',
+ 'language': null,
+ 'authority': null,
+ 'confidence': -1,
+ 'place': 0
+ }]
+ }
+ });
+
+ function orcidQueueElement(id: number) {
+ return Object.assign(new OrcidQueue(), {
+ 'id': id,
+ 'profileItemId': testProfileItemId,
+ 'entityId': `test-entity-${id}`,
+ 'description': `test description ${id}`,
+ 'recordType': 'Publication',
+ 'operation': 'INSERT',
+ 'type': 'orcidqueue',
+ });
+ }
+
+ const orcidQueueElements = [orcidQueueElement(1), orcidQueueElement(2)];
+
+ const orcidQueueServiceSpy = jasmine.createSpyObj('orcidQueueService', ['searchByProfileItemId', 'clearFindByProfileItemRequests']);
+ orcidQueueServiceSpy.searchByProfileItemId.and.returnValue(createSuccessfulRemoteDataObject$>(createPaginatedList(orcidQueueElements)));
+
+ beforeEach(waitForAsync(() => {
+ orcidAuthService = jasmine.createSpyObj('OrcidAuthService', {
+ getOrcidAuthorizeUrl: jasmine.createSpy('getOrcidAuthorizeUrl')
+ });
+
+ void TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot({
+ loader: {
+ provide: TranslateLoader,
+ useClass: TranslateLoaderMock
+ }
+ }),
+ RouterTestingModule.withRoutes([])
+ ],
+ declarations: [OrcidQueueComponent],
+ providers: [
+ { provide: OrcidAuthService, useValue: orcidAuthService },
+ { provide: OrcidQueueService, useValue: orcidQueueServiceSpy },
+ { provide: OrcidHistoryDataService, useValue: {} },
+ { provide: PaginationService, useValue: new PaginationServiceStub() },
+ { provide: NotificationsService, useValue: new NotificationsServiceStub() },
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ }).compileComponents();
+
+ orcidQueueService = TestBed.inject(OrcidQueueService);
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(OrcidQueueComponent);
+ component = fixture.componentInstance;
+ component.item = mockItemLinkedToOrcid;
+ debugElement = fixture.debugElement;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should show the ORCID queue elements', () => {
+ const table = debugElement.queryAll(By.css('[data-test="orcidQueueElementRow"]'));
+ expect(table.length).toBe(2);
+ });
+
+});
diff --git a/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.ts b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.ts
new file mode 100644
index 0000000000..99ba33ee82
--- /dev/null
+++ b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.ts
@@ -0,0 +1,302 @@
+import { Component, Input, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
+
+import { TranslateService } from '@ngx-translate/core';
+import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
+import { debounceTime, distinctUntilChanged, switchMap, tap } from 'rxjs/operators';
+
+import { PaginatedList } from '../../../core/data/paginated-list.model';
+import { RemoteData } from '../../../core/data/remote-data';
+import { OrcidHistory } from '../../../core/orcid/model/orcid-history.model';
+import { OrcidQueue } from '../../../core/orcid/model/orcid-queue.model';
+import { OrcidHistoryDataService } from '../../../core/orcid/orcid-history-data.service';
+import { OrcidQueueService } from '../../../core/orcid/orcid-queue.service';
+import { PaginationService } from '../../../core/pagination/pagination.service';
+import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
+import { hasValue } from '../../../shared/empty.util';
+import { NotificationsService } from '../../../shared/notifications/notifications.service';
+import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
+import { AlertType } from '../../../shared/alert/aletr-type';
+import { Item } from '../../../core/shared/item.model';
+import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
+
+@Component({
+ selector: 'ds-orcid-queue',
+ templateUrl: './orcid-queue.component.html',
+ styleUrls: ['./orcid-queue.component.scss']
+})
+export class OrcidQueueComponent implements OnInit, OnDestroy {
+
+ /**
+ * The item for which showing the orcid settings
+ */
+ @Input() item: Item;
+
+ /**
+ * Pagination config used to display the list
+ */
+ public paginationOptions: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
+ id: 'oqp',
+ pageSize: 5
+ });
+
+ /**
+ * A boolean representing if results are loading
+ */
+ public processing$ = new BehaviorSubject(false);
+
+ /**
+ * A list of orcid queue records
+ */
+ private list$: BehaviorSubject>> = new BehaviorSubject>>({} as any);
+
+ /**
+ * The AlertType enumeration
+ * @type {AlertType}
+ */
+ AlertTypeEnum = AlertType;
+
+ /**
+ * Array to track all subscriptions and unsubscribe them onDestroy
+ * @type {Array}
+ */
+ private subs: Subscription[] = [];
+
+ constructor(private orcidAuthService: OrcidAuthService,
+ private orcidQueueService: OrcidQueueService,
+ protected translateService: TranslateService,
+ private paginationService: PaginationService,
+ private notificationsService: NotificationsService,
+ private orcidHistoryService: OrcidHistoryDataService,
+ ) {
+ }
+
+ ngOnInit(): void {
+ this.updateList();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (!changes.item.isFirstChange() && changes.item.currentValue !== changes.item.previousValue) {
+ this.updateList();
+ }
+ }
+
+ /**
+ * Retrieve queue list
+ */
+ updateList() {
+ this.subs.push(
+ this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions).pipe(
+ debounceTime(100),
+ distinctUntilChanged(),
+ tap(() => this.processing$.next(true)),
+ switchMap((config: PaginationComponentOptions) => this.orcidQueueService.searchByProfileItemId(this.item.id, config, false)),
+ getFirstCompletedRemoteData()
+ ).subscribe((result: RemoteData>) => {
+ this.processing$.next(false);
+ this.list$.next(result);
+ this.orcidQueueService.clearFindByProfileItemRequests();
+ })
+ );
+ }
+
+ /**
+ * Return the list of orcid queue records
+ */
+ getList(): Observable>> {
+ return this.list$.asObservable();
+ }
+
+ /**
+ * Return the icon class for the queue object type
+ *
+ * @param orcidQueue The OrcidQueue object
+ */
+ getIconClass(orcidQueue: OrcidQueue): string {
+ if (!orcidQueue.recordType) {
+ return 'fa fa-user';
+ }
+ switch (orcidQueue.recordType.toLowerCase()) {
+ case 'publication':
+ return 'fas fa-book';
+ case 'project':
+ return 'fas fa-wallet';
+ case 'education':
+ return 'fas fa-school';
+ case 'affiliation':
+ return 'fas fa-university';
+ case 'country':
+ return 'fas fa-globe-europe';
+ case 'external_ids':
+ case 'researcher_urls':
+ return 'fas fa-external-link-alt';
+ default:
+ return 'fa fa-user';
+ }
+ }
+
+ /**
+ * Return the icon tooltip message for the queue object type
+ *
+ * @param orcidQueue The OrcidQueue object
+ */
+ getIconTooltip(orcidQueue: OrcidQueue): string {
+ if (!orcidQueue.recordType) {
+ return '';
+ }
+
+ return 'person.page.orcid.sync-queue.tooltip.' + orcidQueue.recordType.toLowerCase();
+ }
+
+ /**
+ * Return the icon tooltip message for the queue object operation
+ *
+ * @param orcidQueue The OrcidQueue object
+ */
+ getOperationTooltip(orcidQueue: OrcidQueue): string {
+ if (!orcidQueue.operation) {
+ return '';
+ }
+
+ return 'person.page.orcid.sync-queue.tooltip.' + orcidQueue.operation.toLowerCase();
+ }
+
+ /**
+ * Return the icon class for the queue object operation
+ *
+ * @param orcidQueue The OrcidQueue object
+ */
+ getOperationClass(orcidQueue: OrcidQueue): string {
+
+ if (!orcidQueue.operation) {
+ return '';
+ }
+
+ switch (orcidQueue.operation.toLowerCase()) {
+ case 'insert':
+ return 'fas fa-plus';
+ case 'update':
+ return 'fas fa-edit';
+ case 'delete':
+ return 'fas fa-trash-alt';
+ default:
+ return '';
+ }
+ }
+
+ /**
+ * Discard a queue entry from the synchronization
+ *
+ * @param orcidQueue The OrcidQueue object to discard
+ */
+ discardEntry(orcidQueue: OrcidQueue) {
+ this.processing$.next(true);
+ this.subs.push(this.orcidQueueService.deleteById(orcidQueue.id).pipe(
+ getFirstCompletedRemoteData()
+ ).subscribe((remoteData) => {
+ this.processing$.next(false);
+ if (remoteData.isSuccess) {
+ this.notificationsService.success(this.translateService.get('person.page.orcid.sync-queue.discard.success'));
+ this.updateList();
+ } else {
+ this.notificationsService.error(this.translateService.get('person.page.orcid.sync-queue.discard.error'));
+ }
+ }));
+ }
+
+ /**
+ * Send a queue entry to orcid for the synchronization
+ *
+ * @param orcidQueue The OrcidQueue object to synchronize
+ */
+ send(orcidQueue: OrcidQueue) {
+ this.processing$.next(true);
+ this.subs.push(this.orcidHistoryService.sendToORCID(orcidQueue).pipe(
+ getFirstCompletedRemoteData()
+ ).subscribe((remoteData) => {
+ this.processing$.next(false);
+ if (remoteData.isSuccess) {
+ this.handleOrcidHistoryRecordCreation(remoteData.payload);
+ } else if (remoteData.statusCode === 422) {
+ this.handleValidationErrors(remoteData);
+ } else {
+ this.notificationsService.error(this.translateService.get('person.page.orcid.sync-queue.send.error'));
+ }
+ }));
+ }
+
+
+ /**
+ * Return the error message for Unauthorized response
+ * @private
+ */
+ private getUnauthorizedErrorContent(): Observable {
+ return this.orcidAuthService.getOrcidAuthorizeUrl(this.item).pipe(
+ switchMap((authorizeUrl) => this.translateService.get(
+ 'person.page.orcid.sync-queue.send.unauthorized-error.content',
+ { orcid: authorizeUrl }
+ ))
+ );
+ }
+
+ /**
+ * Manage notification by response
+ * @private
+ */
+ private handleOrcidHistoryRecordCreation(orcidHistory: OrcidHistory) {
+ switch (orcidHistory.status) {
+ case 200:
+ case 201:
+ case 204:
+ this.notificationsService.success(this.translateService.get('person.page.orcid.sync-queue.send.success'));
+ this.updateList();
+ break;
+ case 400:
+ this.notificationsService.error(this.translateService.get('person.page.orcid.sync-queue.send.bad-request-error'), null, { timeOut: 0 });
+ break;
+ case 401:
+ combineLatest([
+ this.translateService.get('person.page.orcid.sync-queue.send.unauthorized-error.title'),
+ this.getUnauthorizedErrorContent()],
+ ).subscribe(([title, content]) => {
+ this.notificationsService.error(title, content, { timeOut: 0 }, true);
+ });
+ break;
+ case 404:
+ this.notificationsService.warning(this.translateService.get('person.page.orcid.sync-queue.send.not-found-warning'));
+ break;
+ case 409:
+ this.notificationsService.error(this.translateService.get('person.page.orcid.sync-queue.send.conflict-error'), null, { timeOut: 0 });
+ break;
+ default:
+ this.notificationsService.error(this.translateService.get('person.page.orcid.sync-queue.send.error'), null, { timeOut: 0 });
+ }
+ }
+
+ /**
+ * Manage validation errors
+ * @private
+ */
+ private handleValidationErrors(remoteData: RemoteData) {
+ const translations = [this.translateService.get('person.page.orcid.sync-queue.send.validation-error')];
+ const errorMessage = remoteData.errorMessage;
+ if (errorMessage && errorMessage.indexOf('Error codes:') > 0) {
+ errorMessage.substring(errorMessage.indexOf(':') + 1).trim().split(',')
+ .forEach((error) => translations.push(this.translateService.get('person.page.orcid.sync-queue.send.validation-error.' + error)));
+ }
+ combineLatest(translations).subscribe((messages) => {
+ const title = messages.shift();
+ const content = '' + messages.map((message) => `- ${message}
`).join('') + '
';
+ this.notificationsService.error(title, content, { timeOut: 0 }, true);
+ });
+ }
+
+ /**
+ * Unsubscribe from all subscriptions
+ */
+ ngOnDestroy(): void {
+ this.list$ = null;
+ this.subs.filter((subscription) => hasValue(subscription))
+ .forEach((subscription) => subscription.unsubscribe());
+ }
+
+}
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
new file mode 100644
index 0000000000..ee9a15268a
--- /dev/null
+++ b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.html
@@ -0,0 +1,106 @@
+
+
{{'person.orcid.sync.setting' | translate}}
+
+
diff --git a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.scss b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.scss
new file mode 100644
index 0000000000..e69de29bb2
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..4312d35be9
--- /dev/null
+++ b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts
@@ -0,0 +1,261 @@
+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),
+ });
+ spyOn(comp.settingsUpdated, 'emit');
+ });
+
+ 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();
+ expect(comp.settingsUpdated.emit).toHaveBeenCalled();
+ });
+
+ it('should show notification on error', () => {
+ researcherProfileService.findByRelatedItem.and.returnValue(createFailedRemoteDataObject$());
+
+ scheduler.schedule(() => comp.onSubmit(formGroup));
+ scheduler.flush();
+
+ expect(notificationsService.error).toHaveBeenCalled();
+ expect(comp.settingsUpdated.emit).not.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();
+ expect(comp.settingsUpdated.emit).not.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
new file mode 100644
index 0000000000..6e8b0c8216
--- /dev/null
+++ b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts
@@ -0,0 +1,196 @@
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { FormGroup } from '@angular/forms';
+
+import { TranslateService } from '@ngx-translate/core';
+import { Operation } from 'fast-json-patch';
+import { of } from 'rxjs';
+import { switchMap } from 'rxjs/operators';
+
+import { RemoteData } from '../../../core/data/remote-data';
+import { ResearcherProfileService } from '../../../core/profile/researcher-profile.service';
+import { Item } from '../../../core/shared/item.model';
+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-sync-setting',
+ templateUrl: './orcid-sync-settings.component.html',
+ styleUrls: ['./orcid-sync-settings.component.scss']
+})
+export class OrcidSyncSettingsComponent implements OnInit {
+
+ /**
+ * The item for which showing the orcid settings
+ */
+ @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;
+
+ /**
+ * 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 }[];
+
+ /**
+ * The synchronization options for funding
+ */
+ syncFundingOptions: { value: string, label: string }[];
+
+ /**
+ * The profile synchronization options
+ */
+ syncProfileOptions: { value: string, label: string, checked: boolean }[];
+
+ /**
+ * An event emitted when settings are updated
+ */
+ @Output() settingsUpdated: EventEmitter = new EventEmitter();
+
+ constructor(private researcherProfileService: ResearcherProfileService,
+ private notificationsService: NotificationsService,
+ private translateService: TranslateService) {
+ }
+
+ /**
+ * Init orcid settings form
+ */
+ 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('dspace.orcid.sync-profile');
+
+ this.syncProfileOptions = ['BIOGRAPHICAL', 'IDENTIFIERS']
+ .map((value) => {
+ return {
+ label: this.messagePrefix + '.sync-profile.' + value.toLowerCase(),
+ value: value,
+ checked: syncProfilePreferences.includes(value)
+ };
+ });
+
+ this.currentSyncMode = this.getCurrentPreference('dspace.orcid.sync-mode', ['BATCH', 'MANUAL'], 'MANUAL');
+ this.currentSyncPublications = this.getCurrentPreference('dspace.orcid.sync-publications', ['DISABLED', 'ALL'], 'DISABLED');
+ this.currentSyncFunding = this.getCurrentPreference('dspace.orcid.sync-fundings', ['DISABLED', 'ALL'], 'DISABLED');
+ }
+
+ /**
+ * 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);
+ 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.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'));
+ this.settingsUpdated.emit();
+ } else {
+ this.notificationsService.error(this.translateService.get(this.messagePrefix + '.synchronization-settings-update.error'));
+ }
+ });
+ }
+
+ /**
+ * 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',
+ value: currentValue
+ });
+ }
+
+}
diff --git a/src/app/item-page/simple/item-types/publication/publication.component.html b/src/app/item-page/simple/item-types/publication/publication.component.html
index 96454914cd..d83202ce12 100644
--- a/src/app/item-page/simple/item-types/publication/publication.component.html
+++ b/src/app/item-page/simple/item-types/publication/publication.component.html
@@ -12,6 +12,9 @@
{{'publication.page.titleprefix' | translate}}
+
diff --git a/src/app/item-page/simple/item-types/publication/publication.component.spec.ts b/src/app/item-page/simple/item-types/publication/publication.component.spec.ts
index 761849f232..a623a34b15 100644
--- a/src/app/item-page/simple/item-types/publication/publication.component.spec.ts
+++ b/src/app/item-page/simple/item-types/publication/publication.component.spec.ts
@@ -1,6 +1,6 @@
import { HttpClient } from '@angular/common/http';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
-import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Store } from '@ngrx/store';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
@@ -17,7 +17,7 @@ import { RemoteData } from '../../../../core/data/remote-data';
import { Bitstream } from '../../../../core/shared/bitstream.model';
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
import { Item } from '../../../../core/shared/item.model';
-import { MetadataMap } from '../../../../core/shared/metadata.models';
+import { MetadataMap } from '../../../../core/shared/metadata.models';
import { UUIDService } from '../../../../core/shared/uuid.service';
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
@@ -26,22 +26,16 @@ import { TruncatableService } from '../../../../shared/truncatable/truncatable.s
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
import {
- createRelationshipsObservable,
- iiifEnabled,
- iiifSearchEnabled, mockRouteService
+ createRelationshipsObservable, getIIIFEnabled, getIIIFSearchEnabled, mockRouteService
} from '../shared/item.component.spec';
import { PublicationComponent } from './publication.component';
import { createPaginatedList } from '../../../../shared/testing/utils.test';
import { RouteService } from '../../../../core/services/route.service';
-
-const iiifEnabledMap: MetadataMap = {
- 'dspace.iiif.enabled': [iiifEnabled],
-};
-
-const iiifEnabledWithSearchMap: MetadataMap = {
- 'dspace.iiif.enabled': [iiifEnabled],
- 'iiif.search.enabled': [iiifSearchEnabled],
-};
+import { VersionHistoryDataService } from '../../../../core/data/version-history-data.service';
+import { VersionDataService } from '../../../../core/data/version-data.service';
+import { RouterTestingModule } from '@angular/router/testing';
+import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service';
+import { SearchService } from '../../../../core/shared/search/search.service';
const noMetadata = new MetadataMap();
@@ -64,12 +58,15 @@ describe('PublicationComponent', () => {
}
};
TestBed.configureTestingModule({
- imports: [TranslateModule.forRoot({
- loader: {
- provide: TranslateLoader,
- useClass: TranslateLoaderMock
- }
- })],
+ imports: [
+ TranslateModule.forRoot({
+ loader: {
+ provide: TranslateLoader,
+ useClass: TranslateLoaderMock
+ }
+ }),
+ RouterTestingModule,
+ ],
declarations: [PublicationComponent, GenericItemPageFieldComponent, TruncatePipe],
providers: [
{ provide: ItemDataService, useValue: {} },
@@ -85,18 +82,23 @@ describe('PublicationComponent', () => {
{ provide: HttpClient, useValue: {} },
{ provide: DSOChangeAnalyzer, useValue: {} },
{ provide: DefaultChangeAnalyzer, useValue: {} },
+ { provide: VersionHistoryDataService, useValue: {} },
+ { provide: VersionDataService, useValue: {} },
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
+ { provide: WorkspaceitemDataService, useValue: {} },
+ { provide: SearchService, useValue: {} },
{ provide: RouteService, useValue: mockRouteService }
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(PublicationComponent, {
- set: { changeDetection: ChangeDetectionStrategy.Default }
- }).compileComponents();
+ set: {changeDetection: ChangeDetectionStrategy.Default}
+ });
}));
describe('default view', () => {
beforeEach(waitForAsync(() => {
+ TestBed.compileComponents();
fixture = TestBed.createComponent(PublicationComponent);
comp = fixture.componentInstance;
comp.object = getItem(noMetadata);
@@ -137,6 +139,42 @@ describe('PublicationComponent', () => {
describe('with IIIF viewer', () => {
beforeEach(waitForAsync(() => {
+ const iiifEnabledMap: MetadataMap = {
+ 'dspace.iiif.enabled': [getIIIFEnabled(true)],
+ 'iiif.search.enabled': [getIIIFSearchEnabled(false)],
+ };
+ TestBed.compileComponents();
+ fixture = TestBed.createComponent(PublicationComponent);
+ comp = fixture.componentInstance;
+ comp.object = getItem(iiifEnabledMap);
+ fixture.detectChanges();
+ }));
+
+ it('should contain an iiif viewer component', () => {
+ const fields = fixture.debugElement.queryAll(By.css('ds-mirador-viewer'));
+ expect(fields.length).toBeGreaterThanOrEqual(1);
+ });
+ it('should not retrieve the query term for previous route', fakeAsync((): void => {
+ //tick(10)
+ expect(comp.iiifQuery$).toBeFalsy();
+ }));
+
+ });
+
+ describe('with IIIF viewer and search', () => {
+
+ beforeEach(waitForAsync(() => {
+ const localMockRouteService = {
+ getPreviousUrl(): Observable