From 35f8b55f588985146482313a6ebad80154b6edbd Mon Sep 17 00:00:00 2001 From: Vlad Nouski Date: Tue, 31 Oct 2023 14:21:42 +0100 Subject: [PATCH] fix: random order of buttons in status tab --- .../item-operation/itemOperation.model.ts | 8 + .../item-status/item-status.component.html | 2 +- .../item-status/item-status.component.ts | 212 +++++++++--------- 3 files changed, 121 insertions(+), 101 deletions(-) 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.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