diff --git a/src/app/item-page/edit-item-page/item-operation/itemOperation.model.ts b/src/app/item-page/edit-item-page/item-operation/itemOperation.model.ts
index 33302dcba6c..a6f08ac95ce 100644
--- a/src/app/item-page/edit-item-page/item-operation/itemOperation.model.ts
+++ b/src/app/item-page/edit-item-page/item-operation/itemOperation.model.ts
@@ -28,4 +28,12 @@ export class ItemOperation {
this.disabled = disabled;
}
+ /**
+ * Set whether this operation is authorized
+ * @param authorized
+ */
+ setAuthorized(authorized: boolean): void {
+ this.authorized = authorized;
+ }
+
}
diff --git a/src/app/item-page/edit-item-page/item-status/item-status.component.html b/src/app/item-page/edit-item-page/item-status/item-status.component.html
index 8d4faaa2ac5..155b9478043 100644
--- a/src/app/item-page/edit-item-page/item-status/item-status.component.html
+++ b/src/app/item-page/edit-item-page/item-status/item-status.component.html
@@ -27,7 +27,7 @@
-
+
diff --git a/src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts b/src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts
index a67de2f435a..17ac3efa09d 100644
--- a/src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts
+++ b/src/app/item-page/edit-item-page/item-status/item-status.component.spec.ts
@@ -16,6 +16,7 @@ import { AuthorizationDataService } from '../../../core/data/feature-authorizati
import { IdentifierDataService } from '../../../core/data/identifier-data.service';
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
+import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
let mockIdentifierDataService: IdentifierDataService;
let mockConfigurationDataService: ConfigurationDataService;
@@ -57,12 +58,18 @@ describe('ItemStatusComponent', () => {
};
let authorizationService: AuthorizationDataService;
+ let orcidAuthService: any;
beforeEach(waitForAsync(() => {
authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true)
});
+ orcidAuthService = jasmine.createSpyObj('OrcidAuthService', {
+ onlyAdminCanDisconnectProfileFromOrcid: observableOf ( true ),
+ isLinkedToOrcid: true
+ });
+
TestBed.configureTestingModule({
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
declarations: [ItemStatusComponent],
@@ -71,7 +78,8 @@ describe('ItemStatusComponent', () => {
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: AuthorizationDataService, useValue: authorizationService },
{ provide: IdentifierDataService, useValue: mockIdentifierDataService },
- { provide: ConfigurationDataService, useValue: mockConfigurationDataService }
+ { provide: ConfigurationDataService, useValue: mockConfigurationDataService },
+ { provide: OrcidAuthService, useValue: orcidAuthService },
], schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents();
}));
diff --git a/src/app/item-page/edit-item-page/item-status/item-status.component.ts b/src/app/item-page/edit-item-page/item-status/item-status.component.ts
index 828f8d74391..8e04985c184 100644
--- a/src/app/item-page/edit-item-page/item-status/item-status.component.ts
+++ b/src/app/item-page/edit-item-page/item-status/item-status.component.ts
@@ -3,21 +3,20 @@ import { fadeIn, fadeInOut } from '../../../shared/animations/fade';
import { Item } from '../../../core/shared/item.model';
import { ActivatedRoute } from '@angular/router';
import { ItemOperation } from '../item-operation/itemOperation.model';
-import { distinctUntilChanged, map, mergeMap, switchMap, toArray } from 'rxjs/operators';
-import { BehaviorSubject, Observable, Subscription } from 'rxjs';
+import { concatMap, distinctUntilChanged, first, map, mergeMap, switchMap, toArray } from 'rxjs/operators';
+import { BehaviorSubject, combineLatest, Observable, of, Subscription } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
-import { hasValue, isNotEmpty } from '../../../shared/empty.util';
-import {
- getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, getFirstSucceededRemoteData, getRemoteDataPayload,
-} from '../../../core/shared/operators';
+import { hasValue } from '../../../shared/empty.util';
+import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, } from '../../../core/shared/operators';
import { IdentifierDataService } from '../../../core/data/identifier-data.service';
import { Identifier } from '../../../shared/object-list/identifier-data/identifier.model';
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
import { IdentifierData } from '../../../shared/object-list/identifier-data/identifier-data.model';
+import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
@Component({
selector: 'ds-item-status',
@@ -73,6 +72,7 @@ export class ItemStatusComponent implements OnInit {
private authorizationService: AuthorizationDataService,
private identifierDataService: IdentifierDataService,
private configurationService: ConfigurationDataService,
+ private orcidAuthService: OrcidAuthService
) {
}
@@ -82,14 +82,16 @@ export class ItemStatusComponent implements OnInit {
ngOnInit(): void {
this.itemRD$ = this.route.parent.data.pipe(map((data) => data.dso));
this.itemRD$.pipe(
+ first(),
map((data: RemoteData- ) => data.payload)
- ).subscribe((item: Item) => {
- this.statusData = Object.assign({
- id: item.id,
- handle: item.handle,
- lastModified: item.lastModified
- });
- this.statusDataKeys = Object.keys(this.statusData);
+ ).pipe(
+ switchMap((item: Item) => {
+ this.statusData = Object.assign({
+ id: item.id,
+ handle: item.handle,
+ lastModified: item.lastModified
+ });
+ this.statusDataKeys = Object.keys(this.statusData);
// Observable for item identifiers (retrieved from embedded link)
this.identifiers$ = this.identifierDataService.getIdentifierDataFor(item).pipe(
@@ -105,99 +107,108 @@ export class ItemStatusComponent implements OnInit {
// Observable for configuration determining whether the Register DOI feature is enabled
let registerConfigEnabled$: Observable = this.configurationService.findByPropertyName('identifiers.item-status.register-doi').pipe(
getFirstCompletedRemoteData(),
- map((rd: RemoteData) => {
- // If the config property is exposed via rest and has a value set, return it
- if (rd.hasSucceeded && hasValue(rd.payload) && isNotEmpty(rd.payload.values)) {
- return rd.payload.values[0] === 'true';
- }
- // Otherwise, return false
- return false;
- })
+ map((enabledRD: RemoteData) => enabledRD.hasSucceeded && enabledRD.payload.values.length > 0)
);
- /*
- Construct a base list of operations.
- The key is used to build messages
- i18n example: 'item.edit.tabs.status.buttons..label'
- The value is supposed to be a href for the button
- */
- const operations: ItemOperation[] = [];
- operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations', FeatureID.CanManagePolicies, true));
- operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper', FeatureID.CanManageMappings, true));
- if (item.isWithdrawn) {
- operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate', FeatureID.ReinstateItem, true));
- } else {
- operations.push(new ItemOperation('withdraw', this.getCurrentUrl(item) + '/withdraw', FeatureID.WithdrawItem, true));
- }
- if (item.isDiscoverable) {
- operations.push(new ItemOperation('private', this.getCurrentUrl(item) + '/private', FeatureID.CanMakePrivate, true));
- } else {
- operations.push(new ItemOperation('public', this.getCurrentUrl(item) + '/public', FeatureID.CanMakePrivate, true));
- }
- operations.push(new ItemOperation('delete', this.getCurrentUrl(item) + '/delete', FeatureID.CanDelete, true));
- operations.push(new ItemOperation('move', this.getCurrentUrl(item) + '/move', FeatureID.CanMove, true));
- this.operations$.next(operations);
-
- /*
- When the identifier data stream changes, determine whether the register DOI button should be shown or not.
- This is based on whether the DOI is in the right state (minted or pending, not already queued for registration
- or registered) and whether the configuration property identifiers.item-status.register-doi is true
+ /**
+ * Construct a base list of operations.
+ * The key is used to build messages
+ * i18n example: 'item.edit.tabs.status.buttons..label'
+ * The value is supposed to be a href for the button
*/
- this.identifierDataService.getIdentifierDataFor(item).pipe(
- getFirstSucceededRemoteData(),
- getRemoteDataPayload(),
- mergeMap((data: IdentifierData) => {
- let identifiers = data.identifiers;
- let no_doi = true;
- let pending = false;
- if (identifiers !== undefined && identifiers !== null) {
- identifiers.forEach((identifier: Identifier) => {
- if (hasValue(identifier) && identifier.identifierType === 'doi') {
- // The item has some kind of DOI
- no_doi = false;
- if (identifier.identifierStatus === 'PENDING' || identifier.identifierStatus === 'MINTED'
- || identifier.identifierStatus == null) {
- // The item's DOI is pending, minted or null.
- // It isn't registered, reserved, queued for registration or reservation or update, deleted
- // or queued for deletion.
- pending = true;
- }
+ const currentUrl = this.getCurrentUrl(item);
+ const inititalOperations: ItemOperation[] = [
+ new ItemOperation('authorizations', `${currentUrl}/authorizations`, FeatureID.CanManagePolicies, true),
+ new ItemOperation('mappedCollections', `${currentUrl}/mapper`, FeatureID.CanManageMappings, true),
+ item.isWithdrawn
+ ? new ItemOperation('reinstate', `${currentUrl}/reinstate`, FeatureID.ReinstateItem, true)
+ : new ItemOperation('withdraw', `${currentUrl}/withdraw`, FeatureID.WithdrawItem, true),
+ item.isDiscoverable
+ ? new ItemOperation('private', `${currentUrl}/private`, FeatureID.CanMakePrivate, true)
+ : new ItemOperation('public', `${currentUrl}/public`, FeatureID.CanMakePrivate, true),
+ new ItemOperation('move', `${currentUrl}/move`, FeatureID.CanMove, true),
+ new ItemOperation('delete', `${currentUrl}/delete`, FeatureID.CanDelete, true)
+ ];
+
+ this.operations$.next(inititalOperations);
+
+ /**
+ * When the identifier data stream changes, determine whether the register DOI button should be shown or not.
+ * This is based on whether the DOI is in the right state (minted or pending, not already queued for registration
+ * or registered) and whether the configuration property identifiers.item-status.register-doi is true
+ */
+ const ops$ = this.identifierDataService.getIdentifierDataFor(item).pipe(
+ getFirstCompletedRemoteData(),
+ mergeMap((dataRD: RemoteData) => {
+ if (dataRD.hasSucceeded) {
+ let identifiers = dataRD.payload.identifiers;
+ let no_doi = true;
+ let pending = false;
+ if (identifiers !== undefined && identifiers !== null) {
+ identifiers.forEach((identifier: Identifier) => {
+ if (hasValue(identifier) && identifier.identifierType === 'doi') {
+ // The item has some kind of DOI
+ no_doi = false;
+ if (['PENDING', 'MINTED', null].includes(identifier.identifierStatus)) {
+ // The item's DOI is pending, minted or null.
+ // It isn't registered, reserved, queued for registration or reservation or update, deleted
+ // or queued for deletion.
+ pending = true;
+ }
+ }
+ });
}
- });
- }
- // If there is no DOI, or a pending/minted/null DOI, and the config is enabled, return true
- return registerConfigEnabled$.pipe(
- map((enabled: boolean) => {
- return enabled && (pending || no_doi);
+ // If there is no DOI, or a pending/minted/null DOI, and the config is enabled, return true
+ return registerConfigEnabled$.pipe(
+ map((enabled: boolean) => {
+ return enabled && (pending || no_doi);
+ }
+ ));
+ } else {
+ return of(false);
+ }
+ }),
+ // Switch map pushes the register DOI operation onto a copy of the base array then returns to the pipe
+ switchMap((showDoi: boolean) => {
+ const ops = [...inititalOperations];
+ if (showDoi) {
+ const op = new ItemOperation('register-doi', `${currentUrl}/register-doi`, FeatureID.CanRegisterDOI, true);
+ ops.splice(ops.length - 1, 0, op); // Add item before last
+ }
+ return inititalOperations;
+ }),
+ concatMap((op: ItemOperation) => {
+ if (hasValue(op.featureID)) {
+ return this.authorizationService.isAuthorized(op.featureID, item.self).pipe(
+ distinctUntilChanged(),
+ map((authorized) => {
+ op.setDisabled(!authorized);
+ op.setAuthorized(authorized);
+ return op;
+ })
+ );
}
- ));
- }),
- // Switch map pushes the register DOI operation onto a copy of the base array then returns to the pipe
- switchMap((showDoi: boolean) => {
- let ops = [...operations];
- if (showDoi) {
- ops.push(new ItemOperation('register-doi', this.getCurrentUrl(item) + '/register-doi', FeatureID.CanRegisterDOI, true));
- }
- return ops;
- }),
- // Merge map checks and transforms each operation in the array based on whether it is authorized or not (disabled)
- mergeMap((op: ItemOperation) => {
- if (hasValue(op.featureID)) {
- return this.authorizationService.isAuthorized(op.featureID, item.self).pipe(
- distinctUntilChanged(),
- map((authorized) => new ItemOperation(op.operationKey, op.operationUrl, op.featureID, !authorized, authorized))
- );
- } else {
return [op];
- }
- }),
- // Wait for all operations to be emitted and return as an array
- toArray(),
- ).subscribe((data) => {
- // Update the operations$ subject that draws the administrative buttons on the status page
- this.operations$.next(data);
- });
- });
+ }),
+ toArray()
+ );
+
+ let orcidOps$ = of([]);
+ if (this.orcidAuthService.isLinkedToOrcid(item)) {
+ orcidOps$ = this.orcidAuthService.onlyAdminCanDisconnectProfileFromOrcid().pipe(
+ map((canDisconnect) => {
+ if (canDisconnect) {
+ return [new ItemOperation('unlinkOrcid', `${currentUrl}/unlink-orcid`)];
+ }
+ return [];
+ })
+ );
+ }
+
+ return combineLatest([ops$, orcidOps$]);
+ }),
+ map(([ops, orcidOps]: [ItemOperation[], ItemOperation[]]) => [...ops, ...orcidOps])
+ ).subscribe((ops) => this.operations$.next(ops));
this.itemPageRoute$ = this.itemRD$.pipe(
getAllSucceededRemoteDataPayload(),
@@ -206,6 +217,7 @@ export class ItemStatusComponent implements OnInit {
}
+
/**
* Get the current url without query params
* @returns {string} url