mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge pull request #2638 from DSpace/backport-2633-to-dspace-7_x
[Port dspace-7_x] Edit-item view: random order of buttons in status tab
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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();
|
||||
}));
|
||||
|
@@ -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<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(
|
||||
@@ -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(),
|
||||
@@ -206,6 +217,7 @@ export class ItemStatusComponent implements OnInit {
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the current url without query params
|
||||
* @returns {string} url
|
||||
|
Reference in New Issue
Block a user