Skip to content

Commit

Permalink
Merge pull request #2633 from vNovski/edit-item-view-random-order-of-…
Browse files Browse the repository at this point in the history
…buttons-in-status-tab

Edit-item view: random order of buttons in status tab
  • Loading branch information
tdonohue authored Nov 13, 2023
2 parents e99fff8 + fbbbc18 commit 7ad5a5b
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 102 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
</div>

<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>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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],
Expand All @@ -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();
}));
Expand Down
212 changes: 112 additions & 100 deletions src/app/item-page/edit-item-page/item-status/item-status.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -73,6 +72,7 @@ export class ItemStatusComponent implements OnInit {
private authorizationService: AuthorizationDataService,
private identifierDataService: IdentifierDataService,
private configurationService: ConfigurationDataService,
private orcidAuthService: OrcidAuthService
) {
}

Expand All @@ -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<Item>) => 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(
Expand All @@ -105,99 +107,108 @@ export class ItemStatusComponent implements OnInit {
// Observable for configuration determining whether the Register DOI feature is enabled
let registerConfigEnabled$: Observable<boolean> = this.configurationService.findByPropertyName('identifiers.item-status.register-doi').pipe(
getFirstCompletedRemoteData(),
map((rd: RemoteData<ConfigurationProperty>) => {
// 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<ConfigurationProperty>) => 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.<key>.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.<key>.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<IdentifierData>) => {
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(),
Expand All @@ -206,6 +217,7 @@ export class ItemStatusComponent implements OnInit {

}


/**
* Get the current url without query params
* @returns {string} url
Expand Down

0 comments on commit 7ad5a5b

Please sign in to comment.