Merge pull request #2633 from vNovski/edit-item-view-random-order-of-buttons-in-status-tab

Edit-item view: random order of buttons in status tab
This commit is contained in:
Tim Donohue
2023-11-13 16:05:27 -06:00
committed by GitHub
4 changed files with 130 additions and 102 deletions

View File

@@ -28,4 +28,12 @@ export class ItemOperation {
this.disabled = disabled; this.disabled = disabled;
} }
/**
* Set whether this operation is authorized
* @param authorized
*/
setAuthorized(authorized: boolean): void {
this.authorized = authorized;
}
} }

View File

@@ -27,7 +27,7 @@
</div> </div>
<div *ngFor="let operation of (operations$ | async)" class="w-100" [ngClass]="{'pt-3': operation}"> <div *ngFor="let operation of (operations$ | async)" class="w-100" [ngClass]="{'pt-3': operation}">
<ds-item-operation *ngIf="operation" [operation]="operation"></ds-item-operation> <ds-item-operation [operation]="operation"></ds-item-operation>
</div> </div>
</div> </div>

View File

@@ -16,6 +16,7 @@ import { AuthorizationDataService } from '../../../core/data/feature-authorizati
import { IdentifierDataService } from '../../../core/data/identifier-data.service'; import { IdentifierDataService } from '../../../core/data/identifier-data.service';
import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model'; import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
let mockIdentifierDataService: IdentifierDataService; let mockIdentifierDataService: IdentifierDataService;
let mockConfigurationDataService: ConfigurationDataService; let mockConfigurationDataService: ConfigurationDataService;
@@ -57,12 +58,18 @@ describe('ItemStatusComponent', () => {
}; };
let authorizationService: AuthorizationDataService; let authorizationService: AuthorizationDataService;
let orcidAuthService: any;
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
authorizationService = jasmine.createSpyObj('authorizationService', { authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true) isAuthorized: observableOf(true)
}); });
orcidAuthService = jasmine.createSpyObj('OrcidAuthService', {
onlyAdminCanDisconnectProfileFromOrcid: observableOf ( true ),
isLinkedToOrcid: true
});
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
declarations: [ItemStatusComponent], declarations: [ItemStatusComponent],
@@ -71,7 +78,8 @@ describe('ItemStatusComponent', () => {
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: AuthorizationDataService, useValue: authorizationService }, { provide: AuthorizationDataService, useValue: authorizationService },
{ provide: IdentifierDataService, useValue: mockIdentifierDataService }, { provide: IdentifierDataService, useValue: mockIdentifierDataService },
{ provide: ConfigurationDataService, useValue: mockConfigurationDataService } { provide: ConfigurationDataService, useValue: mockConfigurationDataService },
{ provide: OrcidAuthService, useValue: orcidAuthService },
], schemas: [CUSTOM_ELEMENTS_SCHEMA] ], schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents(); }).compileComponents();
})); }));

View File

@@ -3,21 +3,20 @@ import { fadeIn, fadeInOut } from '../../../shared/animations/fade';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { ItemOperation } from '../item-operation/itemOperation.model'; import { ItemOperation } from '../item-operation/itemOperation.model';
import { distinctUntilChanged, map, mergeMap, switchMap, toArray } from 'rxjs/operators'; import { concatMap, distinctUntilChanged, first, map, mergeMap, switchMap, toArray } from 'rxjs/operators';
import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { BehaviorSubject, combineLatest, Observable, of, Subscription } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths'; import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { hasValue, isNotEmpty } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
import { import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, } from '../../../core/shared/operators';
getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, getFirstSucceededRemoteData, getRemoteDataPayload,
} from '../../../core/shared/operators';
import { IdentifierDataService } from '../../../core/data/identifier-data.service'; import { IdentifierDataService } from '../../../core/data/identifier-data.service';
import { Identifier } from '../../../shared/object-list/identifier-data/identifier.model'; import { Identifier } from '../../../shared/object-list/identifier-data/identifier.model';
import { ConfigurationProperty } from '../../../core/shared/configuration-property.model'; import { ConfigurationProperty } from '../../../core/shared/configuration-property.model';
import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
import { IdentifierData } from '../../../shared/object-list/identifier-data/identifier-data.model'; import { IdentifierData } from '../../../shared/object-list/identifier-data/identifier-data.model';
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
@Component({ @Component({
selector: 'ds-item-status', selector: 'ds-item-status',
@@ -73,6 +72,7 @@ export class ItemStatusComponent implements OnInit {
private authorizationService: AuthorizationDataService, private authorizationService: AuthorizationDataService,
private identifierDataService: IdentifierDataService, private identifierDataService: IdentifierDataService,
private configurationService: ConfigurationDataService, private configurationService: ConfigurationDataService,
private orcidAuthService: OrcidAuthService
) { ) {
} }
@@ -82,8 +82,10 @@ export class ItemStatusComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.itemRD$ = this.route.parent.data.pipe(map((data) => data.dso)); this.itemRD$ = this.route.parent.data.pipe(map((data) => data.dso));
this.itemRD$.pipe( this.itemRD$.pipe(
first(),
map((data: RemoteData<Item>) => data.payload) map((data: RemoteData<Item>) => data.payload)
).subscribe((item: Item) => { ).pipe(
switchMap((item: Item) => {
this.statusData = Object.assign({ this.statusData = Object.assign({
id: item.id, id: item.id,
handle: item.handle, handle: item.handle,
@@ -105,49 +107,41 @@ export class ItemStatusComponent implements OnInit {
// Observable for configuration determining whether the Register DOI feature is enabled // Observable for configuration determining whether the Register DOI feature is enabled
let registerConfigEnabled$: Observable<boolean> = this.configurationService.findByPropertyName('identifiers.item-status.register-doi').pipe( let registerConfigEnabled$: Observable<boolean> = this.configurationService.findByPropertyName('identifiers.item-status.register-doi').pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
map((rd: RemoteData<ConfigurationProperty>) => { map((enabledRD: RemoteData<ConfigurationProperty>) => enabledRD.hasSucceeded && enabledRD.payload.values.length > 0)
// 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;
})
); );
/* /**
Construct a base list of operations. * Construct a base list of operations.
The key is used to build messages * The key is used to build messages
i18n example: 'item.edit.tabs.status.buttons.<key>.label' * i18n example: 'item.edit.tabs.status.buttons.<key>.label'
The value is supposed to be a href for the button * The value is supposed to be a href for the button
*/ */
const operations: ItemOperation[] = []; const currentUrl = this.getCurrentUrl(item);
operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations', FeatureID.CanManagePolicies, true)); const inititalOperations: ItemOperation[] = [
operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper', FeatureID.CanManageMappings, true)); new ItemOperation('authorizations', `${currentUrl}/authorizations`, FeatureID.CanManagePolicies, true),
if (item.isWithdrawn) { new ItemOperation('mappedCollections', `${currentUrl}/mapper`, FeatureID.CanManageMappings, true),
operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate', FeatureID.ReinstateItem, true)); item.isWithdrawn
} else { ? new ItemOperation('reinstate', `${currentUrl}/reinstate`, FeatureID.ReinstateItem, true)
operations.push(new ItemOperation('withdraw', this.getCurrentUrl(item) + '/withdraw', FeatureID.WithdrawItem, true)); : new ItemOperation('withdraw', `${currentUrl}/withdraw`, FeatureID.WithdrawItem, true),
} item.isDiscoverable
if (item.isDiscoverable) { ? new ItemOperation('private', `${currentUrl}/private`, FeatureID.CanMakePrivate, true)
operations.push(new ItemOperation('private', this.getCurrentUrl(item) + '/private', FeatureID.CanMakePrivate, true)); : new ItemOperation('public', `${currentUrl}/public`, FeatureID.CanMakePrivate, true),
} else { new ItemOperation('move', `${currentUrl}/move`, FeatureID.CanMove, true),
operations.push(new ItemOperation('public', this.getCurrentUrl(item) + '/public', FeatureID.CanMakePrivate, true)); new ItemOperation('delete', `${currentUrl}/delete`, FeatureID.CanDelete, 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);
/* 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 * 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
*/ */
this.identifierDataService.getIdentifierDataFor(item).pipe( const ops$ = this.identifierDataService.getIdentifierDataFor(item).pipe(
getFirstSucceededRemoteData(), getFirstCompletedRemoteData(),
getRemoteDataPayload(), mergeMap((dataRD: RemoteData<IdentifierData>) => {
mergeMap((data: IdentifierData) => { if (dataRD.hasSucceeded) {
let identifiers = data.identifiers; let identifiers = dataRD.payload.identifiers;
let no_doi = true; let no_doi = true;
let pending = false; let pending = false;
if (identifiers !== undefined && identifiers !== null) { if (identifiers !== undefined && identifiers !== null) {
@@ -155,8 +149,7 @@ export class ItemStatusComponent implements OnInit {
if (hasValue(identifier) && identifier.identifierType === 'doi') { if (hasValue(identifier) && identifier.identifierType === 'doi') {
// The item has some kind of DOI // The item has some kind of DOI
no_doi = false; no_doi = false;
if (identifier.identifierStatus === 'PENDING' || identifier.identifierStatus === 'MINTED' if (['PENDING', 'MINTED', null].includes(identifier.identifierStatus)) {
|| identifier.identifierStatus == null) {
// The item's DOI is pending, minted or null. // The item's DOI is pending, minted or null.
// It isn't registered, reserved, queued for registration or reservation or update, deleted // It isn't registered, reserved, queued for registration or reservation or update, deleted
// or queued for deletion. // or queued for deletion.
@@ -171,33 +164,51 @@ export class ItemStatusComponent implements OnInit {
return enabled && (pending || no_doi); 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 // Switch map pushes the register DOI operation onto a copy of the base array then returns to the pipe
switchMap((showDoi: boolean) => { switchMap((showDoi: boolean) => {
let ops = [...operations]; const ops = [...inititalOperations];
if (showDoi) { if (showDoi) {
ops.push(new ItemOperation('register-doi', this.getCurrentUrl(item) + '/register-doi', FeatureID.CanRegisterDOI, true)); const op = new ItemOperation('register-doi', `${currentUrl}/register-doi`, FeatureID.CanRegisterDOI, true);
ops.splice(ops.length - 1, 0, op); // Add item before last
} }
return ops; return inititalOperations;
}), }),
// Merge map checks and transforms each operation in the array based on whether it is authorized or not (disabled) concatMap((op: ItemOperation) => {
mergeMap((op: ItemOperation) => {
if (hasValue(op.featureID)) { if (hasValue(op.featureID)) {
return this.authorizationService.isAuthorized(op.featureID, item.self).pipe( return this.authorizationService.isAuthorized(op.featureID, item.self).pipe(
distinctUntilChanged(), distinctUntilChanged(),
map((authorized) => new ItemOperation(op.operationKey, op.operationUrl, op.featureID, !authorized, authorized)) map((authorized) => {
op.setDisabled(!authorized);
op.setAuthorized(authorized);
return op;
})
); );
} else {
return [op];
} }
return [op];
}), }),
// Wait for all operations to be emitted and return as an array toArray()
toArray(), );
).subscribe((data) => {
// Update the operations$ subject that draws the administrative buttons on the status page let orcidOps$ = of([]);
this.operations$.next(data); 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( this.itemPageRoute$ = this.itemRD$.pipe(
getAllSucceededRemoteDataPayload(), getAllSucceededRemoteDataPayload(),
@@ -206,6 +217,7 @@ export class ItemStatusComponent implements OnInit {
} }
/** /**
* Get the current url without query params * Get the current url without query params
* @returns {string} url * @returns {string} url